commit 9f867b19da0961b2af45c35b00caaf2df895e3e3 Author: CN-JS-HuiBai Date: Tue Apr 14 22:41:14 2026 +0800 First Commmit diff --git a/.fpm_openwrt b/.fpm_openwrt new file mode 100644 index 00000000..3223ec8a --- /dev/null +++ b/.fpm_openwrt @@ -0,0 +1,31 @@ +-s dir +--name sing-box +--category net +--license GPL-3.0-or-later +--description "The universal proxy platform." +--url "https://sing-box.sagernet.org/" +--maintainer "nekohasekai " +--no-deb-generate-changes + +--config-files /etc/config/sing-box +--config-files /etc/sing-box/config.json + +--depends ca-bundle +--depends kmod-inet-diag +--depends kmod-tun +--depends firewall4 +--depends kmod-nft-queue + +--before-remove release/config/openwrt.prerm + +release/config/config.json=/etc/sing-box/config.json + +release/config/openwrt.conf=/etc/config/sing-box +release/config/openwrt.init=/etc/init.d/sing-box +release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box + +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 +release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box + +LICENSE=/usr/share/licenses/sing-box/LICENSE diff --git a/.fpm_pacman b/.fpm_pacman new file mode 100644 index 00000000..8c86dfd9 --- /dev/null +++ b/.fpm_pacman @@ -0,0 +1,23 @@ +-s dir +--name sing-box +--category net +--license GPL-3.0-or-later +--description "The universal proxy platform." +--url "https://sing-box.sagernet.org/" +--maintainer "nekohasekai " +--config-files etc/sing-box/config.json +--after-install release/config/sing-box.postinst + +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 +release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box + +LICENSE=/usr/share/licenses/sing-box/LICENSE diff --git a/.fpm_systemd b/.fpm_systemd new file mode 100644 index 00000000..9b455da9 --- /dev/null +++ b/.fpm_systemd @@ -0,0 +1,26 @@ +-s dir +--name sing-box +--category net +--license GPL-3.0-or-later +--description "The universal proxy platform." +--url "https://sing-box.sagernet.org/" +--vendor SagerNet +--maintainer "nekohasekai " +--deb-field "Bug: https://github.com/SagerNet/sing-box/issues" +--no-deb-generate-changes +--config-files /etc/sing-box/config.json +--after-install release/config/sing-box.postinst + +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 +release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box + +LICENSE=/usr/share/licenses/sing-box/LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d2b74d08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +/.idea/ +/vendor/ +/*.json +/*.srs +/*.db +/site/ +/bin/ +/dist/ +/sing-box +/sing-box.exe +/build/ +/*.jar +/*.aar +/*.xcframework/ +/experimental/libbox/*.aar +/experimental/libbox/*.xcframework/ +/experimental/libbox/*.nupkg +.DS_Store +/config.d/ +/venv/ +CLAUDE.md +AGENTS.md +/.claude/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..45ffb563 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "clients/apple"] + path = clients/apple + url = https://github.com/SagerNet/sing-box-for-apple.git +[submodule "clients/android"] + path = clients/android + url = https://github.com/SagerNet/sing-box-for-android.git diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..d6905dc1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,64 @@ +version: "2" +run: + go: "1.25" + build-tags: + - with_gvisor + - with_quic + - with_dhcp + - with_wireguard + - with_utls + - with_acme + - with_clash_api + - with_tailscale + - with_ccm + - with_ocm + - badlinkname + - tfogo_checklinkname0 +linters: + default: none + enable: + - govet + - ineffassign + - paralleltest + - staticcheck + settings: + staticcheck: + checks: + - all + - -S1000 + - -S1008 + - -S1017 + - -ST1003 + - -QF1001 + - -QF1003 + - -QF1008 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - transport/simple-obfs + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofumpt + settings: + gci: + sections: + - standard + - prefix(github.com/sagernet/) + - default + custom-order: true + exclusions: + generated: lax + paths: + - transport/simple-obfs + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c8600d57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder +LABEL maintainer="nekohasekai " +COPY . /go/src/github.com/sagernet/sing-box +WORKDIR /go/src/github.com/sagernet/sing-box +ARG TARGETOS TARGETARCH +ARG GOPROXY="" +ENV GOPROXY ${GOPROXY} +ENV CGO_ENABLED=0 +ENV GOOS=$TARGETOS +ENV GOARCH=$TARGETARCH +RUN set -ex \ + && apk add git build-base \ + && export COMMIT=$(git rev-parse --short HEAD) \ + && export VERSION=$(go run ./cmd/internal/read_tag) \ + && export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \ + && export LDFLAGS_SHARED=$(cat release/LDFLAGS) \ + && go build -v -trimpath -tags "$TAGS" \ + -o /go/bin/sing-box \ + -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" $LDFLAGS_SHARED -s -w -buildid=" \ + ./cmd/sing-box +FROM --platform=$TARGETPLATFORM alpine AS dist +LABEL maintainer="nekohasekai " +RUN set -ex \ + && apk add --no-cache --upgrade bash tzdata ca-certificates nftables +COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box +ENTRYPOINT ["sing-box"] diff --git a/Dockerfile.binary b/Dockerfile.binary new file mode 100644 index 00000000..78fc5667 --- /dev/null +++ b/Dockerfile.binary @@ -0,0 +1,14 @@ +ARG BASE_IMAGE=alpine +FROM ${BASE_IMAGE} +ARG TARGETARCH +ARG TARGETVARIANT +LABEL maintainer="nekohasekai " +RUN set -ex \ + && if command -v apk > /dev/null; then \ + apk add --no-cache --upgrade bash tzdata ca-certificates nftables; \ + else \ + apt-get update && apt-get install -y --no-install-recommends bash tzdata ca-certificates nftables \ + && rm -rf /var/lib/apt/lists/*; \ + fi +COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box +ENTRYPOINT ["sing-box"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..175f3503 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +In addition, no derivative work may use the name or imply association +with this application without prior consent. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..1a1138cc --- /dev/null +++ b/Makefile @@ -0,0 +1,276 @@ +NAME = sing-box +COMMIT = $(shell git rev-parse --short HEAD) +TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS) + +GOHOSTOS = $(shell go env GOHOSTOS) +GOHOSTARCH = $(shell go env GOHOSTARCH) +VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest) + +LDFLAGS_SHARED = $(shell cat release/LDFLAGS) +PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid=" +MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" +MAIN = ./cmd/sing-box +PREFIX ?= $(shell go env GOPATH) +SING_FFI ?= sing-ffi +LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json + +.PHONY: test release docs build + +build: + export GOTOOLCHAIN=local && \ + go build $(MAIN_PARAMS) $(MAIN) + +race: + export GOTOOLCHAIN=local && \ + go build -race $(MAIN_PARAMS) $(MAIN) + +ci_build: + export GOTOOLCHAIN=local && \ + go build $(PARAMS) $(MAIN) && \ + go build $(MAIN_PARAMS) $(MAIN) + +generate_completions: + go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN) + +install: + go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) + +fmt: + @gofumpt -l -w . + @gofmt -s -w . + @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . + +fmt_docs: + go run ./cmd/internal/format_docs + +fmt_install: + go install -v mvdan.cc/gofumpt@latest + go install -v github.com/daixiang0/gci@latest + +lint: + GOOS=linux golangci-lint run ./... + GOOS=android golangci-lint run ./... + GOOS=windows golangci-lint run ./... + GOOS=darwin golangci-lint run ./... + GOOS=freebsd golangci-lint run ./... + +lint_install: + go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + +proto: + @go run ./cmd/internal/protogen + @gofumpt -l -w . + @gofumpt -l -w . + +proto_install: + go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +update_certificates: + go run ./cmd/internal/update_certificates + +release: + go run ./cmd/internal/build goreleaser release --clean --skip publish + mkdir dist/release + mv dist/*.tar.gz \ + dist/*.zip \ + dist/*.deb \ + dist/*.rpm \ + dist/*_amd64.pkg.tar.zst \ + dist/*_arm64.pkg.tar.zst \ + dist/release + ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release + rm -r dist/release + +release_repo: + go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean + +release_install: + go install -v github.com/tcnksm/ghr@latest + +update_android_version: + go run ./cmd/internal/update_android_version + +build_android: + cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop + +upload_android: + mkdir -p dist/release_android + cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android + cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android + ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android + rm -rf dist/release_android + +release_android: lib_android update_android_version build_android upload_android + +publish_android: + cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop + +# TODO: find why and remove `-destination 'generic/platform=iOS'` +# TODO: remove xcode clean when fix control widget fixed +build_ios: + cd ../sing-box-for-apple && \ + rm -rf build/SFI.xcarchive && \ + xcodebuild clean -scheme SFI && \ + xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌" + +upload_ios_app_store: + cd ../sing-box-for-apple && \ + xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates + +export_ios_ipa: + cd ../sing-box-for-apple && \ + xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \ + cp build/SFI/sing-box.ipa dist/SFI.ipa + +upload_ios_ipa: + cd dist && \ + cp SFI.ipa "SFI-${VERSION}.ipa" && \ + ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa" + +release_ios: build_ios upload_ios_app_store + +build_macos: + cd ../sing-box-for-apple && \ + rm -rf build/SFM.xcarchive && \ + xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌" + +upload_macos_app_store: + cd ../sing-box-for-apple && \ + xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates + +release_macos: build_macos upload_macos_app_store + +build_macos_standalone: + $(MAKE) -C ../sing-box-for-apple archive_macos_standalone + +build_macos_dmg: + $(MAKE) -C ../sing-box-for-apple build_macos_dmg + +build_macos_pkg: + $(MAKE) -C ../sing-box-for-apple build_macos_pkg + +notarize_macos_dmg: + $(MAKE) -C ../sing-box-for-apple notarize_macos_dmg + +notarize_macos_pkg: + $(MAKE) -C ../sing-box-for-apple notarize_macos_pkg + +upload_macos_dmg: + mkdir -p dist/SFM + cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg" + cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg" + cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg" + +upload_macos_pkg: + mkdir -p dist/SFM + cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg" + cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg" + cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg" + +upload_macos_dsyms: + mkdir -p dist/SFM + cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs + cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip" + ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip" + +release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms + +build_tvos: + cd ../sing-box-for-apple && \ + rm -rf build/SFT.xcarchive && \ + xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌" + +upload_tvos_app_store: + cd ../sing-box-for-apple && \ + xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates + +export_tvos_ipa: + cd ../sing-box-for-apple && \ + xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \ + cp build/SFT/sing-box.ipa dist/SFT.ipa + +upload_tvos_ipa: + cd dist && \ + cp SFT.ipa "SFT-${VERSION}.ipa" && \ + ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa" + +release_tvos: build_tvos upload_tvos_app_store + +update_apple_version: + go run ./cmd/internal/update_apple_version + +update_macos_version: + MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version + +release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone + +release_apple_beta: update_apple_version release_ios release_macos release_tvos + +publish_testflight: + go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS)) + +prepare_app_store: + go run -v ./cmd/internal/app_store_connect prepare_app_store + +publish_app_store: + go run -v ./cmd/internal/app_store_connect publish_app_store + +test: + @go test -v ./... && \ + cd test && \ + go mod tidy && \ + go test -v -tags "$(TAGS_TEST)" . + +test_stdio: + @go test -v ./... && \ + cd test && \ + go mod tidy && \ + go test -v -tags "$(TAGS_TEST),force_stdio" . + +lib_android: + go run ./cmd/internal/build_libbox -target android + +lib_apple: + go run ./cmd/internal/build_libbox -target apple + +lib_windows: + $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp + +lib_android_new: + $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android + +lib_apple_new: + $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple + +lib_install: + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12 + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12 + +docs: + venv/bin/mkdocs serve + +publish_docs: + venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history + +docs_install: + python3 -m venv venv + source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*" + +clean: + rm -rf bin dist sing-box + rm -f $(shell go env GOPATH)/sing-box + +update: + git fetch + git reset FETCH_HEAD --hard + git clean -fdx + +%: + @: diff --git a/README.md b/README.md new file mode 100644 index 00000000..90be2a83 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents + + +Warp sponsorship + + +--- + +# sing-box + +The universal proxy platform. + +[![Packaging status](https://repology.org/badge/vertical-allrepos/sing-box.svg)](https://repology.org/project/sing-box/versions) + +## Documentation + +https://sing-box.sagernet.org + +## License + +``` +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +In addition, no derivative work may use the name or imply association +with this application without prior consent. +``` \ No newline at end of file diff --git a/adapter/certificate.go b/adapter/certificate.go new file mode 100644 index 00000000..0998e130 --- /dev/null +++ b/adapter/certificate.go @@ -0,0 +1,21 @@ +package adapter + +import ( + "context" + "crypto/x509" + + "github.com/sagernet/sing/service" +) + +type CertificateStore interface { + LifecycleService + Pool() *x509.CertPool +} + +func RootPoolFromContext(ctx context.Context) *x509.CertPool { + store := service.FromContext[CertificateStore](ctx) + if store == nil { + return nil + } + return store.Pool() +} diff --git a/adapter/certificate/adapter.go b/adapter/certificate/adapter.go new file mode 100644 index 00000000..802020c1 --- /dev/null +++ b/adapter/certificate/adapter.go @@ -0,0 +1,21 @@ +package certificate + +type Adapter struct { + providerType string + providerTag string +} + +func NewAdapter(providerType string, providerTag string) Adapter { + return Adapter{ + providerType: providerType, + providerTag: providerTag, + } +} + +func (a *Adapter) Type() string { + return a.providerType +} + +func (a *Adapter) Tag() string { + return a.providerTag +} diff --git a/adapter/certificate/manager.go b/adapter/certificate/manager.go new file mode 100644 index 00000000..e4b9b535 --- /dev/null +++ b/adapter/certificate/manager.go @@ -0,0 +1,158 @@ +package certificate + +import ( + "context" + "os" + "sync" + "time" + + "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" + F "github.com/sagernet/sing/common/format" +) + +var _ adapter.CertificateProviderManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.CertificateProviderRegistry + access sync.Mutex + started bool + stage adapter.StartStage + providers []adapter.CertificateProviderService + providerByTag map[string]adapter.CertificateProviderService +} + +func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager { + return &Manager{ + logger: logger, + registry: registry, + providerByTag: make(map[string]adapter.CertificateProviderService), + } +} + +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 + providers := m.providers + m.access.Unlock() + for _, provider := range providers { + name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]" + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err := adapter.LegacyStart(provider, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if !m.started { + return nil + } + m.started = false + providers := m.providers + m.providers = nil + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, provider := range providers { + name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]" + m.logger.Trace("close ", name) + startTime := time.Now() + monitor.Start("close ", name) + err = E.Append(err, provider.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + monitor.Finish() + m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return err +} + +func (m *Manager) CertificateProviders() []adapter.CertificateProviderService { + m.access.Lock() + defer m.access.Unlock() + return m.providers +} + +func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) { + m.access.Lock() + provider, found := m.providerByTag[tag] + m.access.Unlock() + return provider, found +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + provider, found := m.providerByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.providerByTag, tag) + index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool { + return it == provider + }) + if index == -1 { + panic("invalid certificate provider index") + } + m.providers = append(m.providers[:index], m.providers[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return provider.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error { + provider, err := m.registry.Create(ctx, logger, tag, providerType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]" + for _, stage := range adapter.ListStartStages { + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err = adapter.LegacyStart(provider, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + if existsProvider, loaded := m.providerByTag[tag]; loaded { + if m.started { + err = existsProvider.Close() + if err != nil { + return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]") + } + } + existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool { + return it == existsProvider + }) + if existsIndex == -1 { + panic("invalid certificate provider index") + } + m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...) + } + m.providers = append(m.providers, provider) + m.providerByTag[tag] = provider + return nil +} diff --git a/adapter/certificate/registry.go b/adapter/certificate/registry.go new file mode 100644 index 00000000..5a080f2c --- /dev/null +++ b/adapter/certificate/registry.go @@ -0,0 +1,72 @@ +package certificate + +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.CertificateProviderService, error) + +func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) { + registry.register(providerType, func() any { + return new(Options) + }, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.CertificateProviderRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, 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(providerType string) (any, bool) { + m.access.Lock() + defer m.access.Unlock() + optionsConstructor, loaded := m.optionsType[providerType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) { + m.access.Lock() + defer m.access.Unlock() + constructor, loaded := m.constructor[providerType] + if !loaded { + return nil, E.New("certificate provider type not found: " + providerType) + } + return constructor(ctx, logger, tag, options) +} + +func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + m.access.Lock() + defer m.access.Unlock() + m.optionsType[providerType] = optionsConstructor + m.constructor[providerType] = constructor +} diff --git a/adapter/certificate_provider.go b/adapter/certificate_provider.go new file mode 100644 index 00000000..70bdeb88 --- /dev/null +++ b/adapter/certificate_provider.go @@ -0,0 +1,38 @@ +package adapter + +import ( + "context" + "crypto/tls" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type CertificateProvider interface { + GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) +} + +type ACMECertificateProvider interface { + CertificateProvider + GetACMENextProtos() []string +} + +type CertificateProviderService interface { + Lifecycle + Type() string + Tag() string + CertificateProvider +} + +type CertificateProviderRegistry interface { + option.CertificateProviderOptionsRegistry + Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error) +} + +type CertificateProviderManager interface { + Lifecycle + CertificateProviders() []CertificateProviderService + Get(tag string) (CertificateProviderService, bool) + Remove(tag string) error + Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error +} diff --git a/adapter/connections.go b/adapter/connections.go new file mode 100644 index 00000000..a0b9c0ef --- /dev/null +++ b/adapter/connections.go @@ -0,0 +1,18 @@ +package adapter + +import ( + "context" + "net" + + N "github.com/sagernet/sing/common/network" +) + +type ConnectionManager interface { + Lifecycle + Count() int + CloseAll() + TrackConn(conn net.Conn) net.Conn + TrackPacketConn(conn net.PacketConn) net.PacketConn + NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) +} diff --git a/adapter/dns.go b/adapter/dns.go new file mode 100644 index 00000000..7e2c5fa0 --- /dev/null +++ b/adapter/dns.go @@ -0,0 +1,98 @@ +package adapter + +import ( + "context" + "net/netip" + "time" + + 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" +) + +type DNSRouter interface { + Lifecycle + Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error) + Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error) + ClearCache() + LookupReverseMapping(ip netip.Addr) (string, bool) + ResetNetwork() +} + +type DNSClient interface { + Start() + Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) + Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) + ClearCache() +} + +type DNSQueryOptions struct { + Transport DNSTransport + Strategy C.DomainStrategy + LookupStrategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache 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, + DisableOptimisticCache: options.DisableOptimisticCache, + RewriteTTL: options.RewriteTTL, + ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), + }, nil +} + +type RDRCStore interface { + LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) + SaveRDRC(transportName string, qName string, qType uint16) error + SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) +} + +type DNSCacheStore interface { + LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) + SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error + SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) + ClearDNSCache() error +} + +type DNSTransport interface { + Lifecycle + Type() string + Tag() string + Dependencies() []string + Reset() + Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) +} + +type DNSTransportRegistry interface { + option.DNSTransportOptionsRegistry + CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error) +} + +type DNSTransportManager interface { + Lifecycle + Transports() []DNSTransport + Transport(tag string) (DNSTransport, bool) + Default() DNSTransport + FakeIP() FakeIPTransport + Remove(tag string) error + Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error +} diff --git a/adapter/endpoint.go b/adapter/endpoint.go new file mode 100644 index 00000000..f09f08ce --- /dev/null +++ b/adapter/endpoint.go @@ -0,0 +1,28 @@ +package adapter + +import ( + "context" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type Endpoint interface { + Lifecycle + Type() string + Tag() string + Outbound +} + +type EndpointRegistry interface { + option.EndpointOptionsRegistry + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error) +} + +type EndpointManager interface { + Lifecycle + Endpoints() []Endpoint + Get(tag string) (Endpoint, bool) + Remove(tag string) error + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error +} diff --git a/adapter/endpoint/adapter.go b/adapter/endpoint/adapter.go new file mode 100644 index 00000000..c75e4d83 --- /dev/null +++ b/adapter/endpoint/adapter.go @@ -0,0 +1,43 @@ +package endpoint + +import "github.com/sagernet/sing-box/option" + +type Adapter struct { + endpointType string + endpointTag string + network []string + dependencies []string +} + +func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter { + return Adapter{ + endpointType: endpointType, + endpointTag: endpointTag, + network: network, + dependencies: dependencies, + } +} + +func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter { + var dependencies []string + if dialOptions.Detour != "" { + dependencies = []string{dialOptions.Detour} + } + return NewAdapter(endpointType, endpointTag, network, dependencies) +} + +func (a *Adapter) Type() string { + return a.endpointType +} + +func (a *Adapter) Tag() string { + return a.endpointTag +} + +func (a *Adapter) Network() []string { + return a.network +} + +func (a *Adapter) Dependencies() []string { + return a.dependencies +} diff --git a/adapter/endpoint/manager.go b/adapter/endpoint/manager.go new file mode 100644 index 00000000..8b7c287f --- /dev/null +++ b/adapter/endpoint/manager.go @@ -0,0 +1,161 @@ +package endpoint + +import ( + "context" + "os" + "sync" + "time" + + "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" + F "github.com/sagernet/sing/common/format" +) + +var _ adapter.EndpointManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.EndpointRegistry + access sync.Mutex + started bool + stage adapter.StartStage + endpoints []adapter.Endpoint + endpointByTag map[string]adapter.Endpoint +} + +func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager { + return &Manager{ + logger: logger, + registry: registry, + endpointByTag: make(map[string]adapter.Endpoint), + } +} + +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 + if stage == adapter.StartStateStart { + // started with outbound manager + return nil + } + for _, endpoint := range m.endpoints { + name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err := adapter.LegacyStart(endpoint, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if !m.started { + return nil + } + m.started = false + endpoints := m.endpoints + m.endpoints = nil + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, endpoint := range endpoints { + name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" + m.logger.Trace("close ", name) + startTime := time.Now() + monitor.Start("close ", name) + err = E.Append(err, endpoint.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + monitor.Finish() + m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func (m *Manager) Endpoints() []adapter.Endpoint { + m.access.Lock() + defer m.access.Unlock() + return m.endpoints +} + +func (m *Manager) Get(tag string) (adapter.Endpoint, bool) { + m.access.Lock() + defer m.access.Unlock() + endpoint, found := m.endpointByTag[tag] + return endpoint, found +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + endpoint, found := m.endpointByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.endpointByTag, tag) + index := common.Index(m.endpoints, func(it adapter.Endpoint) bool { + return it == endpoint + }) + if index == -1 { + panic("invalid endpoint index") + } + m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return endpoint.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error { + endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" + for _, stage := range adapter.ListStartStages { + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err = adapter.LegacyStart(endpoint, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { + if m.started { + err = existsEndpoint.Close() + if err != nil { + return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]") + } + } + existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool { + return it == existsEndpoint + }) + if existsIndex == -1 { + panic("invalid endpoint index") + } + m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...) + } + m.endpoints = append(m.endpoints, endpoint) + m.endpointByTag[tag] = endpoint + return nil +} diff --git a/adapter/endpoint/registry.go b/adapter/endpoint/registry.go new file mode 100644 index 00000000..92cb9025 --- /dev/null +++ b/adapter/endpoint/registry.go @@ -0,0 +1,72 @@ +package endpoint + +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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.EndpointRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, 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, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, 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, router, 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 +} diff --git a/adapter/experimental.go b/adapter/experimental.go new file mode 100644 index 00000000..49fd2bd3 --- /dev/null +++ b/adapter/experimental.go @@ -0,0 +1,152 @@ +package adapter + +import ( + "bytes" + "context" + "encoding/binary" + "io" + "time" + + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/common/varbin" +) + +type ClashServer interface { + LifecycleService + ConnectionTracker + Mode() string + ModeList() []string + SetModeUpdateHook(hook *observable.Subscriber[struct{}]) + HistoryStorage() URLTestHistoryStorage +} + +type URLTestHistory struct { + Time time.Time `json:"time"` + Delay uint16 `json:"delay"` +} + +type URLTestHistoryStorage interface { + SetHook(hook *observable.Subscriber[struct{}]) + LoadURLTestHistory(tag string) *URLTestHistory + DeleteURLTestHistory(tag string) + StoreURLTestHistory(tag string, history *URLTestHistory) + Close() error +} + +type V2RayServer interface { + LifecycleService + StatsService() ConnectionTracker +} + +type CacheFile interface { + LifecycleService + + StoreFakeIP() bool + FakeIPStorage + + StoreRDRC() bool + RDRCStore + + StoreDNS() bool + DNSCacheStore + + SetDisableExpire(disableExpire bool) + SetOptimisticTimeout(timeout time.Duration) + + LoadMode() string + StoreMode(mode string) error + LoadSelected(group string) string + StoreSelected(group string, selected string) error + LoadGroupExpand(group string) (isExpand bool, loaded bool) + StoreGroupExpand(group string, expand bool) error + LoadRuleSet(tag string) *SavedBinary + SaveRuleSet(tag string, set *SavedBinary) error +} + +type SavedBinary struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedBinary) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + _, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + _, err = buffer.Write(s.Content) + if err != nil { + return nil, err + } + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + _, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag))) + if err != nil { + return nil, err + } + _, err = buffer.WriteString(s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedBinary) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLength, err := binary.ReadUvarint(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLength) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + etagLength, err := binary.ReadUvarint(reader) + if err != nil { + return err + } + etagBytes := make([]byte, etagLength) + _, err = io.ReadFull(reader, etagBytes) + if err != nil { + return err + } + s.LastEtag = string(etagBytes) + return nil +} + +type OutboundGroup interface { + Outbound + Now() string + All() []string +} + +type URLTestGroup interface { + OutboundGroup + URLTest(ctx context.Context) (map[string]uint16, error) +} + +func OutboundTag(detour Outbound) string { + if group, isGroup := detour.(OutboundGroup); isGroup { + return group.Now() + } + return detour.Tag() +} diff --git a/adapter/fakeip.go b/adapter/fakeip.go new file mode 100644 index 00000000..0787c146 --- /dev/null +++ b/adapter/fakeip.go @@ -0,0 +1,31 @@ +package adapter + +import ( + "net/netip" + + "github.com/sagernet/sing/common/logger" +) + +type FakeIPStore interface { + SimpleLifecycle + Contains(address netip.Addr) bool + Create(domain string, isIPv6 bool) (netip.Addr, error) + Lookup(address netip.Addr) (string, bool) + Reset() error +} + +type FakeIPStorage interface { + FakeIPMetadata() *FakeIPMetadata + FakeIPSaveMetadata(metadata *FakeIPMetadata) error + FakeIPSaveMetadataAsync(metadata *FakeIPMetadata) + FakeIPStore(address netip.Addr, domain string) error + FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) + FakeIPLoad(address netip.Addr) (string, bool) + FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) + FakeIPReset() error +} + +type FakeIPTransport interface { + DNSTransport + Store() FakeIPStore +} diff --git a/adapter/fakeip_metadata.go b/adapter/fakeip_metadata.go new file mode 100644 index 00000000..7df77d42 --- /dev/null +++ b/adapter/fakeip_metadata.go @@ -0,0 +1,50 @@ +package adapter + +import ( + "bytes" + "encoding" + "encoding/binary" + "io" + "net/netip" + + "github.com/sagernet/sing/common" +) + +type FakeIPMetadata struct { + Inet4Range netip.Prefix + Inet6Range netip.Prefix + Inet4Current netip.Addr + Inet6Current netip.Addr +} + +func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) { + var buffer bytes.Buffer + for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} { + data, err = marshaler.MarshalBinary() + if err != nil { + return + } + common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data)))) + buffer.Write(data) + } + data = buffer.Bytes() + return +} + +func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} { + var length uint16 + common.Must(binary.Read(reader, binary.BigEndian, &length)) + element := make([]byte, length) + _, err := io.ReadFull(reader, element) + if err != nil { + return err + } + err = unmarshaler.UnmarshalBinary(element) + if err != nil { + return err + } + } + return nil +} diff --git a/adapter/handler.go b/adapter/handler.go new file mode 100644 index 00000000..f8912110 --- /dev/null +++ b/adapter/handler.go @@ -0,0 +1,61 @@ +package adapter + +import ( + "context" + "net" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +// Deprecated +type ConnectionHandler interface { + NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error +} + +type ConnectionHandlerEx interface { + NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) +} + +// Deprecated: use PacketHandlerEx instead +type PacketHandler interface { + NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error +} + +type PacketHandlerEx interface { + NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) +} + +// Deprecated: use OOBPacketHandlerEx instead +type OOBPacketHandler interface { + NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error +} + +type OOBPacketHandlerEx interface { + NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) +} + +// Deprecated +type PacketConnectionHandler interface { + NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error +} + +type PacketConnectionHandlerEx interface { + NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) +} + +// Deprecated: use TCPConnectionHandlerEx instead +// +//nolint:staticcheck +type UpstreamHandlerAdapter interface { + N.TCPConnectionHandler + N.UDPConnectionHandler + E.Handler +} + +type UpstreamHandlerAdapterEx interface { + N.TCPConnectionHandlerEx + N.UDPConnectionHandlerEx +} diff --git a/adapter/inbound.go b/adapter/inbound.go new file mode 100644 index 00000000..6f53b122 --- /dev/null +++ b/adapter/inbound.go @@ -0,0 +1,195 @@ +package adapter + +import ( + "context" + "net" + "net/netip" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + M "github.com/sagernet/sing/common/metadata" + + "github.com/miekg/dns" +) + +type Inbound interface { + Lifecycle + Type() string + Tag() string +} + +type TCPInjectableInbound interface { + Inbound + ConnectionHandlerEx +} + +type UDPInjectableInbound interface { + Inbound + PacketConnectionHandlerEx +} + +type InboundRegistry interface { + option.InboundOptionsRegistry + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error) +} + +type InboundManager interface { + Lifecycle + Inbounds() []Inbound + Get(tag string) (Inbound, bool) + Remove(tag string) error + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error +} + +type InboundContext struct { + Inbound string + InboundType string + IPVersion uint8 + Network string + Source M.Socksaddr + Destination M.Socksaddr + User string + Outbound string + + // sniffer + + Protocol string + Domain string + Client string + SniffContext any + SnifferNames []string + SniffError error + + // cache + + // Deprecated: implement in rule action + InboundDetour string + LastInbound string + OriginDestination M.Socksaddr + RouteOriginalDestination M.Socksaddr + UDPDisableDomainUnmapping bool + UDPConnect bool + UDPTimeout time.Duration + TLSFragment bool + TLSFragmentFallbackDelay time.Duration + TLSRecordFragment bool + + NetworkStrategy *C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType + FallbackDelay time.Duration + + DestinationAddresses []netip.Addr + DNSResponse *dns.Msg + DestinationAddressMatchFromResponse bool + SourceGeoIPCode string + GeoIPCode string + ProcessInfo *ConnectionOwner + SourceMACAddress net.HardwareAddr + SourceHostname string + QueryType uint16 + FakeIP bool + + // rule cache + + IPCIDRMatchSource bool + IPCIDRAcceptEmpty bool + + SourceAddressMatch bool + SourcePortMatch bool + DestinationAddressMatch bool + DestinationPortMatch bool + DidMatch bool + IgnoreDestinationIPCIDRMatch bool +} + +func (c *InboundContext) ResetRuleCache() { + c.IPCIDRMatchSource = false + c.IPCIDRAcceptEmpty = false + c.ResetRuleMatchCache() +} + +func (c *InboundContext) ResetRuleMatchCache() { + c.SourceAddressMatch = false + c.SourcePortMatch = false + c.DestinationAddressMatch = false + c.DestinationPortMatch = false + c.DidMatch = false +} + +func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr { + return DNSResponseAddresses(c.DNSResponse) +} + +func DNSResponseAddresses(response *dns.Msg) []netip.Addr { + if response == nil || response.Rcode != dns.RcodeSuccess { + return nil + } + addresses := make([]netip.Addr, 0, len(response.Answer)) + for _, rawRecord := range response.Answer { + switch record := rawRecord.(type) { + case *dns.A: + addr := M.AddrFromIP(record.A) + if addr.IsValid() { + addresses = append(addresses, addr) + } + case *dns.AAAA: + addr := M.AddrFromIP(record.AAAA) + if addr.IsValid() { + addresses = append(addresses, addr) + } + case *dns.HTTPS: + for _, value := range record.SVCB.Value { + switch hint := value.(type) { + case *dns.SVCBIPv4Hint: + for _, ip := range hint.Hint { + addr := M.AddrFromIP(ip).Unmap() + if addr.IsValid() { + addresses = append(addresses, addr) + } + } + case *dns.SVCBIPv6Hint: + for _, ip := range hint.Hint { + addr := M.AddrFromIP(ip) + if addr.IsValid() { + addresses = append(addresses, addr) + } + } + } + } + } + } + return addresses +} + +type inboundContextKey struct{} + +func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context { + return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext) +} + +func ContextFrom(ctx context.Context) *InboundContext { + metadata := ctx.Value((*inboundContextKey)(nil)) + if metadata == nil { + return nil + } + return metadata.(*InboundContext) +} + +func ExtendContext(ctx context.Context) (context.Context, *InboundContext) { + var newMetadata InboundContext + if metadata := ContextFrom(ctx); metadata != nil { + newMetadata = *metadata + } + return WithContext(ctx, &newMetadata), &newMetadata +} + +func OverrideContext(ctx context.Context) context.Context { + if metadata := ContextFrom(ctx); metadata != nil { + newMetadata := *metadata + return WithContext(ctx, &newMetadata) + } + return ctx +} diff --git a/adapter/inbound/adapter.go b/adapter/inbound/adapter.go new file mode 100644 index 00000000..1426104a --- /dev/null +++ b/adapter/inbound/adapter.go @@ -0,0 +1,21 @@ +package inbound + +type Adapter struct { + inboundType string + inboundTag string +} + +func NewAdapter(inboundType string, inboundTag string) Adapter { + return Adapter{ + inboundType: inboundType, + inboundTag: inboundTag, + } +} + +func (a *Adapter) Type() string { + return a.inboundType +} + +func (a *Adapter) Tag() string { + return a.inboundTag +} diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go new file mode 100644 index 00000000..438c20f4 --- /dev/null +++ b/adapter/inbound/manager.go @@ -0,0 +1,163 @@ +package inbound + +import ( + "context" + "os" + "sync" + "time" + + "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" + F "github.com/sagernet/sing/common/format" +) + +var _ adapter.InboundManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.InboundRegistry + endpoint adapter.EndpointManager + access sync.Mutex + started bool + stage adapter.StartStage + inbounds []adapter.Inbound + inboundByTag map[string]adapter.Inbound +} + +func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager { + return &Manager{ + logger: logger, + registry: registry, + endpoint: endpoint, + inboundByTag: make(map[string]adapter.Inbound), + } +} + +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 + inbounds := m.inbounds + m.access.Unlock() + for _, inbound := range inbounds { + name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err := adapter.LegacyStart(inbound, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if !m.started { + return nil + } + m.started = false + inbounds := m.inbounds + m.inbounds = nil + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, inbound := range inbounds { + name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" + m.logger.Trace("close ", name) + startTime := time.Now() + monitor.Start("close ", name) + err = E.Append(err, inbound.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + monitor.Finish() + m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func (m *Manager) Inbounds() []adapter.Inbound { + m.access.Lock() + defer m.access.Unlock() + return m.inbounds +} + +func (m *Manager) Get(tag string) (adapter.Inbound, bool) { + m.access.Lock() + inbound, found := m.inboundByTag[tag] + m.access.Unlock() + if found { + return inbound, true + } + return m.endpoint.Get(tag) +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + inbound, found := m.inboundByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.inboundByTag, tag) + index := common.Index(m.inbounds, func(it adapter.Inbound) bool { + return it == inbound + }) + if index == -1 { + panic("invalid inbound index") + } + m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return inbound.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error { + inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" + for _, stage := range adapter.ListStartStages { + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err = adapter.LegacyStart(inbound, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + if existsInbound, loaded := m.inboundByTag[tag]; loaded { + if m.started { + err = existsInbound.Close() + if err != nil { + return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]") + } + } + existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool { + return it == existsInbound + }) + if existsIndex == -1 { + panic("invalid inbound index") + } + m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...) + } + m.inbounds = append(m.inbounds, inbound) + m.inboundByTag[tag] = inbound + return nil +} diff --git a/adapter/inbound/registry.go b/adapter/inbound/registry.go new file mode 100644 index 00000000..01e367d8 --- /dev/null +++ b/adapter/inbound/registry.go @@ -0,0 +1,72 @@ +package inbound + +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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.InboundRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, 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, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, 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, router, 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 +} diff --git a/adapter/inbound_test.go b/adapter/inbound_test.go new file mode 100644 index 00000000..ec8c3128 --- /dev/null +++ b/adapter/inbound_test.go @@ -0,0 +1,45 @@ +package adapter + +import ( + "net" + "net/netip" + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) { + t.Parallel() + + ipv4Hint := net.ParseIP("1.1.1.1") + require.NotNil(t, ipv4Hint) + + response := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Response: true, + Rcode: dns.RcodeSuccess, + }, + Answer: []dns.RR{ + &dns.HTTPS{ + SVCB: dns.SVCB{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("example.com"), + Rrtype: dns.TypeHTTPS, + Class: dns.ClassINET, + Ttl: 60, + }, + Priority: 1, + Target: ".", + Value: []dns.SVCBKeyValue{ + &dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}}, + }, + }, + }, + }, + } + + addresses := DNSResponseAddresses(response) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) + require.True(t, addresses[0].Is4()) +} diff --git a/adapter/lifecycle.go b/adapter/lifecycle.go new file mode 100644 index 00000000..b969c98a --- /dev/null +++ b/adapter/lifecycle.go @@ -0,0 +1,102 @@ +package adapter + +import ( + "reflect" + "strings" + "time" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +type SimpleLifecycle interface { + Start() error + Close() error +} + +type StartStage uint8 + +const ( + StartStateInitialize StartStage = iota + StartStateStart + StartStatePostStart + StartStateStarted +) + +var ListStartStages = []StartStage{ + StartStateInitialize, + StartStateStart, + StartStatePostStart, + StartStateStarted, +} + +func (s StartStage) String() string { + switch s { + case StartStateInitialize: + return "initialize" + case StartStateStart: + return "start" + case StartStatePostStart: + return "post-start" + case StartStateStarted: + return "finish-start" + default: + panic("unknown stage") + } +} + +type Lifecycle interface { + Start(stage StartStage) error + Close() error +} + +type LifecycleService interface { + Name() string + Lifecycle +} + +func getServiceName(service any) string { + if named, ok := service.(interface { + Type() string + Tag() string + }); ok { + tag := named.Tag() + if tag != "" { + return named.Type() + "[" + tag + "]" + } + return named.Type() + } + t := reflect.TypeOf(service) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + return strings.ToLower(t.Name()) +} + +func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error { + for _, service := range services { + name := getServiceName(service) + logger.Trace(stage, " ", name) + startTime := time.Now() + err := service.Start(stage) + if err != nil { + return err + } + logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error { + for _, service := range services { + logger.Trace(stage, " ", service.Name()) + startTime := time.Now() + err := service.Start(stage) + if err != nil { + return E.Cause(err, stage.String(), " ", service.Name()) + } + logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} diff --git a/adapter/lifecycle_legacy.go b/adapter/lifecycle_legacy.go new file mode 100644 index 00000000..f8b25db6 --- /dev/null +++ b/adapter/lifecycle_legacy.go @@ -0,0 +1,52 @@ +package adapter + +func LegacyStart(starter any, stage StartStage) error { + if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle { + return lifecycle.Start(stage) + } + switch stage { + case StartStateInitialize: + if preStarter, isPreStarter := starter.(interface { + PreStart() error + }); isPreStarter { + return preStarter.PreStart() + } + case StartStateStart: + if starter, isStarter := starter.(interface { + Start() error + }); isStarter { + return starter.Start() + } + case StartStateStarted: + if postStarter, isPostStarter := starter.(interface { + PostStart() error + }); isPostStarter { + return postStarter.PostStart() + } + } + return nil +} + +type lifecycleServiceWrapper struct { + SimpleLifecycle + name string +} + +func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService { + return &lifecycleServiceWrapper{ + SimpleLifecycle: service, + name: name, + } +} + +func (l *lifecycleServiceWrapper) Name() string { + return l.name +} + +func (l *lifecycleServiceWrapper) Start(stage StartStage) error { + return LegacyStart(l.SimpleLifecycle, stage) +} + +func (l *lifecycleServiceWrapper) Close() error { + return l.SimpleLifecycle.Close() +} diff --git a/adapter/neighbor.go b/adapter/neighbor.go new file mode 100644 index 00000000..d917db5b --- /dev/null +++ b/adapter/neighbor.go @@ -0,0 +1,23 @@ +package adapter + +import ( + "net" + "net/netip" +) + +type NeighborEntry struct { + Address netip.Addr + MACAddress net.HardwareAddr + Hostname string +} + +type NeighborResolver interface { + LookupMAC(address netip.Addr) (net.HardwareAddr, bool) + LookupHostname(address netip.Addr) (string, bool) + Start() error + Close() error +} + +type NeighborUpdateListener interface { + UpdateNeighborTable(entries []NeighborEntry) +} diff --git a/adapter/network.go b/adapter/network.go new file mode 100644 index 00000000..dd53b2b4 --- /dev/null +++ b/adapter/network.go @@ -0,0 +1,60 @@ +package adapter + +import ( + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" +) + +type NetworkManager interface { + Lifecycle + Initialize(ruleSets []RuleSet) + InterfaceFinder() control.InterfaceFinder + UpdateInterfaces() error + DefaultNetworkInterface() *NetworkInterface + NetworkInterfaces() []NetworkInterface + AutoDetectInterface() bool + AutoDetectInterfaceFunc() control.Func + ProtectFunc() control.Func + DefaultOptions() NetworkOptions + RegisterAutoRedirectOutputMark(mark uint32) error + AutoRedirectOutputMark() uint32 + AutoRedirectOutputMarkFunc() control.Func + NetworkMonitor() tun.NetworkUpdateMonitor + InterfaceMonitor() tun.DefaultInterfaceMonitor + PackageManager() tun.PackageManager + NeedWIFIState() bool + WIFIState() WIFIState + UpdateWIFIState() + ResetNetwork() +} + +type NetworkOptions struct { + BindInterface string + RoutingMark uint32 + DomainResolver string + DomainResolveOptions DNSQueryOptions + NetworkStrategy *C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType + FallbackDelay time.Duration +} + +type InterfaceUpdateListener interface { + InterfaceUpdated() +} + +type WIFIState struct { + SSID string + BSSID string +} + +type NetworkInterface struct { + control.Interface + Type C.InterfaceType + DNSServers []string + Expensive bool + Constrained bool +} diff --git a/adapter/outbound.go b/adapter/outbound.go new file mode 100644 index 00000000..91fb9c65 --- /dev/null +++ b/adapter/outbound.go @@ -0,0 +1,47 @@ +package adapter + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + N "github.com/sagernet/sing/common/network" +) + +// Note: for proxy protocols, outbound creates early connections by default. + +type Outbound interface { + Type() string + Tag() string + Network() []string + Dependencies() []string + N.Dialer +} + +type OutboundWithPreferredRoutes interface { + Outbound + PreferredDomain(domain string) bool + PreferredAddress(address netip.Addr) bool +} + +type DirectRouteOutbound interface { + Outbound + NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) +} + +type OutboundRegistry interface { + option.OutboundOptionsRegistry + CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) +} + +type OutboundManager interface { + Lifecycle + Outbounds() []Outbound + Outbound(tag string) (Outbound, bool) + Default() Outbound + Remove(tag string) error + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error +} diff --git a/adapter/outbound/adapter.go b/adapter/outbound/adapter.go new file mode 100644 index 00000000..cd71527a --- /dev/null +++ b/adapter/outbound/adapter.go @@ -0,0 +1,45 @@ +package outbound + +import ( + "github.com/sagernet/sing-box/option" +) + +type Adapter struct { + outboundType string + outboundTag string + network []string + dependencies []string +} + +func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter { + return Adapter{ + outboundType: outboundType, + outboundTag: outboundTag, + network: network, + dependencies: dependencies, + } +} + +func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter { + var dependencies []string + if dialOptions.Detour != "" { + dependencies = []string{dialOptions.Detour} + } + return NewAdapter(outboundType, outboundTag, network, dependencies) +} + +func (a *Adapter) Type() string { + return a.outboundType +} + +func (a *Adapter) Tag() string { + return a.outboundTag +} + +func (a *Adapter) Network() []string { + return a.network +} + +func (a *Adapter) Dependencies() []string { + return a.dependencies +} diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go new file mode 100644 index 00000000..5c1b5d99 --- /dev/null +++ b/adapter/outbound/manager.go @@ -0,0 +1,317 @@ +package outbound + +import ( + "context" + "io" + "os" + "strings" + "sync" + "time" + + "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" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" +) + +var _ adapter.OutboundManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.OutboundRegistry + endpoint adapter.EndpointManager + defaultTag string + access sync.RWMutex + started bool + stage adapter.StartStage + outbounds []adapter.Outbound + outboundByTag map[string]adapter.Outbound + dependByTag map[string][]string + defaultOutbound adapter.Outbound + defaultOutboundFallback func() (adapter.Outbound, error) +} + +func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { + return &Manager{ + logger: logger, + registry: registry, + endpoint: endpoint, + defaultTag: defaultTag, + outboundByTag: make(map[string]adapter.Outbound), + dependByTag: make(map[string][]string), + } +} + +func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) { + m.defaultOutboundFallback = defaultOutboundFallback +} + +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 + if stage == adapter.StartStateStart { + if m.defaultTag != "" && m.defaultOutbound == nil { + defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) + if !loaded { + m.access.Unlock() + return E.New("default outbound not found: ", m.defaultTag) + } + m.defaultOutbound = defaultEndpoint + } + if m.defaultOutbound == nil { + directOutbound, err := m.defaultOutboundFallback() + if err != nil { + m.access.Unlock() + return E.Cause(err, "create direct outbound for fallback") + } + m.outbounds = append(m.outbounds, directOutbound) + m.outboundByTag[directOutbound.Tag()] = directOutbound + m.defaultOutbound = directOutbound + } + outbounds := m.outbounds + m.access.Unlock() + return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) + } else { + outbounds := m.outbounds + m.access.Unlock() + for _, outbound := range outbounds { + name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err := adapter.LegacyStart(outbound, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + return nil +} + +func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { + monitor := taskmonitor.New(m.logger, C.StartTimeout) + started := make(map[string]bool) + for { + canContinue := false + startOne: + for _, outboundToStart := range outbounds { + outboundTag := outboundToStart.Tag() + if started[outboundTag] { + continue + } + dependencies := outboundToStart.Dependencies() + for _, dependency := range dependencies { + if !started[dependency] { + continue startOne + } + } + started[outboundTag] = true + canContinue = true + name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]" + if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter { + m.logger.Trace("start ", name) + startTime := time.Now() + monitor.Start("start ", name) + err := starter.Start(adapter.StartStateStart) + monitor.Finish() + if err != nil { + return E.Cause(err, "start ", name) + } + m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } else if starter, isStarter := outboundToStart.(interface { + Start() error + }); isStarter { + m.logger.Trace("start ", name) + startTime := time.Now() + monitor.Start("start ", name) + err := starter.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start ", name) + } + m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + if len(started) == len(outbounds) { + break + } + if canContinue { + continue + } + currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool { + return !started[it.Tag()] + }) + var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error + lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error { + problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool { + return !started[it] + }) + if common.Contains(oTree, problemOutboundTag) { + return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) + } + m.access.Lock() + problemOutbound := m.outboundByTag[problemOutboundTag] + m.access.Unlock() + if problemOutbound == nil { + return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]") + } + return lintOutbound(append(oTree, problemOutboundTag), problemOutbound) + } + return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound) + } + return nil +} + +func (m *Manager) Close() error { + monitor := taskmonitor.New(m.logger, C.StopTimeout) + m.access.Lock() + if !m.started { + m.access.Unlock() + return nil + } + m.started = false + outbounds := m.outbounds + m.outbounds = nil + m.access.Unlock() + var err error + for _, outbound := range outbounds { + if closer, isCloser := outbound.(io.Closer); isCloser { + name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" + m.logger.Trace("close ", name) + startTime := time.Now() + monitor.Start("close ", name) + err = E.Append(err, closer.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + monitor.Finish() + m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + return nil +} + +func (m *Manager) Outbounds() []adapter.Outbound { + m.access.RLock() + defer m.access.RUnlock() + return m.outbounds +} + +func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) { + m.access.RLock() + outbound, found := m.outboundByTag[tag] + m.access.RUnlock() + if found { + return outbound, true + } + return m.endpoint.Get(tag) +} + +func (m *Manager) Default() adapter.Outbound { + m.access.RLock() + defer m.access.RUnlock() + return m.defaultOutbound +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + defer m.access.Unlock() + outbound, found := m.outboundByTag[tag] + if !found { + return os.ErrInvalid + } + delete(m.outboundByTag, tag) + index := common.Index(m.outbounds, func(it adapter.Outbound) bool { + return it == outbound + }) + if index == -1 { + panic("invalid inbound index") + } + m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...) + started := m.started + if m.defaultOutbound == outbound { + if len(m.outbounds) > 0 { + m.defaultOutbound = m.outbounds[0] + m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag()) + } else { + m.defaultOutbound = nil + } + } + dependBy := m.dependByTag[tag] + if len(dependBy) > 0 { + return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", ")) + } + dependencies := outbound.Dependencies() + for _, dependency := range dependencies { + if len(m.dependByTag[dependency]) == 1 { + delete(m.dependByTag, dependency) + } else { + m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool { + return it != tag + }) + } + } + if started { + return common.Close(outbound) + } + return nil +} + +func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error { + if tag == "" { + return os.ErrInvalid + } + outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options) + if err != nil { + return err + } + if m.started { + name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" + for _, stage := range adapter.ListStartStages { + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err = adapter.LegacyStart(outbound, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + m.access.Lock() + defer m.access.Unlock() + if existsOutbound, loaded := m.outboundByTag[tag]; loaded { + if m.started { + err = common.Close(existsOutbound) + if err != nil { + return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]") + } + } + existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool { + return it == existsOutbound + }) + if existsIndex == -1 { + panic("invalid inbound index") + } + m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...) + } + m.outbounds = append(m.outbounds, outbound) + m.outboundByTag[tag] = outbound + dependencies := outbound.Dependencies() + for _, dependency := range dependencies { + m.dependByTag[dependency] = append(m.dependByTag[dependency], tag) + } + if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) { + m.defaultOutbound = outbound + if m.started { + m.logger.Info("updated default outbound to ", outbound.Tag()) + } + } + return nil +} diff --git a/adapter/outbound/registry.go b/adapter/outbound/registry.go new file mode 100644 index 00000000..8743ba10 --- /dev/null +++ b/adapter/outbound/registry.go @@ -0,0 +1,72 @@ +package outbound + +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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.OutboundRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) +) + +type Registry struct { + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructors map[string]constructorFunc +} + +func NewRegistry() *Registry { + return &Registry{ + optionsType: make(map[string]optionsConstructorFunc), + constructors: make(map[string]constructorFunc), + } +} + +func (r *Registry) CreateOptions(outboundType string) (any, bool) { + r.access.Lock() + defer r.access.Unlock() + optionsConstructor, loaded := r.optionsType[outboundType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) { + r.access.Lock() + defer r.access.Unlock() + constructor, loaded := r.constructors[outboundType] + if !loaded { + return nil, E.New("outbound type not found: " + outboundType) + } + return constructor(ctx, router, logger, tag, options) +} + +func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + r.access.Lock() + defer r.access.Unlock() + r.optionsType[outboundType] = optionsConstructor + r.constructors[outboundType] = constructor +} diff --git a/adapter/platform.go b/adapter/platform.go new file mode 100644 index 00000000..fd966548 --- /dev/null +++ b/adapter/platform.go @@ -0,0 +1,74 @@ +package adapter + +import ( + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/logger" +) + +type PlatformInterface interface { + Initialize(networkManager NetworkManager) error + + UsePlatformAutoDetectInterfaceControl() bool + AutoDetectInterfaceControl(fd int) error + + UsePlatformInterface() bool + OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) + + UsePlatformDefaultInterfaceMonitor() bool + CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor + + UsePlatformNetworkInterfaces() bool + NetworkInterfaces() ([]NetworkInterface, error) + + UnderNetworkExtension() bool + NetworkExtensionIncludeAllNetworks() bool + + ClearDNSCache() + RequestPermissionForWIFIState() error + ReadWIFIState() WIFIState + SystemCertificates() []string + + UsePlatformConnectionOwnerFinder() bool + FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error) + + UsePlatformWIFIMonitor() bool + + UsePlatformNotification() bool + SendNotification(notification *Notification) error + + UsePlatformNeighborResolver() bool + StartNeighborMonitor(listener NeighborUpdateListener) error + CloseNeighborMonitor(listener NeighborUpdateListener) error +} + +type FindConnectionOwnerRequest struct { + IpProtocol int32 + SourceAddress string + SourcePort int32 + DestinationAddress string + DestinationPort int32 +} + +type ConnectionOwner struct { + ProcessID uint32 + UserId int32 + UserName string + ProcessPath string + AndroidPackageNames []string +} + +type Notification struct { + Identifier string + TypeName string + TypeID int32 + Title string + Subtitle string + Body string + OpenURL string +} + +type SystemProxyStatus struct { + Available bool + Enabled bool +} diff --git a/adapter/prestart.go b/adapter/prestart.go new file mode 100644 index 00000000..b8e8da30 --- /dev/null +++ b/adapter/prestart.go @@ -0,0 +1 @@ +package adapter diff --git a/adapter/router.go b/adapter/router.go new file mode 100644 index 00000000..f1e3da9a --- /dev/null +++ b/adapter/router.go @@ -0,0 +1,122 @@ +package adapter + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "sync" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-tun" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" +) + +type Router interface { + Lifecycle + ConnectionRouter + PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) + ConnectionRouterEx + RuleSet(tag string) (RuleSet, bool) + Rules() []Rule + NeedFindProcess() bool + NeedFindNeighbor() bool + NeighborResolver() NeighborResolver + AppendTracker(tracker ConnectionTracker) + ResetNetwork() +} + +type ConnectionTracker interface { + RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn + RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn +} + +// Deprecated: Use ConnectionRouterEx instead. +type ConnectionRouter interface { + RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error + RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error +} + +type ConnectionRouterEx interface { + ConnectionRouter + RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) +} + +type RuleSet interface { + Name() string + StartContext(ctx context.Context, startContext *HTTPStartContext) error + PostStart() error + Metadata() RuleSetMetadata + ExtractIPSet() []*netipx.IPSet + IncRef() + DecRef() + Cleanup() + RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback] + UnregisterCallback(element *list.Element[RuleSetUpdateCallback]) + Close() error + HeadlessRule +} + +type RuleSetUpdateCallback func(it RuleSet) + +type DNSRuleSetUpdateValidator interface { + ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error +} + +// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent. +type RuleSetMetadata struct { + ContainsProcessRule bool + ContainsWIFIRule bool + ContainsIPCIDRRule bool + ContainsDNSQueryTypeRule bool +} +type HTTPStartContext struct { + ctx context.Context + access sync.Mutex + httpClientCache map[string]*http.Client +} + +func NewHTTPStartContext(ctx context.Context) *HTTPStartContext { + return &HTTPStartContext{ + ctx: ctx, + httpClientCache: make(map[string]*http.Client), + } +} + +func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client { + c.access.Lock() + defer c.access.Unlock() + if httpClient, loaded := c.httpClientCache[detour]; loaded { + return httpClient + } + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + Time: ntp.TimeFuncFromContext(c.ctx), + RootCAs: RootPoolFromContext(c.ctx), + }, + }, + } + c.httpClientCache[detour] = httpClient + return httpClient +} + +func (c *HTTPStartContext) Close() { + c.access.Lock() + defer c.access.Unlock() + for _, client := range c.httpClientCache { + client.CloseIdleConnections() + } +} diff --git a/adapter/rule.go b/adapter/rule.go new file mode 100644 index 00000000..2117ba45 --- /dev/null +++ b/adapter/rule.go @@ -0,0 +1,40 @@ +package adapter + +import ( + C "github.com/sagernet/sing-box/constant" + + "github.com/miekg/dns" +) + +type HeadlessRule interface { + Match(metadata *InboundContext) bool + String() string +} + +type Rule interface { + HeadlessRule + SimpleLifecycle + Type() string + Action() RuleAction +} + +type DNSRule interface { + Rule + LegacyPreMatch(metadata *InboundContext) bool + WithAddressLimit() bool + MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool +} + +type RuleAction interface { + Type() string + String() string +} + +func IsFinalAction(action RuleAction) bool { + switch action.Type() { + case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate: + return false + default: + return true + } +} diff --git a/adapter/service.go b/adapter/service.go new file mode 100644 index 00000000..534bd7eb --- /dev/null +++ b/adapter/service.go @@ -0,0 +1,27 @@ +package adapter + +import ( + "context" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type Service interface { + 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 +} diff --git a/adapter/service/adapter.go b/adapter/service/adapter.go new file mode 100644 index 00000000..6c6242ea --- /dev/null +++ b/adapter/service/adapter.go @@ -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 +} diff --git a/adapter/service/manager.go b/adapter/service/manager.go new file mode 100644 index 00000000..f17aa07e --- /dev/null +++ b/adapter/service/manager.go @@ -0,0 +1,158 @@ +package service + +import ( + "context" + "os" + "sync" + "time" + + "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" + F "github.com/sagernet/sing/common/format" +) + +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 { + name := "service/" + service.Type() + "[" + service.Tag() + "]" + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err := adapter.LegacyStart(service, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + 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 { + name := "service/" + service.Type() + "[" + service.Tag() + "]" + m.logger.Trace("close ", name) + startTime := time.Now() + monitor.Start("close ", name) + err = E.Append(err, service.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + monitor.Finish() + m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + 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 { + name := "service/" + service.Type() + "[" + service.Tag() + "]" + for _, stage := range adapter.ListStartStages { + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err = adapter.LegacyStart(service, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + 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 +} diff --git a/adapter/service/registry.go b/adapter/service/registry.go new file mode 100644 index 00000000..42fec82f --- /dev/null +++ b/adapter/service/registry.go @@ -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 +} diff --git a/adapter/ssm.go b/adapter/ssm.go new file mode 100644 index 00000000..caab9221 --- /dev/null +++ b/adapter/ssm.go @@ -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 +} diff --git a/adapter/tailscale.go b/adapter/tailscale.go new file mode 100644 index 00000000..22f48e62 --- /dev/null +++ b/adapter/tailscale.go @@ -0,0 +1,49 @@ +package adapter + +import "context" + +type TailscaleEndpoint interface { + SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error + StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error +} + +type TailscalePingResult struct { + LatencyMs float64 + IsDirect bool + Endpoint string + DERPRegionID int32 + DERPRegionCode string + Error string +} + +type TailscaleEndpointStatus struct { + BackendState string + AuthURL string + NetworkName string + MagicDNSSuffix string + Self *TailscalePeer + UserGroups []*TailscaleUserGroup +} + +type TailscaleUserGroup struct { + UserID int64 + LoginName string + DisplayName string + ProfilePicURL string + Peers []*TailscalePeer +} + +type TailscalePeer struct { + HostName string + DNSName string + OS string + TailscaleIPs []string + Online bool + ExitNode bool + ExitNodeOption bool + Active bool + RxBytes int64 + TxBytes int64 + UserID int64 + KeyExpiry int64 +} diff --git a/adapter/time.go b/adapter/time.go new file mode 100644 index 00000000..be2631d8 --- /dev/null +++ b/adapter/time.go @@ -0,0 +1,8 @@ +package adapter + +import "time" + +type TimeService interface { + SimpleLifecycle + TimeFunc() func() time.Time +} diff --git a/adapter/upstream.go b/adapter/upstream.go new file mode 100644 index 00000000..59c8f75f --- /dev/null +++ b/adapter/upstream.go @@ -0,0 +1,168 @@ +package adapter + +import ( + "context" + "net" + + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type ( + ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) +) + +func NewUpstreamHandlerEx( + metadata InboundContext, + connectionHandler ConnectionHandlerFuncEx, + packetHandler PacketConnectionHandlerFuncEx, +) UpstreamHandlerAdapterEx { + return &myUpstreamHandlerWrapperEx{ + metadata: metadata, + connectionHandler: connectionHandler, + packetHandler: packetHandler, + } +} + +var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil) + +type myUpstreamHandlerWrapperEx struct { + metadata InboundContext + connectionHandler ConnectionHandlerFuncEx + packetHandler PacketConnectionHandlerFuncEx +} + +func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + myMetadata := w.metadata + if source.IsValid() { + myMetadata.Source = source + } + if destination.IsValid() { + myMetadata.Destination = destination + } + w.connectionHandler(ctx, conn, myMetadata, onClose) +} + +func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + myMetadata := w.metadata + if source.IsValid() { + myMetadata.Source = source + } + if destination.IsValid() { + myMetadata.Destination = destination + } + w.packetHandler(ctx, conn, myMetadata, onClose) +} + +var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil) + +type myUpstreamContextHandlerWrapperEx struct { + connectionHandler ConnectionHandlerFuncEx + packetHandler PacketConnectionHandlerFuncEx +} + +func NewUpstreamContextHandlerEx( + connectionHandler ConnectionHandlerFuncEx, + packetHandler PacketConnectionHandlerFuncEx, +) UpstreamHandlerAdapterEx { + return &myUpstreamContextHandlerWrapperEx{ + connectionHandler: connectionHandler, + packetHandler: packetHandler, + } +} + +func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + _, myMetadata := ExtendContext(ctx) + if source.IsValid() { + myMetadata.Source = source + } + if destination.IsValid() { + myMetadata.Destination = destination + } + w.connectionHandler(ctx, conn, *myMetadata, onClose) +} + +func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + _, myMetadata := ExtendContext(ctx) + if source.IsValid() { + myMetadata.Source = source + } + if destination.IsValid() { + myMetadata.Destination = destination + } + w.packetHandler(ctx, conn, *myMetadata, onClose) +} + +func NewRouteHandlerEx( + metadata InboundContext, + router ConnectionRouterEx, +) UpstreamHandlerAdapterEx { + return &routeHandlerWrapperEx{ + metadata: metadata, + router: router, + } +} + +var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil) + +type routeHandlerWrapperEx struct { + metadata InboundContext + router ConnectionRouterEx +} + +func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + if source.IsValid() { + r.metadata.Source = source + } + if destination.IsValid() { + r.metadata.Destination = destination + } + r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose) +} + +func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + if source.IsValid() { + r.metadata.Source = source + } + if destination.IsValid() { + r.metadata.Destination = destination + } + r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose) +} + +func NewRouteContextHandlerEx( + router ConnectionRouterEx, +) UpstreamHandlerAdapterEx { + return &routeContextHandlerWrapperEx{ + router: router, + } +} + +var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil) + +type routeContextHandlerWrapperEx struct { + router ConnectionRouterEx +} + +func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + _, metadata := ExtendContext(ctx) + if source.IsValid() { + metadata.Source = source + } + if destination.IsValid() { + metadata.Destination = destination + } + r.router.RouteConnectionEx(ctx, conn, *metadata, onClose) +} + +func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + _, metadata := ExtendContext(ctx) + if source.IsValid() { + metadata.Source = source + } + if destination.IsValid() { + metadata.Destination = destination + } + r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose) +} diff --git a/adapter/upstream_legacy.go b/adapter/upstream_legacy.go new file mode 100644 index 00000000..65402563 --- /dev/null +++ b/adapter/upstream_legacy.go @@ -0,0 +1,234 @@ +package adapter + +import ( + "context" + "net" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type ( + // Deprecated + ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error + // Deprecated + PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error +) + +// Deprecated +// +//nolint:staticcheck +func NewUpstreamHandler( + metadata InboundContext, + connectionHandler ConnectionHandlerFunc, + packetHandler PacketConnectionHandlerFunc, + errorHandler E.Handler, +) UpstreamHandlerAdapter { + return &myUpstreamHandlerWrapper{ + metadata: metadata, + connectionHandler: connectionHandler, + packetHandler: packetHandler, + errorHandler: errorHandler, + } +} + +var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) + +// Deprecated: use myUpstreamHandlerWrapperEx instead. +// +//nolint:staticcheck +type myUpstreamHandlerWrapper struct { + metadata InboundContext + connectionHandler ConnectionHandlerFunc + packetHandler PacketConnectionHandlerFunc + errorHandler E.Handler +} + +// Deprecated: use myUpstreamHandlerWrapperEx instead. +func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.connectionHandler(ctx, conn, myMetadata) +} + +// Deprecated: use myUpstreamHandlerWrapperEx instead. +func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.packetHandler(ctx, conn, myMetadata) +} + +// Deprecated: use myUpstreamHandlerWrapperEx instead. +func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { + w.errorHandler.NewError(ctx, err) +} + +// Deprecated: removed +func UpstreamMetadata(metadata InboundContext) M.Metadata { + return M.Metadata{ + Source: metadata.Source.Unwrap(), + Destination: metadata.Destination.Unwrap(), + } +} + +// Deprecated: Use NewUpstreamContextHandlerEx instead. +type myUpstreamContextHandlerWrapper struct { + connectionHandler ConnectionHandlerFunc + packetHandler PacketConnectionHandlerFunc + errorHandler E.Handler +} + +// Deprecated: Use NewUpstreamContextHandlerEx instead. +func NewUpstreamContextHandler( + connectionHandler ConnectionHandlerFunc, + packetHandler PacketConnectionHandlerFunc, + errorHandler E.Handler, +) UpstreamHandlerAdapter { + return &myUpstreamContextHandlerWrapper{ + connectionHandler: connectionHandler, + packetHandler: packetHandler, + errorHandler: errorHandler, + } +} + +// Deprecated: Use NewUpstreamContextHandlerEx instead. +func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.connectionHandler(ctx, conn, *myMetadata) +} + +// Deprecated: Use NewUpstreamContextHandlerEx instead. +func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.packetHandler(ctx, conn, *myMetadata) +} + +// Deprecated: Use NewUpstreamContextHandlerEx instead. +func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { + w.errorHandler.NewError(ctx, err) +} + +// Deprecated: Use ConnectionRouterEx instead. +func NewRouteHandler( + metadata InboundContext, + router ConnectionRouter, + logger logger.ContextLogger, +) UpstreamHandlerAdapter { + return &routeHandlerWrapper{ + metadata: metadata, + router: router, + logger: logger, + } +} + +// Deprecated: Use ConnectionRouterEx instead. +func NewRouteContextHandler( + router ConnectionRouter, + logger logger.ContextLogger, +) UpstreamHandlerAdapter { + return &routeContextHandlerWrapper{ + router: router, + logger: logger, + } +} + +var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) + +// Deprecated: Use ConnectionRouterEx instead. +// +//nolint:staticcheck +type routeHandlerWrapper struct { + metadata InboundContext + router ConnectionRouter + logger logger.ContextLogger +} + +// Deprecated: Use ConnectionRouterEx instead. +func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RouteConnection(ctx, conn, myMetadata) +} + +// Deprecated: Use ConnectionRouterEx instead. +func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RoutePacketConnection(ctx, conn, myMetadata) +} + +// Deprecated: Use ConnectionRouterEx instead. +func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { + w.logger.ErrorContext(ctx, err) +} + +var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) + +// Deprecated: Use ConnectionRouterEx instead. +type routeContextHandlerWrapper struct { + router ConnectionRouter + logger logger.ContextLogger +} + +// Deprecated: Use ConnectionRouterEx instead. +func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RouteConnection(ctx, conn, *myMetadata) +} + +// Deprecated: Use ConnectionRouterEx instead. +func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RoutePacketConnection(ctx, conn, *myMetadata) +} + +// Deprecated: Use ConnectionRouterEx instead. +func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { + w.logger.ErrorContext(ctx, err) +} diff --git a/adapter/v2ray.go b/adapter/v2ray.go new file mode 100644 index 00000000..d9370807 --- /dev/null +++ b/adapter/v2ray.go @@ -0,0 +1,24 @@ +package adapter + +import ( + "context" + "net" + + N "github.com/sagernet/sing/common/network" +) + +type V2RayServerTransport interface { + Network() []string + Serve(listener net.Listener) error + ServePacket(listener net.PacketConn) error + Close() error +} + +type V2RayServerTransportHandler interface { + N.TCPConnectionHandlerEx +} + +type V2RayClientTransport interface { + DialContext(ctx context.Context) (net.Conn, error) + Close() error +} diff --git a/box.go b/box.go new file mode 100644 index 00000000..b4844f9e --- /dev/null +++ b/box.go @@ -0,0 +1,609 @@ +package box + +import ( + "context" + "fmt" + "io" + "os" + "runtime/debug" + "time" + + "github.com/sagernet/sing-box/adapter" + boxCertificate "github.com/sagernet/sing-box/adapter/certificate" + "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" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/direct" + "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +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 + service *boxService.Manager + certificateProvider *boxCertificate.Manager + dnsTransport *dns.TransportManager + dnsRouter *dns.Router + connection *route.ConnectionManager + router *route.Router + internalService []adapter.LifecycleService + done chan struct{} +} + +type Options struct { + option.Options + Context context.Context + PlatformLogWriter log.PlatformWriter +} + +func Context( + ctx context.Context, + inboundRegistry adapter.InboundRegistry, + outboundRegistry adapter.OutboundRegistry, + endpointRegistry adapter.EndpointRegistry, + dnsTransportRegistry adapter.DNSTransportRegistry, + serviceRegistry adapter.ServiceRegistry, + certificateProviderRegistry adapter.CertificateProviderRegistry, +) context.Context { + if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || + service.FromContext[adapter.InboundRegistry](ctx) == nil { + ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry) + ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry) + } + if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil || + service.FromContext[adapter.OutboundRegistry](ctx) == nil { + ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry) + ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry) + } + if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil || + service.FromContext[adapter.EndpointRegistry](ctx) == nil { + ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry) + ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry) + } + if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil { + 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) + } + if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil { + ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry) + ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry) + } + return ctx +} + +func New(options Options) (*Box, error) { + createdAt := time.Now() + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + ctx = service.ContextWithDefaultRegistry(ctx) + + endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx) + inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) + outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) + dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) + serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx) + certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx) + + if endpointRegistry == nil { + return nil, E.New("missing endpoint registry in context") + } + if inboundRegistry == nil { + return nil, E.New("missing inbound registry in context") + } + 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") + } + if certificateProviderRegistry == nil { + return nil, E.New("missing certificate provider registry in context") + } + + ctx = pause.WithDefaultManager(ctx) + experimentalOptions := common.PtrValueOrDefault(options.Experimental) + err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + if err != nil { + return nil, err + } + var needCacheFile bool + var needClashAPI bool + var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } + if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { + needClashAPI = true + } + if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { + needV2RayAPI = true + } + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + var defaultLogWriter io.Writer + if platformInterface != nil { + defaultLogWriter = io.Discard + } + logFactory, err := log.New(log.Options{ + Context: ctx, + Options: common.PtrValueOrDefault(options.Log), + Observable: needClashAPI, + DefaultWriter: defaultLogWriter, + BaseTime: createdAt, + PlatformWriter: options.PlatformLogWriter, + }) + if err != nil { + return nil, E.Cause(err, "create log factory") + } + + 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) + } + + routeOptions := common.PtrValueOrDefault(options.Route) + dnsOptions := common.PtrValueOrDefault(options.DNS) + endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) + 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) + certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry) + 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) + service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager) + dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions) + if err != nil { + return nil, E.Cause(err, "initialize DNS router") + } + service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) + service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter) + networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) + if err != nil { + return nil, E.Cause(err, "initialize network manager") + } + service.MustRegister[adapter.NetworkManager](ctx, networkManager) + 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) + err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet) + if err != nil { + return nil, E.Cause(err, "initialize router") + } + ntpOptions := common.PtrValueOrDefault(options.NTP) + var timeService *tls.TimeServiceWrapper + if ntpOptions.Enabled { + timeService = new(tls.TimeServiceWrapper) + service.MustRegister[ntp.TimeService](ctx, timeService) + } + for i, transportOptions := range dnsOptions.Servers { + var tag string + if transportOptions.Tag != "" { + tag = transportOptions.Tag + } else { + tag = F.ToString(i) + } + err = dnsTransportManager.Create( + ctx, + logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")), + tag, + transportOptions.Type, + transportOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize DNS server[", i, "]") + } + } + err = dnsRouter.Initialize(dnsOptions.Rules) + if err != nil { + return nil, E.Cause(err, "initialize dns router") + } + for i, endpointOptions := range options.Endpoints { + var tag string + if endpointOptions.Tag != "" { + tag = endpointOptions.Tag + } else { + tag = F.ToString(i) + } + endpointCtx := ctx + if tag != "" { + // TODO: remove this + endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{ + Outbound: tag, + }) + } + err = endpointManager.Create( + endpointCtx, + router, + logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")), + tag, + endpointOptions.Type, + endpointOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize endpoint[", i, "]") + } + } + for i, inboundOptions := range options.Inbounds { + var tag string + if inboundOptions.Tag != "" { + tag = inboundOptions.Tag + } else { + tag = F.ToString(i) + } + err = inboundManager.Create( + ctx, + router, + logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), + tag, + inboundOptions.Type, + inboundOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize inbound[", 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, "]") + } + } + for i, outboundOptions := range options.Outbounds { + var tag string + if outboundOptions.Tag != "" { + tag = outboundOptions.Tag + } else { + tag = F.ToString(i) + } + outboundCtx := ctx + if tag != "" { + // TODO: remove this + outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{ + Outbound: tag, + }) + } + err = outboundManager.Create( + outboundCtx, + router, + logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), + tag, + outboundOptions.Type, + outboundOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize outbound[", i, "]") + } + } + for i, certificateProviderOptions := range options.CertificateProviders { + var tag string + if certificateProviderOptions.Tag != "" { + tag = certificateProviderOptions.Tag + } else { + tag = F.ToString(i) + } + err = certificateProviderManager.Create( + ctx, + logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")), + tag, + certificateProviderOptions.Type, + certificateProviderOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize certificate provider[", i, "]") + } + } + outboundManager.Initialize(func() (adapter.Outbound, error) { + return direct.NewOutbound( + ctx, + router, + logFactory.NewLogger("outbound/direct"), + "direct", + option.DirectOutboundOptions{}, + ) + }) + dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { + return dnsTransportRegistry.CreateDNSTransport( + ctx, + logFactory.NewLogger("dns/local"), + "local", + C.DNSTypeLocal, + &option.LocalDNSServerOptions{}, + ) + }) + if platformInterface != nil { + err = platformInterface.Initialize(networkManager) + if err != nil { + return nil, E.Cause(err, "initialize platform interface") + } + } + if needCacheFile { + cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile)) + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + internalServices = append(internalServices, cacheFile) + } + if needClashAPI { + clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) + clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) + clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions) + if err != nil { + return nil, E.Cause(err, "create clash-server") + } + router.AppendTracker(clashServer) + service.MustRegister[adapter.ClashServer](ctx, clashServer) + internalServices = append(internalServices, clashServer) + } + if needV2RayAPI { + v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) + if err != nil { + return nil, E.Cause(err, "create v2ray-server") + } + if v2rayServer.StatsService() != nil { + router.AppendTracker(v2rayServer.StatsService()) + internalServices = append(internalServices, v2rayServer) + service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) + } + } + if ntpOptions.Enabled { + ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain()) + if err != nil { + return nil, E.Cause(err, "create NTP service") + } + ntpService := ntp.NewService(ntp.Options{ + Context: ctx, + Dialer: ntpDialer, + Logger: logFactory.NewLogger("ntp"), + Server: ntpOptions.ServerOptions.Build(), + Interval: time.Duration(ntpOptions.Interval), + WriteToSystem: ntpOptions.WriteToSystem, + }) + timeService.TimeService = ntpService + internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service")) + } + return &Box{ + network: networkManager, + endpoint: endpointManager, + inbound: inboundManager, + outbound: outboundManager, + dnsTransport: dnsTransportManager, + service: serviceManager, + certificateProvider: certificateProviderManager, + dnsRouter: dnsRouter, + connection: connectionManager, + router: router, + createdAt: createdAt, + logFactory: logFactory, + logger: logFactory.Logger(), + internalService: internalServices, + done: make(chan struct{}), + }, nil +} + +func (s *Box) PreStart() error { + err := s.preStart() + if err != nil { + // TODO: remove catch error + defer func() { + v := recover() + if v != nil { + println(err.Error()) + debug.PrintStack() + panic("panic on early close: " + fmt.Sprint(v)) + } + }() + s.Close() + return err + } + s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") + return nil +} + +func (s *Box) Start() error { + err := s.start() + if err != nil { + // TODO: remove catch error + defer func() { + v := recover() + if v != nil { + println(err.Error()) + debug.PrintStack() + println("panic on early start: " + fmt.Sprint(v)) + } + }() + s.Close() + return err + } + s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") + return nil +} + +func (s *Box) preStart() error { + monitor := taskmonitor.New(s.logger, C.StartTimeout) + monitor.Start("start logger") + err := s.logFactory.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start logger") + } + err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter) + if err != nil { + return err + } + return nil +} + +func (s *Box) start() error { + err := s.preStart() + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service) + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service) + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService) + if err != nil { + return err + } + return nil +} + +func (s *Box) Close() error { + select { + case <-s.done: + return os.ErrClosed + default: + close(s.done) + } + var err error + for _, closeItem := range []struct { + name string + service adapter.Lifecycle + }{ + {"service", s.service}, + {"inbound", s.inbound}, + {"certificate-provider", s.certificateProvider}, + {"endpoint", s.endpoint}, + {"outbound", s.outbound}, + {"router", s.router}, + {"connection", s.connection}, + {"dns-router", s.dnsRouter}, + {"dns-transport", s.dnsTransport}, + {"network", s.network}, + } { + s.logger.Trace("close ", closeItem.name) + startTime := time.Now() + err = E.Append(err, closeItem.service.Close(), func(err error) error { + return E.Cause(err, "close ", closeItem.name) + }) + s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + for _, lifecycleService := range s.internalService { + s.logger.Trace("close ", lifecycleService.Name()) + startTime := time.Now() + err = E.Append(err, lifecycleService.Close(), func(err error) error { + return E.Cause(err, "close ", lifecycleService.Name()) + }) + s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + s.logger.Trace("close logger") + startTime := time.Now() + err = E.Append(err, s.logFactory.Close(), func(err error) error { + return E.Cause(err, "close logger") + }) + s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + return err +} + +func (s *Box) Network() adapter.NetworkManager { + return s.network +} + +func (s *Box) Router() adapter.Router { + return s.router +} + +func (s *Box) Inbound() adapter.InboundManager { + return s.inbound +} + +func (s *Box) Outbound() adapter.OutboundManager { + return s.outbound +} + +func (s *Box) Endpoint() adapter.EndpointManager { + return s.endpoint +} + +func (s *Box) LogFactory() log.Factory { + return s.logFactory +} diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go new file mode 100644 index 00000000..d415abd6 --- /dev/null +++ b/cmd/internal/app_store_connect/main.go @@ -0,0 +1,452 @@ +package main + +import ( + "context" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/sagernet/asc-go/asc" + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +func main() { + ctx := context.Background() + switch os.Args[1] { + case "next_macos_project_version": + err := fetchMacOSVersion(ctx) + if err != nil { + log.Fatal(err) + } + case "publish_testflight": + err := publishTestflight(ctx) + if err != nil { + log.Fatal(err) + } + case "cancel_app_store": + err := cancelAppStore(ctx, os.Args[2]) + if err != nil { + log.Fatal(err) + } + case "prepare_app_store": + err := prepareAppStore(ctx) + if err != nil { + log.Fatal(err) + } + case "publish_app_store": + err := publishAppStore(ctx) + if err != nil { + log.Fatal(err) + } + default: + log.Fatal("unknown action: ", os.Args[1]) + } +} + +const ( + appID = "6673731168" + groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda" +) + +func createClient(expireDuration time.Duration) *asc.Client { + privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH")) + if err != nil { + log.Fatal(err) + } + tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey) + if err != nil { + log.Fatal(err) + } + return asc.NewClient(tokenConfig.Client()) +} + +func fetchMacOSVersion(ctx context.Context) error { + client := createClient(time.Minute) + versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ + FilterPlatform: []string{"MAC_OS"}, + }) + if err != nil { + return err + } + var versionID string +findVersion: + for _, version := range versions.Data { + switch *version.Attributes.AppStoreState { + case asc.AppStoreVersionStateReadyForSale, + asc.AppStoreVersionStatePendingDeveloperRelease: + versionID = version.ID + break findVersion + } + } + if versionID == "" { + return E.New("no version found") + } + latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{}) + if err != nil { + return err + } + versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version) + if err != nil { + return E.Cause(err, "parse version code") + } + os.Stdout.WriteString(F.ToString(versionInt+1, "\n")) + return nil +} + +func publishTestflight(ctx context.Context) error { + if len(os.Args) < 3 { + return E.New("platform required: ios, macos, or tvos") + } + var platform asc.Platform + switch os.Args[2] { + case "ios": + platform = asc.PlatformIOS + case "macos": + platform = asc.PlatformMACOS + case "tvos": + platform = asc.PlatformTVOS + default: + return E.New("unknown platform: ", os.Args[2]) + } + + tagVersion, err := build_shared.ReadTagVersion() + if err != nil { + return err + } + tag := tagVersion.VersionString() + + releaseNotes := F.ToString("sing-box ", tagVersion.String()) + if len(os.Args) >= 4 { + releaseNotes = strings.Join(os.Args[3:], " ") + } + + client := createClient(20 * time.Minute) + + log.Info(tag, " list build IDs") + buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil) + if err != nil { + return err + } + buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string { + return it.ID + }) + + waitingForProcess := false + log.Info(string(platform), " list builds") + for { + builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ + FilterApp: []string{appID}, + FilterPreReleaseVersionPlatform: []string{string(platform)}, + }) + if err != nil { + return err + } + build := builds.Data[0] + log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")") + if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) { + log.Info(string(platform), " ", tag, " waiting for process") + time.Sleep(15 * time.Second) + continue + } + if *build.Attributes.ProcessingState != "VALID" { + waitingForProcess = true + log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState) + time.Sleep(15 * time.Second) + continue + } + log.Info(string(platform), " ", tag, " list localizations") + localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil) + if err != nil { + return err + } + localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool { + return *it.Attributes.Locale == "en-US" + }) + if localization.ID == "" { + log.Fatal(string(platform), " ", tag, " no en-US localization found") + } + if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" { + log.Info(string(platform), " ", tag, " update localization") + _, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(releaseNotes)) + if err != nil { + return err + } + } + log.Info(string(platform), " ", tag, " publish") + response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID}) + if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) { + log.Info("waiting for process") + time.Sleep(15 * time.Second) + continue + } else if err != nil { + return err + } + log.Info(string(platform), " ", tag, " list submissions") + betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{ + FilterBuild: []string{build.ID}, + }) + if err != nil { + return err + } + if len(betaSubmissions.Data) == 0 { + log.Info(string(platform), " ", tag, " create submission") + _, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID) + if err != nil { + if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") { + log.Error(err) + break + } + return err + } + } + break + } + return nil +} + +func cancelAppStore(ctx context.Context, platform string) error { + switch platform { + case "ios": + platform = string(asc.PlatformIOS) + case "macos": + platform = string(asc.PlatformMACOS) + case "tvos": + platform = string(asc.PlatformTVOS) + } + tag, err := build_shared.ReadTag() + if err != nil { + return err + } + client := createClient(time.Minute) + for { + log.Info(platform, " list versions") + versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ + FilterPlatform: []string{string(platform)}, + }) + if isRetryable(response) { + continue + } else if err != nil { + return err + } + version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { + return *it.Attributes.VersionString == tag + }) + if version.ID == "" { + return nil + } + log.Info(platform, " ", tag, " get submission") + submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil) + if response != nil && response.StatusCode == http.StatusNotFound { + return nil + } + if isRetryable(response) { + continue + } else if err != nil { + return err + } + log.Info(platform, " ", tag, " delete submission") + _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID) + if err != nil { + return err + } + return nil + } +} + +func prepareAppStore(ctx context.Context) error { + tag, err := build_shared.ReadTag() + if err != nil { + return err + } + client := createClient(time.Minute) + for _, platform := range []asc.Platform{ + asc.PlatformIOS, + asc.PlatformMACOS, + asc.PlatformTVOS, + } { + log.Info(string(platform), " list versions") + versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ + FilterPlatform: []string{string(platform)}, + }) + if err != nil { + return err + } + version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { + return *it.Attributes.VersionString == tag + }) + log.Info(string(platform), " ", tag, " list builds") + builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ + FilterApp: []string{appID}, + FilterPreReleaseVersionPlatform: []string{string(platform)}, + }) + if err != nil { + return err + } + if len(builds.Data) == 0 { + log.Fatal(platform, " ", tag, " no build found") + } + buildID := common.Ptr(builds.Data[0].ID) + if version.ID == "" { + log.Info(string(platform), " ", tag, " create version") + newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{ + Platform: platform, + VersionString: tag, + }, appID, buildID) + if err != nil { + return err + } + version = newVersion.Data + + } else { + log.Info(string(platform), " ", tag, " check build") + currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID) + if err != nil { + return err + } + if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID { + switch *version.Attributes.AppStoreState { + case asc.AppStoreVersionStatePrepareForSubmission, + asc.AppStoreVersionStateRejected, + asc.AppStoreVersionStateDeveloperRejected: + case asc.AppStoreVersionStateWaitingForReview, + asc.AppStoreVersionStateInReview, + asc.AppStoreVersionStatePendingDeveloperRelease: + submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil) + if err != nil { + return err + } + if submission != nil { + log.Info(string(platform), " ", tag, " delete submission") + _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID) + if err != nil { + return err + } + time.Sleep(5 * time.Second) + } + default: + log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) + } + log.Info(string(platform), " ", tag, " update build") + response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID) + if err != nil { + return err + } + if response.StatusCode != http.StatusNoContent { + response.Write(os.Stderr) + log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status) + } + } else { + switch *version.Attributes.AppStoreState { + case asc.AppStoreVersionStatePrepareForSubmission, + asc.AppStoreVersionStateRejected, + asc.AppStoreVersionStateDeveloperRejected: + case asc.AppStoreVersionStateWaitingForReview, + asc.AppStoreVersionStateInReview, + asc.AppStoreVersionStatePendingDeveloperRelease: + continue + default: + log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) + } + } + } + log.Info(string(platform), " ", tag, " list localization") + localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil) + if err != nil { + return err + } + localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool { + return *it.Attributes.Locale == "en-US" + }) + if localization.ID == "" { + log.Info(string(platform), " ", tag, " no en-US localization found") + } + if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" { + log.Info(string(platform), " ", tag, " update localization") + _, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{ + PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."), + WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")), + }) + if err != nil { + return err + } + } + log.Info(string(platform), " ", tag, " create submission") + fixSubmit: + for { + _, response, err := client.Submission.CreateSubmission(ctx, version.ID) + if err != nil { + switch response.StatusCode { + case http.StatusInternalServerError: + continue + default: + return err + } + } + switch response.StatusCode { + case http.StatusCreated: + break fixSubmit + default: + return err + } + } + } + return nil +} + +func publishAppStore(ctx context.Context) error { + tag, err := build_shared.ReadTag() + if err != nil { + return err + } + client := createClient(time.Minute) + for _, platform := range []asc.Platform{ + asc.PlatformIOS, + asc.PlatformMACOS, + asc.PlatformTVOS, + } { + log.Info(string(platform), " list versions") + versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ + FilterPlatform: []string{string(platform)}, + }) + if err != nil { + return err + } + version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { + return *it.Attributes.VersionString == tag + }) + switch *version.Attributes.AppStoreState { + case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected: + log.Fatal(string(platform), " ", tag, " not submitted") + case asc.AppStoreVersionStateWaitingForReview, + asc.AppStoreVersionStateInReview: + log.Warn(string(platform), " ", tag, " waiting for review") + continue + case asc.AppStoreVersionStatePendingDeveloperRelease: + default: + log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) + } + _, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID) + if err != nil { + return err + } + } + return nil +} + +func isRetryable(response *asc.Response) bool { + if response == nil { + return false + } + switch response.StatusCode { + case http.StatusInternalServerError, http.StatusUnprocessableEntity: + return true + default: + return false + } +} diff --git a/cmd/internal/build/main.go b/cmd/internal/build/main.go new file mode 100644 index 00000000..cae67ba4 --- /dev/null +++ b/cmd/internal/build/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "go/build" + "os" + "os/exec" + + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" +) + +func main() { + build_shared.FindSDK() + + if os.Getenv("GOPATH") == "" { + os.Setenv("GOPATH", build.Default.GOPATH) + } + + command := exec.Command(os.Args[1], os.Args[2:]...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Run() + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go new file mode 100644 index 00000000..c1282169 --- /dev/null +++ b/cmd/internal/build_libbox/main.go @@ -0,0 +1,245 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + _ "github.com/sagernet/gomobile" + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/shell" +) + +var ( + debugEnabled bool + target string + platform string + // withTailscale bool +) + +func init() { + flag.BoolVar(&debugEnabled, "debug", false, "enable debug") + flag.StringVar(&target, "target", "android", "target platform") + flag.StringVar(&platform, "platform", "", "specify platform") + // flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS") +} + +func main() { + flag.Parse() + + build_shared.FindMobile() + + switch target { + case "android": + buildAndroid() + case "apple": + buildApple() + } +} + +var ( + sharedFlags []string + debugFlags []string + sharedTags []string + darwinTags []string + // memcTags []string + notMemcTags []string + debugTags []string +) + +func init() { + sharedFlags = append(sharedFlags, "-trimpath") + sharedFlags = append(sharedFlags, "-buildvcs=false") + currentTag, err := build_shared.ReadTag() + if err != nil { + currentTag = "unknown" + } + sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0") + debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0") + + sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0") + darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace") + // memcTags = append(memcTags, "with_tailscale") + sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird") + notMemcTags = append(notMemcTags, "with_low_memory") + debugTags = append(debugTags, "debug") +} + +type AndroidBuildConfig struct { + AndroidAPI int + OutputName string + Tags []string +} + +func filterTags(tags []string, exclude ...string) []string { + excludeMap := make(map[string]bool) + for _, tag := range exclude { + excludeMap[tag] = true + } + var result []string + for _, tag := range tags { + if !excludeMap[tag] { + result = append(result, tag) + } + } + return result +} + +func checkJavaVersion() { + var javaPath string + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + javaPath = "java" + } else { + javaPath = filepath.Join(javaHome, "bin", "java") + } + + javaVersion, err := shell.Exec(javaPath, "--version").ReadOutput() + if err != nil { + log.Fatal(E.Cause(err, "check java version")) + } + if !strings.Contains(javaVersion, "openjdk 17") { + log.Fatal("java version should be openjdk 17") + } +} + +func getAndroidBindTarget() string { + if platform != "" { + return platform + } else if debugEnabled { + return "android/arm64" + } + return "android" +} + +func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) { + args := []string{ + "bind", + "-v", + "-o", config.OutputName, + "-target", bindTarget, + "-androidapi", strconv.Itoa(config.AndroidAPI), + "-javapkg=io.nekohasekai", + "-libname=box", + } + + if !debugEnabled { + args = append(args, sharedFlags...) + } else { + args = append(args, debugFlags...) + } + + args = append(args, "-tags", strings.Join(config.Tags, ",")) + args = append(args, "./experimental/libbox") + + command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Run() + if err != nil { + log.Fatal(err) + } + + copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") + if rw.IsDir(copyPath) { + copyPath, _ = filepath.Abs(copyPath) + err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName)) + if err != nil { + log.Fatal(err) + } + log.Info("copied ", config.OutputName, " to ", copyPath) + } +} + +func buildAndroid() { + build_shared.FindSDK() + checkJavaVersion() + + bindTarget := getAndroidBindTarget() + + // Build main variant (SDK 23) + mainTags := append([]string{}, sharedTags...) + // mainTags = append(mainTags, memcTags...) + if debugEnabled { + mainTags = append(mainTags, debugTags...) + } + buildAndroidVariant(AndroidBuildConfig{ + AndroidAPI: 23, + OutputName: "libbox.aar", + Tags: mainTags, + }, bindTarget) + + // Build legacy variant (SDK 21, no naive outbound) + legacyTags := filterTags(sharedTags, "with_naive_outbound") + // legacyTags = append(legacyTags, memcTags...) + if debugEnabled { + legacyTags = append(legacyTags, debugTags...) + } + buildAndroidVariant(AndroidBuildConfig{ + AndroidAPI: 21, + OutputName: "libbox-legacy.aar", + Tags: legacyTags, + }, bindTarget) +} + +func buildApple() { + var bindTarget string + if platform != "" { + bindTarget = platform + } else if debugEnabled { + bindTarget = "ios" + } else { + bindTarget = "ios,iossimulator,tvos,tvossimulator,macos" + } + + args := []string{ + "bind", + "-v", + "-target", bindTarget, + "-libname=box", + "-tags-not-macos=with_low_memory", + } + //if !withTailscale { + // args = append(args, "-tags-macos="+strings.Join(memcTags, ",")) + //} + + if !debugEnabled { + args = append(args, sharedFlags...) + } else { + args = append(args, debugFlags...) + } + + tags := append(sharedTags, darwinTags...) + //if withTailscale { + // tags = append(tags, memcTags...) + //} + if debugEnabled { + tags = append(tags, debugTags...) + } + + args = append(args, "-tags", strings.Join(tags, ",")) + args = append(args, "./experimental/libbox") + + command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Run() + if err != nil { + log.Fatal(err) + } + + copyPath := filepath.Join("..", "sing-box-for-apple") + if rw.IsDir(copyPath) { + targetDir := filepath.Join(copyPath, "Libbox.xcframework") + targetDir, _ = filepath.Abs(targetDir) + os.RemoveAll(targetDir) + os.Rename("Libbox.xcframework", targetDir) + log.Info("copied to ", targetDir) + } +} diff --git a/cmd/internal/build_shared/sdk.go b/cmd/internal/build_shared/sdk.go new file mode 100644 index 00000000..5061c321 --- /dev/null +++ b/cmd/internal/build_shared/sdk.go @@ -0,0 +1,106 @@ +package build_shared + +import ( + "go/build" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/rw" +) + +var ( + androidSDKPath string + androidNDKPath string +) + +func FindSDK() { + searchPath := []string{ + "$ANDROID_HOME", + "$HOME/Android/Sdk", + "$HOME/.local/lib/android/sdk", + "$HOME/Library/Android/sdk", + } + for _, path := range searchPath { + path = os.ExpandEnv(path) + if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) { + androidSDKPath = path + break + } + } + if androidSDKPath == "" { + log.Fatal("android SDK not found") + } + if !findNDK() { + log.Fatal("android NDK not found") + } + + os.Setenv("ANDROID_HOME", androidSDKPath) + os.Setenv("ANDROID_SDK_HOME", androidSDKPath) + os.Setenv("ANDROID_NDK_HOME", androidNDKPath) + os.Setenv("NDK", androidNDKPath) + os.Setenv("PATH", os.Getenv("PATH")+":"+filepath.Join(androidNDKPath, "toolchains", "llvm", "prebuilt", runtime.GOOS+"-x86_64", "bin")) +} + +func findNDK() bool { + const fixedVersion = "28.0.13004108" + const versionFile = "source.properties" + if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { + androidNDKPath = fixedPath + return true + } + if ndkHomeEnv := os.Getenv("ANDROID_NDK_HOME"); rw.IsFile(filepath.Join(ndkHomeEnv, versionFile)) { + androidNDKPath = ndkHomeEnv + return true + } + ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk")) + if err != nil { + return false + } + versionNames := common.Map(ndkVersions, os.DirEntry.Name) + if len(versionNames) == 0 { + return false + } + sort.Slice(versionNames, func(i, j int) bool { + iVersions := strings.Split(versionNames[i], ".") + jVersions := strings.Split(versionNames[j], ".") + for k := 0; k < len(iVersions) && k < len(jVersions); k++ { + iVersion, _ := strconv.Atoi(iVersions[k]) + jVersion, _ := strconv.Atoi(jVersions[k]) + if iVersion != jVersion { + return iVersion > jVersion + } + } + return true + }) + for _, versionName := range versionNames { + currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) + if rw.IsFile(filepath.Join(currentNDKPath, versionFile)) { + androidNDKPath = currentNDKPath + log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) + return true + } + } + return false +} + +var GoBinPath string + +func FindMobile() { + goBin := filepath.Join(build.Default.GOPATH, "bin") + if runtime.GOOS == "windows" { + if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) { + log.Fatal("missing gomobile installation") + } + } else { + if !rw.IsFile(filepath.Join(goBin, "gobind")) { + log.Fatal("missing gomobile installation") + } + } + GoBinPath = goBin +} diff --git a/cmd/internal/build_shared/tag.go b/cmd/internal/build_shared/tag.go new file mode 100644 index 00000000..15f05090 --- /dev/null +++ b/cmd/internal/build_shared/tag.go @@ -0,0 +1,38 @@ +package build_shared + +import ( + "github.com/sagernet/sing-box/common/badversion" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/shell" +) + +func ReadTag() (string, error) { + currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput() + if err != nil { + return currentTag, err + } + currentTagRev, _ := shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput() + if currentTagRev == currentTag { + return currentTag[1:], nil + } + shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput() + version := badversion.Parse(currentTagRev[1:]) + return version.String() + "-" + shortCommit, nil +} + +func ReadTagVersionRev() (badversion.Version, error) { + currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()) + return badversion.Parse(currentTagRev[1:]), nil +} + +func ReadTagVersion() (badversion.Version, error) { + currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput()) + currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()) + version := badversion.Parse(currentTagRev[1:]) + if currentTagRev != currentTag { + if version.PreReleaseIdentifier == "" { + version.Patch++ + } + } + return version, nil +} diff --git a/cmd/internal/format_docs/main.go b/cmd/internal/format_docs/main.go new file mode 100644 index 00000000..061b2121 --- /dev/null +++ b/cmd/internal/format_docs/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/log" +) + +func main() { + err := filepath.Walk("docs", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".md") { + return nil + } + return processFile(path) + }) + if err != nil { + log.Fatal(err) + } +} + +func processFile(path string) error { + content, err := os.ReadFile(path) + if err != nil { + return err + } + + lines := strings.Split(string(content), "\n") + modified := false + result := make([]string, 0, len(lines)) + + inQuoteBlock := false + materialLines := []int{} // indices of :material- lines in the block + + for _, line := range lines { + // Check for quote block start + if strings.HasPrefix(line, "!!! quote \"") && strings.Contains(line, "sing-box") { + inQuoteBlock = true + materialLines = nil + result = append(result, line) + continue + } + + // Inside a quote block + if inQuoteBlock { + trimmed := strings.TrimPrefix(line, " ") + isMaterialLine := strings.HasPrefix(trimmed, ":material-") + isEmpty := strings.TrimSpace(line) == "" + isIndented := strings.HasPrefix(line, " ") + + if isMaterialLine { + materialLines = append(materialLines, len(result)) + result = append(result, line) + continue + } + + // Block ends when: + // - Empty line AFTER we've seen material lines, OR + // - Non-indented, non-empty line + blockEnds := (isEmpty && len(materialLines) > 0) || (!isEmpty && !isIndented) + if blockEnds { + // Process collected material lines + if len(materialLines) > 0 { + for j, idx := range materialLines { + isLast := j == len(materialLines)-1 + resultLine := strings.TrimRight(result[idx], " ") + if !isLast { + // Add trailing two spaces for non-last lines + resultLine += " " + } + if result[idx] != resultLine { + modified = true + result[idx] = resultLine + } + } + } + inQuoteBlock = false + materialLines = nil + } + } + + result = append(result, line) + } + + // Handle case where file ends while still in a block + if inQuoteBlock && len(materialLines) > 0 { + for j, idx := range materialLines { + isLast := j == len(materialLines)-1 + resultLine := strings.TrimRight(result[idx], " ") + if !isLast { + resultLine += " " + } + if result[idx] != resultLine { + modified = true + result[idx] = resultLine + } + } + } + + if modified { + newContent := strings.Join(result, "\n") + if !bytes.Equal(content, []byte(newContent)) { + log.Info("formatted: ", path) + return os.WriteFile(path, []byte(newContent), 0o644) + } + } + + return nil +} diff --git a/cmd/internal/protogen/main.go b/cmd/internal/protogen/main.go new file mode 100644 index 00000000..4d5023f7 --- /dev/null +++ b/cmd/internal/protogen/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "go/build" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// envFile returns the name of the Go environment configuration file. +// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 +func envFile() (string, error) { + if file := os.Getenv("GOENV"); file != "" { + if file == "off" { + return "", fmt.Errorf("GOENV=off") + } + return file, nil + } + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + if dir == "" { + return "", fmt.Errorf("missing user-config dir") + } + return filepath.Join(dir, "go", "env"), nil +} + +// GetRuntimeEnv returns the value of runtime environment variable, +// that is set by running following command: `go env -w key=value`. +func GetRuntimeEnv(key string) (string, error) { + file, err := envFile() + if err != nil { + return "", err + } + if file == "" { + return "", fmt.Errorf("missing runtime env file") + } + var data []byte + var runtimeEnv string + data, readErr := os.ReadFile(file) + if readErr != nil { + return "", readErr + } + envStrings := strings.Split(string(data), "\n") + for _, envItem := range envStrings { + envItem = strings.TrimSuffix(envItem, "\r") + envKeyValue := strings.Split(envItem, "=") + if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { + runtimeEnv = strings.TrimSpace(envKeyValue[1]) + } + } + return runtimeEnv, nil +} + +// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. +func GetGOBIN() string { + // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` + GOBIN := os.Getenv("GOBIN") + if GOBIN == "" { + var err error + // The one set by user by running `go env -w GOBIN=/path` + GOBIN, err = GetRuntimeEnv("GOBIN") + if err != nil { + // The default one that Golang uses + return filepath.Join(build.Default.GOPATH, "bin") + } + if GOBIN == "" { + return filepath.Join(build.Default.GOPATH, "bin") + } + return GOBIN + } + return GOBIN +} + +func main() { + pwd, err := os.Getwd() + if err != nil { + fmt.Println("Can not get current working directory.") + os.Exit(1) + } + + GOBIN := GetGOBIN() + binPath := os.Getenv("PATH") + pathSlice := []string{pwd, GOBIN, binPath} + binPath = strings.Join(pathSlice, string(os.PathListSeparator)) + os.Setenv("PATH", binPath) + + suffix := "" + if runtime.GOOS == "windows" { + suffix = ".exe" + } + + protoc := "protoc" + + if linkPath, err := os.Readlink(protoc); err == nil { + protoc = linkPath + } + + protoFilesMap := make(map[string][]string) + walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + + if info.IsDir() { + return nil + } + + dir := filepath.Dir(path) + filename := filepath.Base(path) + if strings.HasSuffix(filename, ".proto") && + filename != "typed_message.proto" && + filename != "descriptor.proto" { + protoFilesMap[dir] = append(protoFilesMap[dir], path) + } + + return nil + }) + if walkErr != nil { + fmt.Println(walkErr) + os.Exit(1) + } + + for _, files := range protoFilesMap { + for _, relProtoFile := range files { + args := []string{ + "-I", ".", + "--go_out", pwd, + "--go_opt", "paths=source_relative", + "--go-grpc_out", pwd, + "--go-grpc_opt", "paths=source_relative", + "--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix), + "--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix), + } + args = append(args, relProtoFile) + cmd := exec.Command(protoc, args...) + cmd.Env = append(cmd.Env, os.Environ()...) + output, cmdErr := cmd.CombinedOutput() + if len(output) > 0 { + fmt.Println(string(output)) + } + if cmdErr != nil { + fmt.Println(cmdErr) + os.Exit(1) + } + } + } + + normalizeWalkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + + if info.IsDir() { + return nil + } + + filename := filepath.Base(path) + if strings.HasSuffix(filename, ".pb.go") && + path != "config.pb.go" { + if err := NormalizeGeneratedProtoFile(path); err != nil { + fmt.Println(err) + os.Exit(1) + } + } + + return nil + }) + if normalizeWalkErr != nil { + fmt.Println(normalizeWalkErr) + os.Exit(1) + } +} + +func NormalizeGeneratedProtoFile(path string) error { + fd, err := os.OpenFile(path, os.O_RDWR, 0o644) + if err != nil { + return err + } + + _, err = fd.Seek(0, io.SeekStart) + if err != nil { + return err + } + out := bytes.NewBuffer(nil) + scanner := bufio.NewScanner(fd) + valid := false + for scanner.Scan() { + if !valid && !strings.HasPrefix(scanner.Text(), "package ") { + continue + } + valid = true + out.Write(scanner.Bytes()) + out.Write([]byte("\n")) + } + _, err = fd.Seek(0, io.SeekStart) + if err != nil { + return err + } + err = fd.Truncate(0) + if err != nil { + return err + } + _, err = io.Copy(fd, bytes.NewReader(out.Bytes())) + if err != nil { + return err + } + return nil +} diff --git a/cmd/internal/read_tag/main.go b/cmd/internal/read_tag/main.go new file mode 100644 index 00000000..c4da1de5 --- /dev/null +++ b/cmd/internal/read_tag/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "flag" + "os" + + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/common/badversion" + "github.com/sagernet/sing-box/log" +) + +var ( + flagRunInCI bool + flagRunNightly bool +) + +func init() { + flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") + flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly") +} + +func main() { + flag.Parse() + var ( + versionStr string + err error + ) + if flagRunNightly { + var version badversion.Version + version, err = build_shared.ReadTagVersion() + if err == nil { + versionStr = version.String() + } + } else { + versionStr, err = build_shared.ReadTag() + } + if flagRunInCI { + if err != nil { + log.Fatal(err) + } + err = setGitHubEnv("version", versionStr) + if err != nil { + log.Fatal(err) + } + } else { + if err != nil { + log.Error(err) + os.Stdout.WriteString("unknown\n") + } else { + os.Stdout.WriteString(versionStr + "\n") + } + } +} + +func setGitHubEnv(name string, value string) error { + outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return err + } + _, err = outputFile.WriteString(name + "=" + value + "\n") + if err != nil { + outputFile.Close() + return err + } + err = outputFile.Close() + if err != nil { + return err + } + os.Stderr.WriteString(name + "=" + value + "\n") + return nil +} diff --git a/cmd/internal/tun_bench/main.go b/cmd/internal/tun_bench/main.go new file mode 100644 index 00000000..e62841dc --- /dev/null +++ b/cmd/internal/tun_bench/main.go @@ -0,0 +1,284 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/netip" + "os" + "os/exec" + "strings" + "syscall" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/shell" +) + +var iperf3Path string + +func main() { + err := main0() + if err != nil { + log.Fatal(err) + } +} + +func main0() error { + err := shell.Exec("sudo", "ls").Run() + if err != nil { + return err + } + results, err := runTests() + if err != nil { + return err + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(results) +} + +func runTests() ([]TestResult, error) { + boxPaths := []string{ + os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"), + //"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box", + "./sing-box", + } + stacks := []string{ + "gvisor", + "system", + } + mtus := []int{ + 1500, + 4064, + // 16384, + // 32768, + // 49152, + 65535, + } + flagList := [][]string{ + {}, + } + var results []TestResult + for _, boxPath := range boxPaths { + for _, stack := range stacks { + for _, mtu := range mtus { + if strings.HasPrefix(boxPath, ".") { + for _, flags := range flagList { + result, err := testOnce(boxPath, stack, mtu, false, flags) + if err != nil { + return nil, err + } + results = append(results, *result) + } + } else { + result, err := testOnce(boxPath, stack, mtu, false, nil) + if err != nil { + return nil, err + } + results = append(results, *result) + } + } + } + } + return results, nil +} + +type TestResult struct { + BoxPath string `json:"box_path"` + Stack string `json:"stack"` + MTU int `json:"mtu"` + Flags []string `json:"flags"` + MultiThread bool `json:"multi_thread"` + UploadSpeed string `json:"upload_speed"` + DownloadSpeed string `json:"download_speed"` +} + +func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) { + testAddress := netip.MustParseAddr("1.1.1.1") + testConfig := option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeTun, + Options: &option.TunInboundOptions{ + Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")}, + AutoRoute: true, + MTU: uint32(mtu), + Stack: stackName, + RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())}, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + IPCIDR: []string{testAddress.String()}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRouteOptions, + RouteOptionsOptions: option.RouteOptionsActionOptions{ + OverrideAddress: "127.0.0.1", + }, + }, + }, + }, + }, + AutoDetectInterface: true, + }, + } + ctx := include.Context(context.Background()) + tempConfig, err := os.CreateTemp("", "tun-bench-*.json") + if err != nil { + return + } + defer os.Remove(tempConfig.Name()) + encoder := json.NewEncoderContext(ctx, tempConfig) + encoder.SetIndent("", " ") + err = encoder.Encode(testConfig) + if err != nil { + return nil, E.Cause(err, "encode test config") + } + tempConfig.Close() + var sudoArgs []string + if len(flags) > 0 { + sudoArgs = append(sudoArgs, "env") + sudoArgs = append(sudoArgs, flags...) + } + sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name()) + boxProcess := shell.Exec("sudo", sudoArgs...) + boxProcess.Stdout = &stderrWriter{} + boxProcess.Stderr = io.Discard + err = boxProcess.Start() + if err != nil { + return + } + + if C.IsDarwin { + iperf3Path, err = exec.LookPath("iperf3-darwin") + } else { + iperf3Path, err = exec.LookPath("iperf3") + } + if err != nil { + return + } + serverProcess := shell.Exec(iperf3Path, "-s") + serverProcess.Stdout = io.Discard + serverProcess.Stderr = io.Discard + err = serverProcess.Start() + if err != nil { + return nil, E.Cause(err, "start iperf3 server") + } + + time.Sleep(time.Second) + + args := []string{"-c", testAddress.String()} + if multiThread { + args = append(args, "-P", "10") + } + + uploadProcess := shell.Exec(iperf3Path, args...) + output, err := uploadProcess.Read() + if err != nil { + boxProcess.Process.Signal(syscall.SIGKILL) + serverProcess.Process.Signal(syscall.SIGKILL) + println(output) + return + } + + uploadResult := common.SubstringBeforeLast(output, "iperf Done.") + uploadResult = common.SubstringBeforeLast(uploadResult, "sender") + uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec") + uploadResult = common.SubstringAfterLast(uploadResult, "Bytes") + uploadResult = strings.ReplaceAll(uploadResult, " ", "") + + result = &TestResult{ + BoxPath: boxPath, + Stack: stackName, + MTU: mtu, + Flags: flags, + MultiThread: multiThread, + UploadSpeed: uploadResult, + } + + downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...) + output, err = downloadProcess.Read() + if err != nil { + boxProcess.Process.Signal(syscall.SIGKILL) + serverProcess.Process.Signal(syscall.SIGKILL) + println(output) + return + } + + downloadResult := common.SubstringBeforeLast(output, "iperf Done.") + downloadResult = common.SubstringBeforeLast(downloadResult, "receiver") + downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec") + downloadResult = common.SubstringAfterLast(downloadResult, "Bytes") + downloadResult = strings.ReplaceAll(downloadResult, " ", "") + + result.DownloadSpeed = downloadResult + + printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult} + if len(flags) > 0 { + printArgs = append(printArgs, "flags", strings.Join(flags, " ")) + } + if multiThread { + printArgs = append(printArgs, "(-P 10)") + } + fmt.Println(printArgs...) + err = boxProcess.Process.Signal(syscall.SIGTERM) + if err != nil { + return + } + + err = serverProcess.Process.Signal(syscall.SIGTERM) + if err != nil { + return + } + + boxDone := make(chan struct{}) + go func() { + boxProcess.Cmd.Wait() + close(boxDone) + }() + + serverDone := make(chan struct{}) + go func() { + serverProcess.Process.Wait() + close(serverDone) + }() + + select { + case <-boxDone: + case <-time.After(2 * time.Second): + boxProcess.Process.Kill() + case <-time.After(4 * time.Second): + println("box process did not close!") + os.Exit(1) + } + + select { + case <-serverDone: + case <-time.After(2 * time.Second): + serverProcess.Process.Kill() + case <-time.After(4 * time.Second): + println("server process did not close!") + os.Exit(1) + } + + return +} + +type stderrWriter struct{} + +func (w *stderrWriter) Write(p []byte) (n int, err error) { + return os.Stderr.Write(p) +} diff --git a/cmd/internal/update_android_version/main.go b/cmd/internal/update_android_version/main.go new file mode 100644 index 00000000..4850fce0 --- /dev/null +++ b/cmd/internal/update_android_version/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" +) + +var ( + flagRunInCI bool + flagRunNightly bool +) + +func init() { + flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") + flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly") +} + +func main() { + flag.Parse() + newVersion := common.Must1(build_shared.ReadTag()) + var androidPath string + if flagRunInCI { + androidPath = "clients/android" + } else { + androidPath = "../sing-box-for-android" + } + androidPath, err := filepath.Abs(androidPath) + if err != nil { + log.Fatal(err) + } + common.Must(os.Chdir(androidPath)) + localProps := common.Must1(os.ReadFile("version.properties")) + var propsList [][]string + for _, propLine := range strings.Split(string(localProps), "\n") { + propsList = append(propsList, strings.Split(propLine, "=")) + } + var ( + versionUpdated bool + goVersionUpdated bool + ) + for _, propPair := range propsList { + switch propPair[0] { + case "VERSION_NAME": + if propPair[1] != newVersion { + log.Info("updated version from ", propPair[1], " to ", newVersion) + versionUpdated = true + propPair[1] = newVersion + } + case "GO_VERSION": + if propPair[1] != runtime.Version() { + log.Info("updated Go version from ", propPair[1], " to ", runtime.Version()) + goVersionUpdated = true + propPair[1] = runtime.Version() + } + } + } + if !(versionUpdated || goVersionUpdated) { + log.Info("version not changed") + return + } else if flagRunInCI && !flagRunNightly { + log.Fatal("version changed, commit changes first.") + } + for _, propPair := range propsList { + switch propPair[0] { + case "VERSION_CODE": + versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64)) + propPair[1] = strconv.Itoa(int(versionCode + 1)) + log.Info("updated version code to ", propPair[1]) + } + } + var newProps []string + for _, propPair := range propsList { + newProps = append(newProps, strings.Join(propPair, "=")) + } + common.Must(os.WriteFile("version.properties", []byte(strings.Join(newProps, "\n")), 0o644)) +} diff --git a/cmd/internal/update_apple_version/main.go b/cmd/internal/update_apple_version/main.go new file mode 100644 index 00000000..1b2d0db5 --- /dev/null +++ b/cmd/internal/update_apple_version/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + + "howett.net/plist" +) + +var flagRunInCI bool + +func init() { + flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") +} + +func main() { + flag.Parse() + newVersion := common.Must1(build_shared.ReadTagVersion()) + var applePath string + if flagRunInCI { + applePath = "clients/apple" + } else { + applePath = "../sing-box-for-apple" + } + applePath, err := filepath.Abs(applePath) + if err != nil { + log.Fatal(err) + } + common.Must(os.Chdir(applePath)) + projectFile := common.Must1(os.Open("sing-box.xcodeproj/project.pbxproj")) + var project map[string]any + decoder := plist.NewDecoder(projectFile) + common.Must(decoder.Decode(&project)) + objectsMap := project["objects"].(map[string]any) + projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj"))) + newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString()) + newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String()) + if updated0 || updated1 { + log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")") + } + var updated2 bool + if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" { + newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion) + if updated2 { + log.Info("updated macos project version to ", macProjectVersion) + } + } + if updated0 || updated1 || updated2 { + common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644)) + } +} + +func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDList []string, newVersion string) (string, bool) { + objectKeyList := findObjectKey(objectsMap, bundleIDList) + var updated bool + for _, objectKey := range objectKeyList { + matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{")) + indexes := matchRegexp.FindStringIndex(projectContent) + if len(indexes) < 2 { + println(projectContent) + log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey)) + } + indexStart := indexes[1] + indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}") + versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20 + versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";") + version := strings.Trim(projectContent[versionStart:versionEnd], "\"") + if version == newVersion { + continue + } + updated = true + projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:] + } + return projectContent, updated +} + +func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) { + objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList) + var updated bool + for _, objectKey := range objectKeyList { + matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{")) + indexes := matchRegexp.FindStringIndex(projectContent) + if len(indexes) < 2 { + println(projectContent) + log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey)) + } + indexStart := indexes[1] + indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}") + versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "CURRENT_PROJECT_VERSION = ") + 26 + versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";") + version := projectContent[versionStart:versionEnd] + if version == newVersion { + continue + } + updated = true + projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:] + } + return projectContent, updated +} + +func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string { + var objectKeyList []string + for objectKey, object := range objectsMap { + buildSettings := object.(map[string]any)["buildSettings"] + if buildSettings == nil { + continue + } + bundleIDObject := buildSettings.(map[string]any)["PRODUCT_BUNDLE_IDENTIFIER"] + if bundleIDObject == nil { + continue + } + if common.Contains(bundleIDList, bundleIDObject.(string)) { + objectKeyList = append(objectKeyList, objectKey) + } + } + return objectKeyList +} + +func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string { + var objectKeyList []string + for objectKey, object := range objectsMap { + buildSettings := object.(map[string]any)["buildSettings"] + if buildSettings == nil { + continue + } + infoPListFile := buildSettings.(map[string]any)["INFOPLIST_FILE"] + if infoPListFile == nil { + continue + } + for _, searchDirectory := range directoryList { + if strings.HasPrefix(infoPListFile.(string), searchDirectory+"/") { + objectKeyList = append(objectKeyList, objectKey) + } + } + + } + return objectKeyList +} diff --git a/cmd/internal/update_certificates/main.go b/cmd/internal/update_certificates/main.go new file mode 100644 index 00000000..55b221e1 --- /dev/null +++ b/cmd/internal/update_certificates/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "encoding/csv" + "io" + "net/http" + "os" + "strings" + + "github.com/sagernet/sing-box/log" + + "golang.org/x/exp/slices" +) + +func main() { + err := updateMozillaIncludedRootCAs() + if err != nil { + log.Error(err) + } + err = updateChromeIncludedRootCAs() + if err != nil { + log.Error(err) + } +} + +func updateMozillaIncludedRootCAs() error { + response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV") + if err != nil { + return err + } + defer response.Body.Close() + reader := csv.NewReader(response.Body) + header, err := reader.Read() + if err != nil { + return err + } + geoIndex := slices.Index(header, "Geographic Focus") + nameIndex := slices.Index(header, "Common Name or Certificate Name") + certIndex := slices.Index(header, "PEM Info") + + generated := strings.Builder{} + generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT. + +package certificate + +import "crypto/x509" + +var mozillaIncluded *x509.CertPool + +func init() { + mozillaIncluded = x509.NewCertPool() +`) + for { + record, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return err + } + if record[geoIndex] == "China" { + continue + } + generated.WriteString("\n // ") + generated.WriteString(record[nameIndex]) + generated.WriteString("\n") + generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`") + cert := record[certIndex] + // Remove single quotes + cert = cert[1 : len(cert)-1] + generated.WriteString(cert) + generated.WriteString("`))\n") + } + generated.WriteString("}\n") + return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644) +} + +func fetchChinaFingerprints() (map[string]bool, error) { + response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv4") + if err != nil { + return nil, err + } + defer response.Body.Close() + reader := csv.NewReader(response.Body) + header, err := reader.Read() + if err != nil { + return nil, err + } + countryIndex := slices.Index(header, "Country") + fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint") + + chinaFingerprints := make(map[string]bool) + for { + record, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + if record[countryIndex] == "China" { + chinaFingerprints[record[fingerprintIndex]] = true + } + } + return chinaFingerprints, nil +} + +func updateChromeIncludedRootCAs() error { + chinaFingerprints, err := fetchChinaFingerprints() + if err != nil { + return err + } + + response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/RootCACertificatesIncludedByRSReportCSV") + if err != nil { + return err + } + defer response.Body.Close() + reader := csv.NewReader(response.Body) + header, err := reader.Read() + if err != nil { + return err + } + subjectIndex := slices.Index(header, "Subject") + statusIndex := slices.Index(header, "Google Chrome Status") + certIndex := slices.Index(header, "X.509 Certificate (PEM)") + fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint") + + generated := strings.Builder{} + generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT. + +package certificate + +import "crypto/x509" + +var chromeIncluded *x509.CertPool + +func init() { + chromeIncluded = x509.NewCertPool() +`) + for { + record, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return err + } + if record[statusIndex] != "Included" { + continue + } + if chinaFingerprints[record[fingerprintIndex]] { + continue + } + generated.WriteString("\n // ") + generated.WriteString(record[subjectIndex]) + generated.WriteString("\n") + generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`") + cert := record[certIndex] + // Remove single quotes if present + if len(cert) > 0 && cert[0] == '\'' { + cert = cert[1 : len(cert)-1] + } + generated.WriteString(cert) + generated.WriteString("`))\n") + } + generated.WriteString("}\n") + return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644) +} diff --git a/cmd/sing-box/cmd.go b/cmd/sing-box/cmd.go new file mode 100644 index 00000000..575cb7a0 --- /dev/null +++ b/cmd/sing-box/cmd.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "os" + "os/user" + "strconv" + "time" + + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" + + "github.com/spf13/cobra" +) + +var ( + globalCtx context.Context + configPaths []string + configDirectories []string + workingDir string + disableColor bool +) + +var mainCommand = &cobra.Command{ + Use: "sing-box", + PersistentPreRun: preRun, +} + +func init() { + mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path") + mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path") + mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory") + mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output") +} + +func preRun(cmd *cobra.Command, args []string) { + globalCtx = context.Background() + sudoUser := os.Getenv("SUDO_USER") + sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID")) + sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID")) + if sudoUID == 0 && sudoGID == 0 && sudoUser != "" { + sudoUserObject, _ := user.Lookup(sudoUser) + if sudoUserObject != nil { + sudoUID, _ = strconv.Atoi(sudoUserObject.Uid) + sudoGID, _ = strconv.Atoi(sudoUserObject.Gid) + } + } + if sudoUID > 0 && sudoGID > 0 { + globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID) + } + if disableColor { + log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger()) + } + if workingDir != "" { + _, err := os.Stat(workingDir) + if err != nil { + filemanager.MkdirAll(globalCtx, workingDir, 0o777) + } + err = os.Chdir(workingDir) + if err != nil { + log.Fatal(err) + } + } + if len(configPaths) == 0 && len(configDirectories) == 0 { + configPaths = append(configPaths, "config.json") + } + globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))) +} diff --git a/cmd/sing-box/cmd_check.go b/cmd/sing-box/cmd_check.go new file mode 100644 index 00000000..29a39081 --- /dev/null +++ b/cmd/sing-box/cmd_check.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + + "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandCheck = &cobra.Command{ + Use: "check", + Short: "Check configuration", + Run: func(cmd *cobra.Command, args []string) { + err := check() + if err != nil { + log.Fatal(err) + } + }, + Args: cobra.NoArgs, +} + +func init() { + mainCommand.AddCommand(commandCheck) +} + +func check() error { + options, err := readConfigAndMerge() + if err != nil { + return err + } + ctx, cancel := context.WithCancel(globalCtx) + instance, err := box.New(box.Options{ + Context: ctx, + Options: options, + }) + if err == nil { + instance.Close() + } + cancel() + return err +} diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go new file mode 100644 index 00000000..ab59c9ae --- /dev/null +++ b/cmd/sing-box/cmd_format.go @@ -0,0 +1,75 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + + "github.com/spf13/cobra" +) + +var commandFormatFlagWrite bool + +var commandFormat = &cobra.Command{ + Use: "format", + Short: "Format configuration", + Run: func(cmd *cobra.Command, args []string) { + err := format() + if err != nil { + log.Fatal(err) + } + }, + Args: cobra.NoArgs, +} + +func init() { + commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + mainCommand.AddCommand(commandFormat) +} + +func format() error { + optionsList, err := readConfig() + if err != nil { + return err + } + for _, optionsEntry := range optionsList { + optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(optionsEntry.options) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(optionsEntry.path) + if !commandFormatFlagWrite { + if len(optionsList) > 1 { + os.Stdout.WriteString(outputPath + "\n") + } + os.Stdout.WriteString(buffer.String() + "\n") + continue + } + if bytes.Equal(optionsEntry.content, buffer.Bytes()) { + continue + } + output, err := os.Create(optionsEntry.path) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_generate.go b/cmd/sing-box/cmd_generate.go new file mode 100644 index 00000000..74d64c55 --- /dev/null +++ b/cmd/sing-box/cmd_generate.go @@ -0,0 +1,92 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "os" + "strconv" + + "github.com/sagernet/sing-box/log" + + "github.com/gofrs/uuid/v5" + "github.com/spf13/cobra" +) + +var commandGenerate = &cobra.Command{ + Use: "generate", + Short: "Generate things", +} + +func init() { + commandGenerate.AddCommand(commandGenerateUUID) + commandGenerate.AddCommand(commandGenerateRandom) + + mainCommand.AddCommand(commandGenerate) +} + +var ( + outputBase64 bool + outputHex bool +) + +var commandGenerateRandom = &cobra.Command{ + Use: "rand ", + Short: "Generate random bytes", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := generateRandom(args) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGenerateRandom.Flags().BoolVar(&outputBase64, "base64", false, "Generate base64 string") + commandGenerateRandom.Flags().BoolVar(&outputHex, "hex", false, "Generate hex string") +} + +func generateRandom(args []string) error { + length, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + + randomBytes := make([]byte, length) + _, err = rand.Read(randomBytes) + if err != nil { + return err + } + + if outputBase64 { + _, err = os.Stdout.WriteString(base64.StdEncoding.EncodeToString(randomBytes) + "\n") + } else if outputHex { + _, err = os.Stdout.WriteString(hex.EncodeToString(randomBytes) + "\n") + } else { + _, err = os.Stdout.Write(randomBytes) + } + + return err +} + +var commandGenerateUUID = &cobra.Command{ + Use: "uuid", + Short: "Generate UUID string", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + err := generateUUID() + if err != nil { + log.Fatal(err) + } + }, +} + +func generateUUID() error { + newUUID, err := uuid.NewV4() + if err != nil { + return err + } + _, err = os.Stdout.WriteString(newUUID.String() + "\n") + return err +} diff --git a/cmd/sing-box/cmd_generate_ech.go b/cmd/sing-box/cmd_generate_ech.go new file mode 100644 index 00000000..7942d588 --- /dev/null +++ b/cmd/sing-box/cmd_generate_ech.go @@ -0,0 +1,36 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGenerateECHKeyPair = &cobra.Command{ + Use: "ech-keypair ", + Short: "Generate TLS ECH key pair", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := generateECHKeyPair(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGenerate.AddCommand(commandGenerateECHKeyPair) +} + +func generateECHKeyPair(serverName string) error { + configPem, keyPem, err := tls.ECHKeygenDefault(serverName) + if err != nil { + return err + } + os.Stdout.WriteString(configPem) + os.Stdout.WriteString(keyPem) + return nil +} diff --git a/cmd/sing-box/cmd_generate_tls.go b/cmd/sing-box/cmd_generate_tls.go new file mode 100644 index 00000000..4cd4060d --- /dev/null +++ b/cmd/sing-box/cmd_generate_tls.go @@ -0,0 +1,40 @@ +package main + +import ( + "os" + "time" + + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var flagGenerateTLSKeyPairMonths int + +var commandGenerateTLSKeyPair = &cobra.Command{ + Use: "tls-keypair ", + Short: "Generate TLS self sign key pair", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := generateTLSKeyPair(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGenerateTLSKeyPair.Flags().IntVarP(&flagGenerateTLSKeyPairMonths, "months", "m", 1, "Valid months") + commandGenerate.AddCommand(commandGenerateTLSKeyPair) +} + +func generateTLSKeyPair(serverName string) error { + privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0)) + if err != nil { + return err + } + os.Stdout.WriteString(string(privateKeyPem) + "\n") + os.Stdout.WriteString(string(publicKeyPem) + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_generate_vapid.go b/cmd/sing-box/cmd_generate_vapid.go new file mode 100644 index 00000000..e83e6d80 --- /dev/null +++ b/cmd/sing-box/cmd_generate_vapid.go @@ -0,0 +1,40 @@ +//go:build go1.20 + +package main + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGenerateVAPIDKeyPair = &cobra.Command{ + Use: "vapid-keypair", + Short: "Generate VAPID key pair", + Run: func(cmd *cobra.Command, args []string) { + err := generateVAPIDKeyPair() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGenerate.AddCommand(commandGenerateVAPIDKeyPair) +} + +func generateVAPIDKeyPair() error { + privateKey, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return err + } + publicKey := privateKey.PublicKey() + os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes()) + "\n") + os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey.Bytes()) + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_generate_wireguard.go b/cmd/sing-box/cmd_generate_wireguard.go new file mode 100644 index 00000000..087e74b2 --- /dev/null +++ b/cmd/sing-box/cmd_generate_wireguard.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/base64" + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func init() { + commandGenerate.AddCommand(commandGenerateWireGuardKeyPair) + commandGenerate.AddCommand(commandGenerateRealityKeyPair) +} + +var commandGenerateWireGuardKeyPair = &cobra.Command{ + Use: "wg-keypair", + Short: "Generate WireGuard key pair", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + err := generateWireGuardKey() + if err != nil { + log.Fatal(err) + } + }, +} + +func generateWireGuardKey() error { + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return err + } + os.Stdout.WriteString("PrivateKey: " + privateKey.String() + "\n") + os.Stdout.WriteString("PublicKey: " + privateKey.PublicKey().String() + "\n") + return nil +} + +var commandGenerateRealityKeyPair = &cobra.Command{ + Use: "reality-keypair", + Short: "Generate reality key pair", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + err := generateRealityKey() + if err != nil { + log.Fatal(err) + } + }, +} + +func generateRealityKey() error { + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return err + } + publicKey := privateKey.PublicKey() + os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]) + "\n") + os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]) + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 00000000..dbbbff13 --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 00000000..b80e5cd3 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + 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/json" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion2 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 00000000..54dd426e --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 00000000..d5157bb4 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 00000000..95db9357 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 00000000..90a7955b --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion2 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 00000000..cedb7adf --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 00000000..f648ce62 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 00000000..791dba24 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go new file mode 100644 index 00000000..6ca9a15a --- /dev/null +++ b/cmd/sing-box/cmd_merge.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/rw" + + "github.com/spf13/cobra" +) + +var commandMerge = &cobra.Command{ + Use: "merge ", + Short: "Merge configurations", + Run: func(cmd *cobra.Command, args []string) { + err := merge(args[0]) + if err != nil { + log.Fatal(err) + } + }, + Args: cobra.ExactArgs(1), +} + +func init() { + mainCommand.AddCommand(commandMerge) +} + +func merge(outputPath string) error { + mergedOptions, err := readConfigAndMerge() + if err != nil { + return err + } + err = mergePathResources(&mergedOptions) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(mergedOptions) + if err != nil { + return E.Cause(err, "encode config") + } + if existsContent, err := os.ReadFile(outputPath); err != nil { + if string(existsContent) == buffer.String() { + return nil + } + } + err = rw.MkdirParent(outputPath) + if err != nil { + return err + } + err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) + if err != nil { + return err + } + outputPath, _ = filepath.Abs(outputPath) + os.Stderr.WriteString(outputPath + "\n") + return nil +} + +func mergePathResources(options *option.Options) error { + for _, inbound := range options.Inbounds { + if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions { + tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions())) + } + } + for _, outbound := range options.Outbounds { + switch outbound.Type { + case C.TypeSSH: + mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions)) + } + if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions { + tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions())) + } + } + return nil +} + +func mergeTLSInboundOptions(options *option.InboundTLSOptions) *option.InboundTLSOptions { + if options == nil { + return nil + } + if options.CertificatePath != "" { + if content, err := os.ReadFile(options.CertificatePath); err == nil { + options.Certificate = trimStringArray(strings.Split(string(content), "\n")) + } + } + if options.KeyPath != "" { + if content, err := os.ReadFile(options.KeyPath); err == nil { + options.Key = trimStringArray(strings.Split(string(content), "\n")) + } + } + if options.ECH != nil { + if options.ECH.KeyPath != "" { + if content, err := os.ReadFile(options.ECH.KeyPath); err == nil { + options.ECH.Key = trimStringArray(strings.Split(string(content), "\n")) + } + } + } + return options +} + +func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.OutboundTLSOptions { + if options == nil { + return nil + } + if options.CertificatePath != "" { + if content, err := os.ReadFile(options.CertificatePath); err == nil { + options.Certificate = trimStringArray(strings.Split(string(content), "\n")) + } + } + if options.ECH != nil { + if options.ECH.ConfigPath != "" { + if content, err := os.ReadFile(options.ECH.ConfigPath); err == nil { + options.ECH.Config = trimStringArray(strings.Split(string(content), "\n")) + } + } + } + return options +} + +func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) { + if options.PrivateKeyPath != "" { + if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil { + options.PrivateKey = trimStringArray(strings.Split(string(content), "\n")) + } + } +} + +func trimStringArray(array []string) []string { + return common.Filter(array, func(it string) bool { + return strings.TrimSpace(it) != "" + }) +} diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 00000000..242ea8b6 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule-sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 00000000..e2cbefc7 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,102 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options)) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} + +func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 { + if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { + return len(rule.PackageNameRegex) > 0 + }) { + version = C.RuleSetVersion4 + } + if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { + return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 || + len(rule.DefaultInterfaceAddress) > 0 + }) { + version = C.RuleSetVersion3 + } + if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { + return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained + }) { + version = C.RuleSetVersion2 + } + return version +} diff --git a/cmd/sing-box/cmd_rule_set_convert.go b/cmd/sing-box/cmd_rule_set_convert.go new file mode 100644 index 00000000..f4f8d2ca --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_convert.go @@ -0,0 +1,89 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/convertor/adguard" + "github.com/sagernet/sing-box/common/srs" + 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/spf13/cobra" +) + +var ( + flagRuleSetConvertType string + flagRuleSetConvertOutput string +) + +var commandRuleSetConvert = &cobra.Command{ + Use: "convert [source-path]", + Short: "Convert adguard DNS filter to rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := convertRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetConvert) + commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard") + commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func convertRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + var rules []option.HeadlessRule + switch flagRuleSetConvertType { + case "adguard": + rules, err = adguard.ToOptions(reader, log.StdLogger()) + case "": + return E.New("source type is required") + default: + return E.New("unsupported source type: ", flagRuleSetConvertType) + } + if err != nil { + return err + } + var outputPath string + if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".txt") { + outputPath = sourcePath[:len(sourcePath)-4] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetConvertOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer outputFile.Close() + err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_decompile.go b/cmd/sing-box/cmd_rule_set_decompile.go new file mode 100644 index 00000000..6fc43d42 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_decompile.go @@ -0,0 +1,101 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/srs" + 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/json" + + "github.com/spf13/cobra" +) + +var flagRuleSetDecompileOutput string + +const flagRuleSetDecompileDefaultOutput = ".json" + +var commandRuleSetDecompile = &cobra.Command{ + Use: "decompile [binary-path]", + Short: "Decompile rule-set binary to json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := decompileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetDecompile) + commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file") +} + +func decompileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + ruleSet, err := srs.Read(reader, true) + if err != nil { + return err + } + if hasRule(ruleSet.Options.Rules, func(rule option.DefaultHeadlessRule) bool { + return len(rule.AdGuardDomain) > 0 + }) { + return E.New("unable to decompile binary AdGuard rules to rule-set.") + } + var outputPath string + if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".srs") { + outputPath = sourcePath[:len(sourcePath)-4] + ".json" + } else { + outputPath = sourcePath + ".json" + } + } else { + outputPath = flagRuleSetDecompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + encoder := json.NewEncoder(outputFile) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} + +func hasRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 00000000..6276204c --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(plainRuleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go new file mode 100644 index 00000000..e3dc19b8 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" + M "github.com/sagernet/sing/common/metadata" + + "github.com/spf13/cobra" +) + +var flagRuleSetMatchFormat string + +var commandRuleSetMatch = &cobra.Command{ + Use: "match ", + Short: "Check if an IP address or a domain matches the rule-set", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + err := ruleSetMatch(args[0], args[1]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format") + commandRuleSet.AddCommand(commandRuleSetMatch) +} + +func ruleSetMatch(sourcePath string, domain string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return E.Cause(err, "read rule-set") + } + } + content, err := io.ReadAll(reader) + if err != nil { + return E.Cause(err, "read rule-set") + } + if flagRuleSetMatchFormat == "" { + switch filepath.Ext(sourcePath) { + case ".json": + flagRuleSetMatchFormat = C.RuleSetFormatSource + case ".srs": + flagRuleSetMatchFormat = C.RuleSetFormatBinary + } + } + var ruleSet option.PlainRuleSetCompat + switch flagRuleSetMatchFormat { + case C.RuleSetFormatSource: + ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + case C.RuleSetFormatBinary: + ruleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule-set format: ", flagRuleSetMatchFormat) + } + plainRuleSet, err := ruleSet.Upgrade() + if err != nil { + return err + } + ipAddress := M.ParseAddr(domain) + var metadata adapter.InboundContext + if ipAddress.IsValid() { + metadata.Destination = M.SocksaddrFrom(ipAddress, 0) + } else { + metadata.Domain = domain + } + for i, ruleOptions := range plainRuleSet.Rules { + var currentRule adapter.HeadlessRule + currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + if currentRule.Match(&metadata) { + println(F.ToString("match rules.[", i, "]: ", currentRule)) + } + } + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_merge.go b/cmd/sing-box/cmd_rule_set_merge.go new file mode 100644 index 00000000..7c8f7a53 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_merge.go @@ -0,0 +1,162 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/rw" + + "github.com/spf13/cobra" +) + +var ( + ruleSetPaths []string + ruleSetDirectories []string +) + +var commandRuleSetMerge = &cobra.Command{ + Use: "merge ", + Short: "Merge rule-set source files", + Run: func(cmd *cobra.Command, args []string) { + err := mergeRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, + Args: cobra.ExactArgs(1), +} + +func init() { + commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path") + commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path") + commandRuleSet.AddCommand(commandRuleSetMerge) +} + +type RuleSetEntry struct { + content []byte + path string + options option.PlainRuleSetCompat +} + +func readRuleSetAt(path string) (*RuleSetEntry, error) { + var ( + configContent []byte + err error + ) + if path == "stdin" { + configContent, err = io.ReadAll(os.Stdin) + } else { + configContent, err = os.ReadFile(path) + } + if err != nil { + return nil, E.Cause(err, "read config at ", path) + } + options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent) + if err != nil { + return nil, E.Cause(err, "decode config at ", path) + } + return &RuleSetEntry{ + content: configContent, + path: path, + options: options, + }, nil +} + +func readRuleSet() ([]*RuleSetEntry, error) { + var optionsList []*RuleSetEntry + for _, path := range ruleSetPaths { + optionsEntry, err := readRuleSetAt(path) + if err != nil { + return nil, err + } + optionsList = append(optionsList, optionsEntry) + } + for _, directory := range ruleSetDirectories { + entries, err := os.ReadDir(directory) + if err != nil { + return nil, E.Cause(err, "read rule-set directory at ", directory) + } + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() { + continue + } + optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name())) + if err != nil { + return nil, err + } + optionsList = append(optionsList, optionsEntry) + } + } + sort.Slice(optionsList, func(i, j int) bool { + return optionsList[i].path < optionsList[j].path + }) + return optionsList, nil +} + +func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) { + optionsList, err := readRuleSet() + if err != nil { + return option.PlainRuleSetCompat{}, err + } + if len(optionsList) == 1 { + return optionsList[0].options, nil + } + var optionVersion uint8 + for _, options := range optionsList { + if optionVersion < options.options.Version { + optionVersion = options.options.Version + } + } + var mergedMessage json.RawMessage + for _, options := range optionsList { + mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) + if err != nil { + return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path) + } + } + mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage) + if err != nil { + return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config") + } + mergedOptions.Version = optionVersion + return mergedOptions, nil +} + +func mergeRuleSet(outputPath string) error { + mergedOptions, err := readRuleSetAndMerge() + if err != nil { + return err + } + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(mergedOptions) + if err != nil { + return E.Cause(err, "encode config") + } + if existsContent, err := os.ReadFile(outputPath); err != nil { + if string(existsContent) == buffer.String() { + return nil + } + } + err = rw.MkdirParent(outputPath) + if err != nil { + return err + } + err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) + if err != nil { + return err + } + outputPath, _ = filepath.Abs(outputPath) + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_upgrade.go b/cmd/sing-box/cmd_rule_set_upgrade.go new file mode 100644 index 00000000..3d77a1d2 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_upgrade.go @@ -0,0 +1,95 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + 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/json" + + "github.com/spf13/cobra" +) + +var commandRuleSetUpgradeFlagWrite bool + +var commandRuleSetUpgrade = &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := upgradeRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetUpgrade) +} + +func upgradeRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + switch plainRuleSetCompat.Version { + case C.RuleSetVersion1: + default: + log.Info("already up-to-date") + return nil + } + plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade() + if err != nil { + return err + } + plainRuleSetCompat.Version = C.RuleSetVersionCurrent + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(plainRuleSetCompat) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go new file mode 100644 index 00000000..f31db9dc --- /dev/null +++ b/cmd/sing-box/cmd_run.go @@ -0,0 +1,212 @@ +package main + +import ( + "context" + "io" + "os" + "os/signal" + "path/filepath" + runtimeDebug "runtime/debug" + "sort" + "strings" + "syscall" + "time" + + "github.com/sagernet/sing-box" + 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/json" + "github.com/sagernet/sing/common/json/badjson" + + "github.com/spf13/cobra" +) + +var commandRun = &cobra.Command{ + Use: "run", + Short: "Run service", + Run: func(cmd *cobra.Command, args []string) { + err := run() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + mainCommand.AddCommand(commandRun) +} + +type OptionsEntry struct { + content []byte + path string + options option.Options +} + +func readConfigAt(path string) (*OptionsEntry, error) { + var ( + configContent []byte + err error + ) + if path == "stdin" { + configContent, err = io.ReadAll(os.Stdin) + } else { + configContent, err = os.ReadFile(path) + } + if err != nil { + return nil, E.Cause(err, "read config at ", path) + } + options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent) + if err != nil { + return nil, E.Cause(err, "decode config at ", path) + } + return &OptionsEntry{ + content: configContent, + path: path, + options: options, + }, nil +} + +func readConfig() ([]*OptionsEntry, error) { + var optionsList []*OptionsEntry + for _, path := range configPaths { + optionsEntry, err := readConfigAt(path) + if err != nil { + return nil, err + } + optionsList = append(optionsList, optionsEntry) + } + for _, directory := range configDirectories { + entries, err := os.ReadDir(directory) + if err != nil { + return nil, E.Cause(err, "read config directory at ", directory) + } + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() { + continue + } + optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name())) + if err != nil { + return nil, err + } + optionsList = append(optionsList, optionsEntry) + } + } + sort.Slice(optionsList, func(i, j int) bool { + return optionsList[i].path < optionsList[j].path + }) + return optionsList, nil +} + +func readConfigAndMerge() (option.Options, error) { + optionsList, err := readConfig() + if err != nil { + return option.Options{}, err + } + if len(optionsList) == 1 { + return optionsList[0].options, nil + } + var mergedMessage json.RawMessage + for _, options := range optionsList { + mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) + if err != nil { + return option.Options{}, E.Cause(err, "merge config at ", options.path) + } + } + var mergedOptions option.Options + err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage) + if err != nil { + return option.Options{}, E.Cause(err, "unmarshal merged config") + } + return mergedOptions, nil +} + +func create() (*box.Box, context.CancelFunc, error) { + options, err := readConfigAndMerge() + if err != nil { + return nil, nil, err + } + if disableColor { + if options.Log == nil { + options.Log = &option.LogOptions{} + } + options.Log.DisableColor = true + } + ctx, cancel := context.WithCancel(globalCtx) + instance, err := box.New(box.Options{ + Context: ctx, + Options: options, + }) + if err != nil { + cancel() + return nil, nil, E.Cause(err, "create service") + } + + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + defer func() { + signal.Stop(osSignals) + close(osSignals) + }() + startCtx, finishStart := context.WithCancel(context.Background()) + go func() { + _, loaded := <-osSignals + if loaded { + cancel() + closeMonitor(startCtx) + } + }() + err = instance.Start() + finishStart() + if err != nil { + cancel() + return nil, nil, E.Cause(err, "start service") + } + return instance, cancel, nil +} + +func run() error { + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + defer signal.Stop(osSignals) + for { + instance, cancel, err := create() + if err != nil { + return err + } + runtimeDebug.FreeOSMemory() + for { + osSignal := <-osSignals + if osSignal == syscall.SIGHUP { + err = check() + if err != nil { + log.Error(E.Cause(err, "reload service")) + continue + } + } + cancel() + closeCtx, closed := context.WithCancel(context.Background()) + go closeMonitor(closeCtx) + err = instance.Close() + closed() + if osSignal != syscall.SIGHUP { + if err != nil { + log.Error(E.Cause(err, "sing-box did not closed properly")) + } + return nil + } + break + } + } +} + +func closeMonitor(ctx context.Context) { + time.Sleep(C.FatalStopTimeout) + select { + case <-ctx.Done(): + return + default: + } + log.Fatal("sing-box did not close!") +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go new file mode 100644 index 00000000..55e5b458 --- /dev/null +++ b/cmd/sing-box/cmd_tools.go @@ -0,0 +1,54 @@ +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" +) + +var commandToolsFlagOutbound string + +var commandTools = &cobra.Command{ + Use: "tools", + Short: "Experimental tools", +} + +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 + } +} diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go new file mode 100644 index 00000000..d352d533 --- /dev/null +++ b/cmd/sing-box/cmd_tools_connect.go @@ -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
", + 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 +} diff --git a/cmd/sing-box/cmd_tools_fetch.go b/cmd/sing-box/cmd_tools_fetch.go new file mode 100644 index 00000000..5ee3b875 --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch.go @@ -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 +} diff --git a/cmd/sing-box/cmd_tools_fetch_http3.go b/cmd/sing-box/cmd_tools_fetch_http3.go new file mode 100644 index 00000000..3caa1e88 --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch_http3.go @@ -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.Conn, 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 +} diff --git a/cmd/sing-box/cmd_tools_fetch_http3_stub.go b/cmd/sing-box/cmd_tools_fetch_http3_stub.go new file mode 100644 index 00000000..ae13f54c --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch_http3_stub.go @@ -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 +} diff --git a/cmd/sing-box/cmd_tools_networkquality.go b/cmd/sing-box/cmd_tools_networkquality.go new file mode 100644 index 00000000..5f63571d --- /dev/null +++ b/cmd/sing-box/cmd_tools_networkquality.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var ( + commandNetworkQualityFlagConfigURL string + commandNetworkQualityFlagSerial bool + commandNetworkQualityFlagMaxRuntime int + commandNetworkQualityFlagHTTP3 bool +) + +var commandNetworkQuality = &cobra.Command{ + Use: "networkquality", + Short: "Run a network quality test", + Run: func(cmd *cobra.Command, args []string) { + err := runNetworkQuality() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandNetworkQuality.Flags().StringVar( + &commandNetworkQualityFlagConfigURL, + "config-url", "", + "Network quality test config URL (default: Apple mensura)", + ) + commandNetworkQuality.Flags().BoolVar( + &commandNetworkQualityFlagSerial, + "serial", false, + "Run download and upload tests sequentially instead of in parallel", + ) + commandNetworkQuality.Flags().IntVar( + &commandNetworkQualityFlagMaxRuntime, + "max-runtime", int(networkquality.DefaultMaxRuntime/time.Second), + "Network quality maximum runtime in seconds", + ) + commandNetworkQuality.Flags().BoolVar( + &commandNetworkQualityFlagHTTP3, + "http3", false, + "Use HTTP/3 (QUIC) for measurement traffic", + ) + commandTools.AddCommand(commandNetworkQuality) +} + +func runNetworkQuality() error { + instance, err := createPreStartedClient() + if err != nil { + return err + } + defer instance.Close() + + dialer, err := createDialer(instance, commandToolsFlagOutbound) + if err != nil { + return err + } + + httpClient := networkquality.NewHTTPClient(dialer) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====") + + result, err := networkquality.Run(networkquality.Options{ + ConfigURL: commandNetworkQualityFlagConfigURL, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: commandNetworkQualityFlagSerial, + MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second, + Context: globalCtx, + OnProgress: func(p networkquality.Progress) { + if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle { + fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d", + networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM, + networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM) + return + } + switch networkquality.Phase(p.Phase) { + case networkquality.PhaseIdle: + if p.IdleLatencyMs > 0 { + fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs) + } else { + fmt.Fprint(os.Stderr, "\rMeasuring idle latency...") + } + case networkquality.PhaseDownload: + fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d", + networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM) + case networkquality.PhaseUpload: + fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d", + networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM) + } + }, + }) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, strings.Repeat("-", 40)) + fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs) + fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy) + fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy) + fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy) + fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy) + return nil +} diff --git a/cmd/sing-box/cmd_tools_stun.go b/cmd/sing-box/cmd_tools_stun.go new file mode 100644 index 00000000..f13086ca --- /dev/null +++ b/cmd/sing-box/cmd_tools_stun.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "os" + + "github.com/sagernet/sing-box/common/stun" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandSTUNFlagServer string + +var commandSTUN = &cobra.Command{ + Use: "stun", + Short: "Run a STUN test", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + err := runSTUN() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address") + commandTools.AddCommand(commandSTUN) +} + +func runSTUN() error { + instance, err := createPreStartedClient() + if err != nil { + return err + } + defer instance.Close() + + dialer, err := createDialer(instance, commandToolsFlagOutbound) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr, "==== STUN TEST ====") + + result, err := stun.Run(stun.Options{ + Server: commandSTUNFlagServer, + Dialer: dialer, + Context: globalCtx, + OnProgress: func(p stun.Progress) { + switch p.Phase { + case stun.PhaseBinding: + if p.ExternalAddr != "" { + fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs) + } else { + fmt.Fprint(os.Stderr, "\rSending binding request...") + } + case stun.PhaseNATMapping: + fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...") + case stun.PhaseNATFiltering: + fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...") + } + }, + }) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr) + fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr) + fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs) + if result.NATTypeSupported { + fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping) + fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering) + } else { + fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server") + } + return nil +} diff --git a/cmd/sing-box/cmd_tools_synctime.go b/cmd/sing-box/cmd_tools_synctime.go new file mode 100644 index 00000000..09d487ef --- /dev/null +++ b/cmd/sing-box/cmd_tools_synctime.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "os" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + + "github.com/spf13/cobra" +) + +var ( + commandSyncTimeFlagServer string + commandSyncTimeOutputFormat string + commandSyncTimeWrite bool +) + +var commandSyncTime = &cobra.Command{ + Use: "synctime", + Short: "Sync time using the NTP protocol", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + err := syncTime() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandSyncTime.Flags().StringVarP(&commandSyncTimeFlagServer, "server", "s", "time.apple.com", "Set NTP server") + commandSyncTime.Flags().StringVarP(&commandSyncTimeOutputFormat, "format", "f", C.TimeLayout, "Set output format") + commandSyncTime.Flags().BoolVarP(&commandSyncTimeWrite, "write", "w", false, "Write time to system") + commandTools.AddCommand(commandSyncTime) +} + +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(), dialer, serverAddress) + if err != nil { + return err + } + if commandSyncTimeWrite { + err = ntp.SetSystemTime(response.Time) + if err != nil { + return E.Cause(err, "write time to system") + } + } + os.Stdout.WriteString(response.Time.Local().Format(commandSyncTimeOutputFormat)) + return nil +} diff --git a/cmd/sing-box/cmd_version.go b/cmd/sing-box/cmd_version.go new file mode 100644 index 00000000..ab5ada2e --- /dev/null +++ b/cmd/sing-box/cmd_version.go @@ -0,0 +1,64 @@ +package main + +import ( + "os" + "runtime" + "runtime/debug" + + C "github.com/sagernet/sing-box/constant" + + "github.com/spf13/cobra" +) + +var commandVersion = &cobra.Command{ + Use: "version", + Short: "Print current version of sing-box", + Run: printVersion, + Args: cobra.NoArgs, +} + +var nameOnly bool + +func init() { + commandVersion.Flags().BoolVarP(&nameOnly, "name", "n", false, "print version name only") + mainCommand.AddCommand(commandVersion) +} + +func printVersion(cmd *cobra.Command, args []string) { + if nameOnly { + os.Stdout.WriteString(C.Version + "\n") + return + } + version := "sing-box version " + C.Version + "\n\n" + version += "Environment: " + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n" + + var tags string + var revision string + + debugInfo, loaded := debug.ReadBuildInfo() + if loaded { + for _, setting := range debugInfo.Settings { + switch setting.Key { + case "-tags": + tags = setting.Value + case "vcs.revision": + revision = setting.Value + } + } + } + + if tags != "" { + version += "Tags: " + tags + "\n" + } + if revision != "" { + version += "Revision: " + revision + "\n" + } + + if C.CGO_ENABLED { + version += "CGO: enabled\n" + } else { + version += "CGO: disabled\n" + } + + os.Stdout.WriteString(version) +} diff --git a/cmd/sing-box/generate_completions.go b/cmd/sing-box/generate_completions.go new file mode 100644 index 00000000..6ab0cade --- /dev/null +++ b/cmd/sing-box/generate_completions.go @@ -0,0 +1,28 @@ +//go:build generate && generate_completions + +package main + +import "github.com/sagernet/sing-box/log" + +func main() { + err := generateCompletions() + if err != nil { + log.Fatal(err) + } +} + +func generateCompletions() error { + err := mainCommand.GenBashCompletionFile("release/completions/sing-box.bash") + if err != nil { + return err + } + err = mainCommand.GenFishCompletionFile("release/completions/sing-box.fish", true) + if err != nil { + return err + } + err = mainCommand.GenZshCompletionFile("release/completions/sing-box.zsh") + if err != nil { + return err + } + return nil +} diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go new file mode 100644 index 00000000..fd55c7d3 --- /dev/null +++ b/cmd/sing-box/main.go @@ -0,0 +1,11 @@ +//go:build !generate + +package main + +import "github.com/sagernet/sing-box/log" + +func main() { + if err := mainCommand.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/common/badtls/raw_conn.go b/common/badtls/raw_conn.go new file mode 100644 index 00000000..774e39a5 --- /dev/null +++ b/common/badtls/raw_conn.go @@ -0,0 +1,176 @@ +//go:build go1.25 && badlinkname + +package badtls + +import ( + "bytes" + "os" + "reflect" + "sync/atomic" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/tls" +) + +type RawConn struct { + pointer unsafe.Pointer + methods *Methods + + IsClient *bool + IsHandshakeComplete *atomic.Bool + Vers *uint16 + CipherSuite *uint16 + + RawInput *bytes.Buffer + Input *bytes.Reader + Hand *bytes.Buffer + + CloseNotifySent *bool + CloseNotifyErr *error + + In *RawHalfConn + Out *RawHalfConn + + BytesSent *int64 + PacketsSent *int64 + + ActiveCall *atomic.Int32 + Tmp *[16]byte +} + +func NewRawConn(rawTLSConn tls.Conn) (*RawConn, error) { + var ( + pointer unsafe.Pointer + methods *Methods + loaded bool + ) + for _, tlsCreator := range methodRegistry { + pointer, methods, loaded = tlsCreator(rawTLSConn) + if loaded { + break + } + } + if !loaded { + return nil, os.ErrInvalid + } + + conn := &RawConn{ + pointer: pointer, + methods: methods, + } + + rawConn := reflect.Indirect(reflect.ValueOf(rawTLSConn)) + + rawIsClient := rawConn.FieldByName("isClient") + if !rawIsClient.IsValid() || rawIsClient.Kind() != reflect.Bool { + return nil, E.New("invalid Conn.isClient") + } + conn.IsClient = (*bool)(unsafe.Pointer(rawIsClient.UnsafeAddr())) + + rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete") + if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.isHandshakeComplete") + } + conn.IsHandshakeComplete = (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr())) + + rawVers := rawConn.FieldByName("vers") + if !rawVers.IsValid() || rawVers.Kind() != reflect.Uint16 { + return nil, E.New("invalid Conn.vers") + } + conn.Vers = (*uint16)(unsafe.Pointer(rawVers.UnsafeAddr())) + + rawCipherSuite := rawConn.FieldByName("cipherSuite") + if !rawCipherSuite.IsValid() || rawCipherSuite.Kind() != reflect.Uint16 { + return nil, E.New("invalid Conn.cipherSuite") + } + conn.CipherSuite = (*uint16)(unsafe.Pointer(rawCipherSuite.UnsafeAddr())) + + rawRawInput := rawConn.FieldByName("rawInput") + if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.rawInput") + } + conn.RawInput = (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr())) + + rawInput := rawConn.FieldByName("input") + if !rawInput.IsValid() || rawInput.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.input") + } + conn.Input = (*bytes.Reader)(unsafe.Pointer(rawInput.UnsafeAddr())) + + rawHand := rawConn.FieldByName("hand") + if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.hand") + } + conn.Hand = (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr())) + + rawCloseNotifySent := rawConn.FieldByName("closeNotifySent") + if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool { + return nil, E.New("invalid Conn.closeNotifySent") + } + conn.CloseNotifySent = (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr())) + + rawCloseNotifyErr := rawConn.FieldByName("closeNotifyErr") + if !rawCloseNotifyErr.IsValid() || rawCloseNotifyErr.Kind() != reflect.Interface { + return nil, E.New("invalid Conn.closeNotifyErr") + } + conn.CloseNotifyErr = (*error)(unsafe.Pointer(rawCloseNotifyErr.UnsafeAddr())) + + rawIn := rawConn.FieldByName("in") + if !rawIn.IsValid() || rawIn.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.in") + } + halfIn, err := NewRawHalfConn(rawIn, methods) + if err != nil { + return nil, E.Cause(err, "invalid Conn.in") + } + conn.In = halfIn + + rawOut := rawConn.FieldByName("out") + if !rawOut.IsValid() || rawOut.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.out") + } + halfOut, err := NewRawHalfConn(rawOut, methods) + if err != nil { + return nil, E.Cause(err, "invalid Conn.out") + } + conn.Out = halfOut + + rawBytesSent := rawConn.FieldByName("bytesSent") + if !rawBytesSent.IsValid() || rawBytesSent.Kind() != reflect.Int64 { + return nil, E.New("invalid Conn.bytesSent") + } + conn.BytesSent = (*int64)(unsafe.Pointer(rawBytesSent.UnsafeAddr())) + + rawPacketsSent := rawConn.FieldByName("packetsSent") + if !rawPacketsSent.IsValid() || rawPacketsSent.Kind() != reflect.Int64 { + return nil, E.New("invalid Conn.packetsSent") + } + conn.PacketsSent = (*int64)(unsafe.Pointer(rawPacketsSent.UnsafeAddr())) + + rawActiveCall := rawConn.FieldByName("activeCall") + if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct { + return nil, E.New("invalid Conn.activeCall") + } + conn.ActiveCall = (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr())) + + rawTmp := rawConn.FieldByName("tmp") + if !rawTmp.IsValid() || rawTmp.Kind() != reflect.Array || rawTmp.Len() != 16 || rawTmp.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("invalid Conn.tmp") + } + conn.Tmp = (*[16]byte)(unsafe.Pointer(rawTmp.UnsafeAddr())) + + return conn, nil +} + +func (c *RawConn) ReadRecord() error { + return c.methods.readRecord(c.pointer) +} + +func (c *RawConn) HandlePostHandshakeMessage() error { + return c.methods.handlePostHandshakeMessage(c.pointer) +} + +func (c *RawConn) WriteRecordLocked(typ uint16, data []byte) (int, error) { + return c.methods.writeRecordLocked(c.pointer, typ, data) +} diff --git a/common/badtls/raw_half_conn.go b/common/badtls/raw_half_conn.go new file mode 100644 index 00000000..4d2c8b64 --- /dev/null +++ b/common/badtls/raw_half_conn.go @@ -0,0 +1,121 @@ +//go:build go1.25 && badlinkname + +package badtls + +import ( + "hash" + "reflect" + "sync" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" +) + +type RawHalfConn struct { + pointer unsafe.Pointer + methods *Methods + *sync.Mutex + Err *error + Version *uint16 + Cipher *any + Seq *[8]byte + ScratchBuf *[13]byte + TrafficSecret *[]byte + Mac *hash.Hash + RawKey *[]byte + RawIV *[]byte + RawMac *[]byte +} + +func NewRawHalfConn(rawHalfConn reflect.Value, methods *Methods) (*RawHalfConn, error) { + halfConn := &RawHalfConn{ + pointer: (unsafe.Pointer)(rawHalfConn.UnsafeAddr()), + methods: methods, + } + + rawMutex := rawHalfConn.FieldByName("Mutex") + if !rawMutex.IsValid() || rawMutex.Kind() != reflect.Struct { + return nil, E.New("badtls: invalid halfConn.Mutex") + } + halfConn.Mutex = (*sync.Mutex)(unsafe.Pointer(rawMutex.UnsafeAddr())) + + rawErr := rawHalfConn.FieldByName("err") + if !rawErr.IsValid() || rawErr.Kind() != reflect.Interface { + return nil, E.New("badtls: invalid halfConn.err") + } + halfConn.Err = (*error)(unsafe.Pointer(rawErr.UnsafeAddr())) + + rawVersion := rawHalfConn.FieldByName("version") + if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 { + return nil, E.New("badtls: invalid halfConn.version") + } + halfConn.Version = (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr())) + + rawCipher := rawHalfConn.FieldByName("cipher") + if !rawCipher.IsValid() || rawCipher.Kind() != reflect.Interface { + return nil, E.New("badtls: invalid halfConn.cipher") + } + halfConn.Cipher = (*any)(unsafe.Pointer(rawCipher.UnsafeAddr())) + + rawSeq := rawHalfConn.FieldByName("seq") + if !rawSeq.IsValid() || rawSeq.Kind() != reflect.Array || rawSeq.Len() != 8 || rawSeq.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("badtls: invalid halfConn.seq") + } + halfConn.Seq = (*[8]byte)(unsafe.Pointer(rawSeq.UnsafeAddr())) + + rawScratchBuf := rawHalfConn.FieldByName("scratchBuf") + if !rawScratchBuf.IsValid() || rawScratchBuf.Kind() != reflect.Array || rawScratchBuf.Len() != 13 || rawScratchBuf.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("badtls: invalid halfConn.scratchBuf") + } + halfConn.ScratchBuf = (*[13]byte)(unsafe.Pointer(rawScratchBuf.UnsafeAddr())) + + rawTrafficSecret := rawHalfConn.FieldByName("trafficSecret") + if !rawTrafficSecret.IsValid() || rawTrafficSecret.Kind() != reflect.Slice || rawTrafficSecret.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("badtls: invalid halfConn.trafficSecret") + } + halfConn.TrafficSecret = (*[]byte)(unsafe.Pointer(rawTrafficSecret.UnsafeAddr())) + + rawMac := rawHalfConn.FieldByName("mac") + if !rawMac.IsValid() || rawMac.Kind() != reflect.Interface { + return nil, E.New("badtls: invalid halfConn.mac") + } + halfConn.Mac = (*hash.Hash)(unsafe.Pointer(rawMac.UnsafeAddr())) + + rawKey := rawHalfConn.FieldByName("rawKey") + if rawKey.IsValid() { + if /*!rawKey.IsValid() || */ rawKey.Kind() != reflect.Slice || rawKey.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("badtls: invalid halfConn.rawKey") + } + halfConn.RawKey = (*[]byte)(unsafe.Pointer(rawKey.UnsafeAddr())) + + rawIV := rawHalfConn.FieldByName("rawIV") + if !rawIV.IsValid() || rawIV.Kind() != reflect.Slice || rawIV.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("badtls: invalid halfConn.rawIV") + } + halfConn.RawIV = (*[]byte)(unsafe.Pointer(rawIV.UnsafeAddr())) + + rawMAC := rawHalfConn.FieldByName("rawMac") + if !rawMAC.IsValid() || rawMAC.Kind() != reflect.Slice || rawMAC.Type().Elem().Kind() != reflect.Uint8 { + return nil, E.New("badtls: invalid halfConn.rawMac") + } + halfConn.RawMac = (*[]byte)(unsafe.Pointer(rawMAC.UnsafeAddr())) + } + + return halfConn, nil +} + +func (hc *RawHalfConn) Decrypt(record []byte) ([]byte, uint8, error) { + return hc.methods.decrypt(hc.pointer, record) +} + +func (hc *RawHalfConn) SetErrorLocked(err error) error { + return hc.methods.setErrorLocked(hc.pointer, err) +} + +func (hc *RawHalfConn) SetTrafficSecret(suite unsafe.Pointer, level int, secret []byte) { + hc.methods.setTrafficSecret(hc.pointer, suite, level, secret) +} + +func (hc *RawHalfConn) ExplicitNonceLen() int { + return hc.methods.explicitNonceLen(hc.pointer) +} diff --git a/common/badtls/read_wait.go b/common/badtls/read_wait.go new file mode 100644 index 00000000..8448b1a2 --- /dev/null +++ b/common/badtls/read_wait.go @@ -0,0 +1,82 @@ +//go:build go1.25 && badlinkname + +package badtls + +import ( + "github.com/sagernet/sing/common/buf" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/tls" +) + +var _ N.ReadWaiter = (*ReadWaitConn)(nil) + +type ReadWaitConn struct { + tls.Conn + rawConn *RawConn + readWaitOptions N.ReadWaitOptions +} + +func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { + if _, isReadWaitConn := conn.(N.ReadWaiter); isReadWaitConn { + return conn, nil + } + rawConn, err := NewRawConn(conn) + if err != nil { + return nil, err + } + return &ReadWaitConn{ + Conn: conn, + rawConn: rawConn, + }, nil +} + +func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { + //err = c.HandshakeContext(context.Background()) + //if err != nil { + // return + //} + c.rawConn.In.Lock() + defer c.rawConn.In.Unlock() + for c.rawConn.Input.Len() == 0 { + err = c.rawConn.ReadRecord() + if err != nil { + return + } + for c.rawConn.Hand.Len() > 0 { + err = c.rawConn.HandlePostHandshakeMessage() + if err != nil { + return + } + } + } + buffer = c.readWaitOptions.NewBuffer() + n, err := c.rawConn.Input.Read(buffer.FreeBytes()) + if err != nil { + buffer.Release() + return + } + buffer.Truncate(n) + + if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 && + // recordType(c.RawInput.Bytes()[0]) == recordTypeAlert { + c.rawConn.RawInput.Bytes()[0] == 21 { + _ = c.rawConn.ReadRecord() + // return n, err // will be io.EOF on closeNotify + } + + c.readWaitOptions.PostReturn(buffer) + return +} + +func (c *ReadWaitConn) Upstream() any { + return c.Conn +} + +func (c *ReadWaitConn) ReaderReplaceable() bool { + return true +} diff --git a/common/badtls/read_wait_stub.go b/common/badtls/read_wait_stub.go new file mode 100644 index 00000000..9258a46e --- /dev/null +++ b/common/badtls/read_wait_stub.go @@ -0,0 +1,13 @@ +//go:build !go1.25 || !badlinkname + +package badtls + +import ( + "os" + + "github.com/sagernet/sing/common/tls" +) + +func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { + return nil, os.ErrInvalid +} diff --git a/common/badtls/registry.go b/common/badtls/registry.go new file mode 100644 index 00000000..34cfe9ec --- /dev/null +++ b/common/badtls/registry.go @@ -0,0 +1,62 @@ +//go:build go1.25 && badlinkname + +package badtls + +import ( + "crypto/tls" + "net" + "unsafe" +) + +type Methods struct { + readRecord func(c unsafe.Pointer) error + handlePostHandshakeMessage func(c unsafe.Pointer) error + writeRecordLocked func(c unsafe.Pointer, typ uint16, data []byte) (int, error) + + setErrorLocked func(hc unsafe.Pointer, err error) error + decrypt func(hc unsafe.Pointer, record []byte) ([]byte, uint8, error) + setTrafficSecret func(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte) + explicitNonceLen func(hc unsafe.Pointer) int +} + +var methodRegistry []func(conn net.Conn) (unsafe.Pointer, *Methods, bool) + +func init() { + methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) { + tlsConn, loaded := conn.(*tls.Conn) + if !loaded { + return nil, nil, false + } + return unsafe.Pointer(tlsConn), &Methods{ + readRecord: stdTLSReadRecord, + handlePostHandshakeMessage: stdTLSHandlePostHandshakeMessage, + writeRecordLocked: stdWriteRecordLocked, + + setErrorLocked: stdSetErrorLocked, + decrypt: stdDecrypt, + setTrafficSecret: stdSetTrafficSecret, + explicitNonceLen: stdExplicitNonceLen, + }, true + }) +} + +//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord +func stdTLSReadRecord(c unsafe.Pointer) error + +//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage +func stdTLSHandlePostHandshakeMessage(c unsafe.Pointer) error + +//go:linkname stdWriteRecordLocked crypto/tls.(*Conn).writeRecordLocked +func stdWriteRecordLocked(c unsafe.Pointer, typ uint16, data []byte) (int, error) + +//go:linkname stdSetErrorLocked crypto/tls.(*halfConn).setErrorLocked +func stdSetErrorLocked(hc unsafe.Pointer, err error) error + +//go:linkname stdDecrypt crypto/tls.(*halfConn).decrypt +func stdDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error) + +//go:linkname stdSetTrafficSecret crypto/tls.(*halfConn).setTrafficSecret +func stdSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte) + +//go:linkname stdExplicitNonceLen crypto/tls.(*halfConn).explicitNonceLen +func stdExplicitNonceLen(hc unsafe.Pointer) int diff --git a/common/badtls/registry_utls.go b/common/badtls/registry_utls.go new file mode 100644 index 00000000..330f64f5 --- /dev/null +++ b/common/badtls/registry_utls.go @@ -0,0 +1,56 @@ +//go:build go1.25 && badlinkname + +package badtls + +import ( + "net" + "unsafe" + + N "github.com/sagernet/sing/common/network" + + "github.com/metacubex/utls" +) + +func init() { + methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) { + var pointer unsafe.Pointer + if uConn, loaded := N.CastReader[*tls.Conn](conn); loaded { + pointer = unsafe.Pointer(uConn) + } else if uConn, loaded := N.CastReader[*tls.UConn](conn); loaded { + pointer = unsafe.Pointer(uConn.Conn) + } else { + return nil, nil, false + } + return pointer, &Methods{ + readRecord: utlsReadRecord, + handlePostHandshakeMessage: utlsHandlePostHandshakeMessage, + writeRecordLocked: utlsWriteRecordLocked, + + setErrorLocked: utlsSetErrorLocked, + decrypt: utlsDecrypt, + setTrafficSecret: utlsSetTrafficSecret, + explicitNonceLen: utlsExplicitNonceLen, + }, true + }) +} + +//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord +func utlsReadRecord(c unsafe.Pointer) error + +//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage +func utlsHandlePostHandshakeMessage(c unsafe.Pointer) error + +//go:linkname utlsWriteRecordLocked github.com/metacubex/utls.(*Conn).writeRecordLocked +func utlsWriteRecordLocked(hc unsafe.Pointer, typ uint16, data []byte) (int, error) + +//go:linkname utlsSetErrorLocked github.com/metacubex/utls.(*halfConn).setErrorLocked +func utlsSetErrorLocked(hc unsafe.Pointer, err error) error + +//go:linkname utlsDecrypt github.com/metacubex/utls.(*halfConn).decrypt +func utlsDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error) + +//go:linkname utlsSetTrafficSecret github.com/metacubex/utls.(*halfConn).setTrafficSecret +func utlsSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte) + +//go:linkname utlsExplicitNonceLen github.com/metacubex/utls.(*halfConn).explicitNonceLen +func utlsExplicitNonceLen(hc unsafe.Pointer) int diff --git a/common/badversion/version.go b/common/badversion/version.go new file mode 100644 index 00000000..a8404297 --- /dev/null +++ b/common/badversion/version.go @@ -0,0 +1,152 @@ +package badversion + +import ( + "strconv" + "strings" + + F "github.com/sagernet/sing/common/format" + + "golang.org/x/mod/semver" +) + +type Version struct { + Major int + Minor int + Patch int + Commit string + PreReleaseIdentifier string + PreReleaseVersion int +} + +func (v Version) LessThan(anotherVersion Version) bool { + return !v.GreaterThanOrEqual(anotherVersion) +} + +func (v Version) LessThanOrEqual(anotherVersion Version) bool { + return v == anotherVersion || anotherVersion.GreaterThan(v) +} + +func (v Version) GreaterThanOrEqual(anotherVersion Version) bool { + return v == anotherVersion || v.GreaterThan(anotherVersion) +} + +func (v Version) GreaterThan(anotherVersion Version) bool { + if v.Major > anotherVersion.Major { + return true + } else if v.Major < anotherVersion.Major { + return false + } + if v.Minor > anotherVersion.Minor { + return true + } else if v.Minor < anotherVersion.Minor { + return false + } + if v.Patch > anotherVersion.Patch { + return true + } else if v.Patch < anotherVersion.Patch { + return false + } + if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" { + return true + } else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" { + return false + } + if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" { + if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier { + if v.PreReleaseVersion > anotherVersion.PreReleaseVersion { + return true + } else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion { + return false + } + } + preReleaseIdentifier := parsePreReleaseIdentifier(v.PreReleaseIdentifier) + anotherPreReleaseIdentifier := parsePreReleaseIdentifier(anotherVersion.PreReleaseIdentifier) + if preReleaseIdentifier < anotherPreReleaseIdentifier { + return true + } else if preReleaseIdentifier > anotherPreReleaseIdentifier { + return false + } + } + return false +} + +func parsePreReleaseIdentifier(identifier string) int { + if strings.HasPrefix(identifier, "rc") { + return 1 + } else if strings.HasPrefix(identifier, "beta") { + return 2 + } else if strings.HasPrefix(identifier, "alpha") { + return 3 + } + return 0 +} + +func (v Version) VersionString() string { + return F.ToString(v.Major, ".", v.Minor, ".", v.Patch) +} + +func (v Version) String() string { + version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch) + if v.PreReleaseIdentifier != "" { + version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion) + } + return version +} + +func (v Version) BadString() string { + version := F.ToString(v.Major, ".", v.Minor) + if v.Patch > 0 { + version = F.ToString(version, ".", v.Patch) + } + if v.PreReleaseIdentifier != "" { + version = F.ToString(version, "-", v.PreReleaseIdentifier) + if v.PreReleaseVersion > 0 { + version = F.ToString(version, v.PreReleaseVersion) + } + } + return version +} + +func IsValid(versionName string) bool { + return semver.IsValid("v" + versionName) +} + +func Parse(versionName string) (version Version) { + if strings.HasPrefix(versionName, "v") { + versionName = versionName[1:] + } + if strings.Contains(versionName, "-") { + parts := strings.Split(versionName, "-") + versionName = parts[0] + identifier := parts[1] + if strings.Contains(identifier, ".") { + identifierParts := strings.Split(identifier, ".") + version.PreReleaseIdentifier = identifierParts[0] + if len(identifierParts) >= 2 { + version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1]) + } + } else { + if strings.HasPrefix(identifier, "alpha") { + version.PreReleaseIdentifier = "alpha" + version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:]) + } else if strings.HasPrefix(identifier, "beta") { + version.PreReleaseIdentifier = "beta" + version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:]) + } else { + version.Commit = identifier + } + } + } + versionElements := strings.Split(versionName, ".") + versionLen := len(versionElements) + if versionLen >= 1 { + version.Major, _ = strconv.Atoi(versionElements[0]) + } + if versionLen >= 2 { + version.Minor, _ = strconv.Atoi(versionElements[1]) + } + if versionLen >= 3 { + version.Patch, _ = strconv.Atoi(versionElements[2]) + } + return +} diff --git a/common/badversion/version_json.go b/common/badversion/version_json.go new file mode 100644 index 00000000..7ec19663 --- /dev/null +++ b/common/badversion/version_json.go @@ -0,0 +1,17 @@ +package badversion + +import "github.com/sagernet/sing/common/json" + +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +func (v *Version) UnmarshalJSON(data []byte) error { + var version string + err := json.Unmarshal(data, &version) + if err != nil { + return err + } + *v = Parse(version) + return nil +} diff --git a/common/badversion/version_test.go b/common/badversion/version_test.go new file mode 100644 index 00000000..d6d5a73c --- /dev/null +++ b/common/badversion/version_test.go @@ -0,0 +1,18 @@ +package badversion + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCompareVersion(t *testing.T) { + t.Parallel() + require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String()) + require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString()) + require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3-beta1"))) + require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3.0-beta1"))) + require.True(t, Parse("1.3.0-beta1").GreaterThan(Parse("1.3.0-alpha1"))) + require.True(t, Parse("1.3.1").GreaterThan(Parse("1.3.0"))) + require.True(t, Parse("1.4").GreaterThan(Parse("1.3"))) +} diff --git a/common/certificate/chrome.go b/common/certificate/chrome.go new file mode 100644 index 00000000..8a361c61 --- /dev/null +++ b/common/certificate/chrome.go @@ -0,0 +1,2817 @@ +// Code generated by 'make update_certificates'. DO NOT EDIT. + +package certificate + +import "crypto/x509" + +var chromeIncluded *x509.CertPool + +func init() { + chromeIncluded = x509.NewCertPool() + + // CN=Actalis Authentication Root CA; O=Actalis S.p.A./03358520967; L=Milan; C=IT + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE-----`)) + + // CN=TunTrust Root CA; O=Agence Nationale de Certification Electronique; C=TN + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE-----`)) + + // CN=Amazon Root CA 4; O=Amazon; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE-----`)) + + // CN=Amazon Root CA 1; O=Amazon; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE-----`)) + + // CN=Amazon Root CA 2; O=Amazon; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE-----`)) + + // CN=Amazon Root CA 3; O=Amazon; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE-----`)) + + // CN=Certum Trusted Network CA; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE-----`)) + + // CN=Certum EC-384 CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE-----`)) + + // CN=Certum Trusted Root CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE-----`)) + + // CN=Certum Trusted Network CA 2; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE-----`)) + + // CN=Autoridad de Certificacion Firmaprofesional CIF A62634068; C=ES + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE-----`)) + + // CN=ANF Secure Server Root CA; OU=ANF CA Raiz; O=ANF Autoridad de Certificacion; C=ES; SerialNumber=G63287510 + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE-----`)) + + // CN=Buypass Class 2 Root CA; O=Buypass AS-983163327; C=NO + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE-----`)) + + // CN=Buypass Class 3 Root CA; O=Buypass AS-983163327; C=NO + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE-----`)) + + // CN=Certainly Root R1; O=Certainly; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE-----`)) + + // CN=Certainly Root E1; O=Certainly; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE-----`)) + + // CN=Certigna; O=Dhimyotis; C=FR + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE-----`)) + + // CN=Certigna Root CA; OU=0002 48146308100036; O=Dhimyotis; C=FR + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE-----`)) + + // OU=certSIGN ROOT CA; O=certSIGN; C=RO + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE-----`)) + + // OU=certSIGN ROOT CA G2; O=CERTSIGN SA; C=RO + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE-----`)) + + // CN=HiPKI Root CA - G1; O=Chunghwa Telecom Co., Ltd.; C=TW + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE-----`)) + + // OU=ePKI Root Certification Authority; O=Chunghwa Telecom Co., Ltd.; C=TW + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE-----`)) + + // CN=D-TRUST BR Root CA 1 2020; O=D-Trust GmbH; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE-----`)) + + // CN=D-TRUST EV Root CA 1 2020; O=D-Trust GmbH; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE-----`)) + + // CN=D-TRUST Root Class 3 CA 2 EV 2009; O=D-Trust GmbH; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE-----`)) + + // CN=D-TRUST Root Class 3 CA 2 2009; O=D-Trust GmbH; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE-----`)) + + // CN=T-TeleSec GlobalRoot Class 3; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE-----`)) + + // CN=T-TeleSec GlobalRoot Class 2; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE-----`)) + + // CN=DigiCert TLS RSA4096 Root G5; O=DigiCert, Inc.; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE-----`)) + + // CN=DigiCert TLS ECC P384 Root G5; O=DigiCert, Inc.; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----`)) + + // CN=DigiCert Assured ID Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE-----`)) + + // CN=DigiCert Assured ID Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE-----`)) + + // CN=DigiCert Assured ID Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE-----`)) + + // CN=DigiCert Global Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE-----`)) + + // CN=DigiCert Global Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE-----`)) + + // CN=DigiCert Global Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE-----`)) + + // CN=DigiCert High Assurance EV Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE-----`)) + + // CN=DigiCert Trusted Root G4; OU=www.digicert.com; O=DigiCert Inc; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE-----`)) + + // CN=QuoVadis Root CA 2; O=QuoVadis Limited; C=BM + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE-----`)) + + // CN=QuoVadis Root CA 2 G3; O=QuoVadis Limited; C=BM + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE-----`)) + + // CN=QuoVadis Root CA 3 G3; O=QuoVadis Limited; C=BM + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE-----`)) + + // CN=CA Disig Root R2; O=Disig a.s.; L=Bratislava; C=SK + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE-----`)) + + // CN=emSign ECC Root CA - G3; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE-----`)) + + // CN=emSign Root CA - G1; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE-----`)) + + // CN=AffirmTrust Commercial; O=AffirmTrust; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE-----`)) + + // CN=Atos TrustedRoot 2011; O=Atos; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE-----`)) + + // CN=Atos TrustedRoot Root CA ECC TLS 2021; O=Atos; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE-----`)) + + // CN=Atos TrustedRoot Root CA RSA TLS 2021; O=Atos; C=DE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE-----`)) + + // CN=GlobalSign; OU=GlobalSign Root CA - R6; O=GlobalSign + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE-----`)) + + // CN=GlobalSign Root E46; O=GlobalSign nv-sa; C=BE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE-----`)) + + // CN=GlobalSign Root R46; O=GlobalSign nv-sa; C=BE + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE-----`)) + + // CN=GlobalSign; OU=GlobalSign ECC Root CA - R5; O=GlobalSign + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE-----`)) + + // CN=GlobalSign; OU=GlobalSign Root CA - R3; O=GlobalSign + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE-----`)) + + // CN=Starfield Root Certificate Authority - G2; O=Starfield Technologies, Inc.; L=Scottsdale; ST=Arizona; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE-----`)) + + // CN=Go Daddy Root Certificate Authority - G2; O=GoDaddy.com, Inc.; L=Scottsdale; ST=Arizona; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE-----`)) + + // CN=GlobalSign; OU=GlobalSign ECC Root CA - R4; O=GlobalSign + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE-----`)) + + // CN=GTS Root R4; O=Google Trust Services LLC; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE-----`)) + + // CN=GTS Root R2; O=Google Trust Services LLC; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE-----`)) + + // CN=GTS Root R1; O=Google Trust Services LLC; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE-----`)) + + // CN=GTS Root R3; O=Google Trust Services LLC; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE-----`)) + + // CN=ACCVRAIZ1; OU=PKIACCV; O=ACCV; C=ES + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE-----`)) + + // OU=AC RAIZ FNMT-RCM; O=FNMT-RCM; C=ES + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE-----`)) + + // CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS; OU=Ceres; O=FNMT-RCM; C=ES; OrganizationIdentifier=VATES-Q2826004J + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE-----`)) + + // CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1; OU=Kamu Sertifikasyon Merkezi - Kamu SM; O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK; L=Gebze - Kocaeli; C=TR + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE-----`)) + + // CN=HARICA TLS RSA Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE-----`)) + + // CN=HARICA TLS ECC Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE-----`)) + + // CN=IdenTrust Commercial Root CA 1; O=IdenTrust; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE-----`)) + + // CN=ISRG Root X1; O=Internet Security Research Group; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE-----`)) + + // CN=ISRG Root X2; O=Internet Security Research Group; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE-----`)) + + // CN=Izenpe.com; O=IZENPE S.A.; C=ES + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE-----`)) + + // CN=SZAFIR ROOT CA2; O=Krajowa Izba Rozliczeniowa S.A.; C=PL + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE-----`)) + + // CN=e-Szigno Root CA 2017; O=Microsec Ltd.; L=Budapest; C=HU; OrganizationIdentifier=VATHU-23584497 + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE-----`)) + + // CN=Microsec e-Szigno Root CA 2009; O=Microsec Ltd.; L=Budapest; C=HU; EmailAddress=info@e-szigno.hu + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE-----`)) + + // CN=Microsoft ECC Root Certificate Authority 2017; O=Microsoft Corporation; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE-----`)) + + // CN=Microsoft RSA Root Certificate Authority 2017; O=Microsoft Corporation; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE-----`)) + + // CN=NAVER Global Root Certification Authority; O=NAVER BUSINESS PLATFORM Corp.; C=KR + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE-----`)) + + // CN=NetLock Arany (Class Gold) Főtanúsítvány; OU=Tanúsítványkiadók (Certification Services); O=NetLock Kft.; L=Budapest; C=HU + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE-----`)) + + // CN=OISTE WISeKey Global Root GC CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE-----`)) + + // CN=OISTE WISeKey Global Root GB CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE-----`)) + + // CN=Security Communication ECC RootCA1; O=SECOM Trust Systems CO.,LTD.; C=JP + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE-----`)) + + // OU=Security Communication RootCA2; O=SECOM Trust Systems CO.,LTD.; C=JP + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE-----`)) + + // CN=Entrust Root Certification Authority; OU=www.entrust.net/CPS is incorporated by reference, (c) 2006 Entrust, Inc.; O=Entrust, Inc.; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE-----`)) + + // CN=Sectigo Public Server Authentication Root E46; O=Sectigo Limited; C=GB + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE-----`)) + + // CN=COMODO ECC Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE-----`)) + + // CN=COMODO Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIID0DCCArigAwIBAgIQIKTEf93f4cdTYwcTiHdgEjANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMTAxMDEwMDAw +MDBaFw0zMDEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo0IwQDAdBgNVHQ4EFgQUC1jli8ZMFTekQKkwqSG+RzZaVv8w +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBAC/JxBwHO89hAgCx2SFRdXIDMLDEFh9sAIsQrK/xR9SuEDwMGvjUk2ysEDd8 +t6aDZK3N3w6HM503sMZ7OHKx8xoOo/lVem0DZgMXlUrxsXrfViEGQo+x06iF3u6X +HWLrp+cxEmbDD6ZLLkGC9/3JG6gbr+48zuOcrigHoSybJMIPIyaDMouGDx8rEkYl +Fo92kANr3ryqImhrjKGsKxE5pttwwn1y6TPn/CbxdFqR5p2ErPioBhlG5qfpqjQi +pKGfeq23sqSaM4hxAjwu1nqyH6LKwN0vEJT9s4yEIHlG1QXUEOTS22RPuFvuG8Ug +R1uUq27UlTMdphVx8fiUylQ5PsE= +-----END CERTIFICATE-----`)) + + // CN=COMODO RSA Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE-----`)) + + // CN=USERTrust RSA Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE-----`)) + + // CN=USERTrust ECC Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE-----`)) + + // CN=Sectigo Public Server Authentication Root R46; O=Sectigo Limited; C=GB + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE-----`)) + + // CN=Entrust Root Certification Authority - G2; OU=See www.entrust.net/legal-terms, (c) 2009 Entrust, Inc. - for authorized use only; O=Entrust, Inc.; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE-----`)) + + // CN=Entrust Root Certification Authority - EC1; OU=See www.entrust.net/legal-terms, (c) 2012 Entrust, Inc. - for authorized use only; O=Entrust, Inc.; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE-----`)) + + // CN=SSL.com Root Certification Authority RSA; O=SSL Corporation; L=Houston; ST=Texas; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE-----`)) + + // CN=SSL.com TLS ECC Root CA 2022; O=SSL Corporation; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE-----`)) + + // CN=SSL.com TLS RSA Root CA 2022; O=SSL Corporation; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE-----`)) + + // CN=SSL.com Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE-----`)) + + // CN=SSL.com EV Root Certification Authority RSA R2; O=SSL Corporation; L=Houston; ST=Texas; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE-----`)) + + // CN=SSL.com EV Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE-----`)) + + // CN=SwissSign Gold CA - G2; O=SwissSign AG; C=CH + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE-----`)) + + // CN=TWCA CYBER Root CA; OU=Root CA; O=TAIWAN-CA; C=TW + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE-----`)) + + // CN=TWCA Global Root CA; OU=Root CA; O=TAIWAN-CA; C=TW + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE-----`)) + + // CN=TeliaSonera Root CA v1; O=TeliaSonera + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE-----`)) + + // CN=Telia Root CA v2; O=Telia Finland Oyj; C=FI + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE-----`)) + + // CN=Trustwave Global ECC P384 Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE-----`)) + + // CN=Trustwave Global ECC P256 Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE-----`)) + + // CN=SecureTrust CA; O=SecureTrust Corporation; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE-----`)) + + // CN=Trustwave Global Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US + chromeIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE-----`)) +} diff --git a/common/certificate/mozilla.go b/common/certificate/mozilla.go new file mode 100644 index 00000000..a5db7267 --- /dev/null +++ b/common/certificate/mozilla.go @@ -0,0 +1,4593 @@ +// Code generated by 'make update_certificates'. DO NOT EDIT. + +package certificate + +import "crypto/x509" + +var mozillaIncluded *x509.CertPool + +func init() { + mozillaIncluded = x509.NewCertPool() + + // Actalis Authentication Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE-----`)) + + // TunTrust Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE-----`)) + + // Amazon Root CA 1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE-----`)) + + // Amazon Root CA 2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE-----`)) + + // Amazon Root CA 3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE-----`)) + + // Amazon Root CA 4 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE-----`)) + + // Starfield Services Root Certificate Authority - G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE-----`)) + + // Certum CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE-----`)) + + // Certum EC-384 CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE-----`)) + + // Certum Trusted Network CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE-----`)) + + // Certum Trusted Network CA 2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE-----`)) + + // Certum Trusted Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE-----`)) + + // Autoridad de Certificacion Firmaprofesional CIF A62634068 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE-----`)) + + // FIRMAPROFESIONAL CA ROOT-A WEB + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE-----`)) + + // ANF Secure Server Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE-----`)) + + // Buypass Class 2 Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE-----`)) + + // Buypass Class 3 Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE-----`)) + + // Certainly Root E1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE-----`)) + + // Certainly Root R1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE-----`)) + + // Certigna + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE-----`)) + + // Certigna Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE-----`)) + + // certSIGN ROOT CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE-----`)) + + // certSIGN ROOT CA G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE-----`)) + + // ePKI Root Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE-----`)) + + // HiPKI Root CA - G1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE-----`)) + + // SecureSign Root CA12 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE-----`)) + + // SecureSign Root CA14 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE-----`)) + + // SecureSign Root CA15 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE-----`)) + + // D-TRUST BR Root CA 1 2020 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE-----`)) + + // D-TRUST BR Root CA 2 2023 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE-----`)) + + // D-TRUST EV Root CA 1 2020 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE-----`)) + + // D-TRUST EV Root CA 2 2023 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE-----`)) + + // D-TRUST Root CA 3 2013 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEDjCCAvagAwIBAgIDD92sMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxHzAdBgNVBAMMFkQtVFJVU1QgUm9vdCBD +QSAzIDIwMTMwHhcNMTMwOTIwMDgyNTUxWhcNMjgwOTIwMDgyNTUxWjBFMQswCQYD +VQQGEwJERTEVMBMGA1UECgwMRC1UcnVzdCBHbWJIMR8wHQYDVQQDDBZELVRSVVNU +IFJvb3QgQ0EgMyAyMDEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xHtCkoIf7O1UmI4SwMoJ35NuOpNcG+QQd55OaYhs9uFp8vabomGxvQcgdJhl8Ywm +CM2oNcqANtFjbehEeoLDbF7eu+g20sRoNoyfMr2EIuDcwu4QRjltr5M5rofmw7wJ +ySxrZ1vZm3Z1TAvgu8XXvD558l++0ZBX+a72Zl8xv9Ntj6e6SvMjZbu376Ml1wrq +WLbviPr6ebJSWNXwrIyhUXQplapRO5AyA58ccnSQ3j3tYdLl4/1kR+W5t0qp9x+u +loYErC/jpIF3t1oW/9gPP/a3eMykr/pbPBJbqFKJcu+I89VEgYaVI5973bzZNO98 +lDyqwEHC451QGsDkGSL8swIDAQABo4IBBTCCAQEwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUP5DIfccVb/Mkj6nDL0uiDyGyL+cwDgYDVR0PAQH/BAQDAgEGMIG+ +BgNVHR8EgbYwgbMwdKByoHCGbmxkYXA6Ly9kaXJlY3RvcnkuZC10cnVzdC5uZXQv +Q049RC1UUlVTVCUyMFJvb3QlMjBDQSUyMDMlMjAyMDEzLE89RC1UcnVzdCUyMEdt +YkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MDugOaA3hjVodHRwOi8v +Y3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2FfM18yMDEzLmNybDAN +BgkqhkiG9w0BAQsFAAOCAQEADlkOWOR0SCNEzzQhtZwUGq2aS7eziG1cqRdw8Cqf +jXv5e4X6xznoEAiwNStfzwLS05zICx7uBVSuN5MECX1sj8J0vPgclL4xAUAt8yQg +t4RVLFzI9XRKEBmLo8ftNdYJSNMOwLo5qLBGArDbxohZwr78e7Erz35ih1WWzAFv +m2chlTWL+BD8cRu3SzdppjvW7IvuwbDzJcmPkn2h6sPKRL8mpXSSnON065102ctN +h9j8tGlsi6BDB2B4l+nZk3zCRrybN1Kj7Yo8E6l7U0tJmhEFLAtuVqwfLoJs4Gln +tQ5tLdnkwBXxP/oYcuEVbSdbLTAoK59ImmQrme/ydUlfXA== +-----END CERTIFICATE-----`)) + + // D-TRUST Root Class 3 CA 2 2009 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE-----`)) + + // D-TRUST Root Class 3 CA 2 EV 2009 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE-----`)) + + // D-Trust SBR Root CA 1 2022 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICXjCCAeOgAwIBAgIQUs/kjG2gSvc/gpcMgAmMlTAKBggqhkjOPQQDAzBJMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSMwIQYDVQQDExpELVRy +dXN0IFNCUiBSb290IENBIDEgMjAyMjAeFw0yMjA3MDYxMTMwMDBaFw0zNzA3MDYx +MTI5NTlaMEkxCzAJBgNVBAYTAkRFMRUwEwYDVQQKEwxELVRydXN0IEdtYkgxIzAh +BgNVBAMTGkQtVHJ1c3QgU0JSIFJvb3QgQ0EgMSAyMDIyMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEWZM59oxJZijXYQzIq38Moy3foqR8kito1S5+HkDLtGhJfxKhq39X +nxkuYy5b/mZxDDMPud5rxIjDse/sOUDjlqvb5XuuH9z5r0aaakYGL8c3ZIsXYv6W +w6LuhOCwlzm8o4GPMIGMMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFPEpox4B +Eh09dVZNx1B8xRmqDxi3MA4GA1UdDwEB/wQEAwIBBjBKBgNVHR8EQzBBMD+gPaA7 +hjlodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Nicl9yb290X2Nh +XzFfMjAyMi5jcmwwCgYIKoZIzj0EAwMDaQAwZgIxAJf53q5Lj5i1HkB/Mn1NVEPa +ic3CqpI80YIec8/6TJIg+2MnxfVzPQk996dhhozzagIxAOcvfLj1JYw7OR82q431 +hqIu4Xpk2mc5Av7+Mz/Zc7ZYWzr8sqTZYHh3zHmnpq5VvQ== +-----END CERTIFICATE-----`)) + + // D-Trust SBR Root CA 2 2022 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFrDCCA5SgAwIBAgIQVNWjlR49lbpyG5rQMSFKujANBgkqhkiG9w0BAQ0FADBJ +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSMwIQYDVQQDExpE +LVRydXN0IFNCUiBSb290IENBIDIgMjAyMjAeFw0yMjA3MDcwNzMwMDBaFw0zNzA3 +MDcwNzI5NTlaMEkxCzAJBgNVBAYTAkRFMRUwEwYDVQQKEwxELVRydXN0IEdtYkgx +IzAhBgNVBAMTGkQtVHJ1c3QgU0JSIFJvb3QgQ0EgMiAyMDIyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAryy8jjaM62SvUWrWbjxekTrqmsPKbPuqJ55k +IqlA37koRVrsU2EWKJjCiqR1eFCE3fogSJIHZUE1ZlESdGGdBwaFOTFXeyg/1Zyl +7FrpHEsnn84nBvM39VLYETMWQTof9WN4ZWOGyb/IAQQfbu7i7KwM7oKS4vYaDT85 ++Z1lk634uQXBPfg3gVbDoP4F7OCUFjojFgTapgqThXJtYTuhjUXW43++Fb02hAj2 +C4NrJqqiveCw56rgrmfE04KlDKmk8DN5DVA/8O+QPSS5f9IgbOqX87+c3EfeCWG9 +lHmVWgJ2NWDERyIN93ZjA9PG+4PGXaut7WklKwNbTSUAQeOMhxdSqOAFK0NNFBPK +5z9DIrw3pHXx9r867zIeru5YhpByugSsQEjvXMR4p6mPJ1rLeuxY8sIIWJBtTQOF +eXEVBQ5OPvnfDwX3XxRIViENM5KxrIzlGP6/D+7gBKq9IfJYtlyJCosYCSIaszXG +ZsL1MxWZgOAI+ZYvE4zu2reIxOk3tddq1zqETatwjNNOFFWgohD8ZNpn6PHLM93J +moqPli9Ygdn4mgBDzJD7VXb7huM3ASgMb/TpWU0Vd1FCSsw0uIBDUIHvV6UT26eU +eQ9Lyn4Xfa+jIWTocVVWjwawR+xZD11wWywWQvCGnnXea01ImITiVxi2nIKZZTqL +gHhXDEkCAwEAAaOBjzCBjDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRds4CU +G+WGv2i6FDSk9u5t8t3f5zAOBgNVHQ8BAf8EBAMCAQYwSgYDVR0fBEMwQTA/oD2g +O4Y5aHR0cDovL2NybC5kLXRydXN0Lm5ldC9jcmwvZC10cnVzdF9zYnJfcm9vdF9j +YV8yXzIwMjIuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA0VC5YGFbNSr2X0/V9K9yv +D1HhTbwhS5P0AEQTBxALJRg+SFmW96Hhk5B4Zho9I+siqwGmjgxRM+ZtjDHurKQB +cDlI3sdmLGsNy3Ofh5LpPkcfuO8v7rdWjEiJ8DinFTmy7sA/F6RzAgicvAaKpMK3 +YWH5w9vE0Hp8Yd6xWJH13WVMLwv46z217Yq+dxy6WQISZnHlmCfODj2vUaJF+YL7 +WqWUcPeLhMNMZSWbe+IfMHCzQI467r3052jFnckpR3EOk8i1SE71ZrsHiHFpa3tI +jm/wEcS0yXAUmCC97afqAdpupZsS/j5EMLPw63VSwPTD+ncmpHeCLW/zKB5OlfAw +94n4LKJQW/K+Mn5sVNtyySpa4By2C9hSmlmh47ABJ8WgFlBm3OuubfSbWz2EbVuH +56mJu2644JtTicD/LkAaiUQuGENnOOR8cl/ZoyklQUE9HHcbZKjDVe5jcWZig/R/ +JpmgVDuhEm1wYs7T+bi9IvzUmtS74jgWL7d9OcKwqQPpnM9+GI123F8Ru+tC7FAJ +PlzskDHYGnK6P2kH7pg0wjSk1toT1qmE8gCGwFS6HhGw4rnEB7SR56rmMVZvsUTE +KmK8ybBlnDT8DBpT3yEXu8JtoQrm8bCqRAlQSTh6XXHiMS4ZsN+VQgR9hIjOCiNn +azidFt4G/ihwOKVarvyD7Q== +-----END CERTIFICATE-----`)) + + // T-TeleSec GlobalRoot Class 2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE-----`)) + + // T-TeleSec GlobalRoot Class 3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE-----`)) + + // Telekom Security SMIME ECC Root 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICRzCCAc2gAwIBAgIQFSrdFMkY0aRWQIamJa8HXzAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMS0wKwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIEVDQyBSb290IDIw +MjEwHhcNMjEwMzE4MTEwODMwWhcNNDYwMzE3MjM1OTU5WjBlMQswCQYDVQQGEwJE +RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMS0wKwYD +VQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIEVDQyBSb290IDIwMjEwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAASwGY+ia7XHzQ8wmTcMw2Bb8fEnIFU9wJKLq1ehb3OD +IcJDEwxeiarHBTV5k2KQ1l0TH9F6oLyeEKdmfEYKsFdsv+ZUOTghbBJccczTWl9t +t6eG37Pf7sLniUGWNfYvSrWjQjBAMB0GA1UdDgQWBBQrywEMY8NTEqWoV6/QnIP7 +vZA6SzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD +AwNoADBlAjEA1rxIkodHA8dwOyW2H65GZ3N0ACdL5KUEogPfXiitbl4DyN1onLa/ +lBBIlS8P/xiLAjABQDOel5dNBfJ0VAzNOf1qawnBJD9hjjiht+jXRBURYv8OYTdH +S0B/Sl+yZ1pzdcI= +-----END CERTIFICATE-----`)) + + // Telekom Security SMIME RSA Root 2023 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgIQDH5i9XlzO51Djotj7ZGVuDANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMS0wKwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIFJTQSBSb290 +IDIwMjMwHhcNMjMwMzI4MTIwOTIyWhcNNDgwMzI3MjM1OTU5WjBlMQswCQYDVQQG +EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMS0w +KwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIFJTQSBSb290IDIwMjMwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDvxQ6LvjLSZ0f/Ckxnsyq/yMPF +keu1xx6R4WaoiItVIIAfUV53l54ZClzHazchfAM2AfSIJdmoLkGq/Ngm4JZAYnmu +V54DOBocsncUPumhctDk4DfRF0btUFx6WMX4K/d1L8+BnlostzqsoFmYBFEM/0nF +UP0e00eFSzNPoje1rwSaJzKdVtU/VWHji2+uUf6X/mkH+mJbJuYUeRWlEziuXze+ +lErWDYAWaaSRsjpJmHWdRhCKXHp/hKXorx7Hq7NaRrWjS/WmIzYARrHbBbYbzp56 +Mlya1XLDnYZNK4TTHrWI2hB4nCLDOyO16xMHvW9T7Jvsm9Nl9QcJ412nmbV+ho7V +Av+3hQnjRxTdlmYYNN4I1d/LGJliCyvsAF1SRNPGlvwyViWRz80ZO5U5PgKHmWO2 +1T40eg8RdYG8fQTKYLQoddcCUd1SAC7H/YnxXPPLpCcSOI+7+4nw5MQ4LL6CoHFh +YpGPSAwvK6mw8csQBOd0vzeQ708qQzWXEsYqcA3eLFVHeWMp9cofagZSHK4tJCKD +Iq/QqjC3Kh//ZSNYZZPIjn1AEDGGeNlVyzww8N5RKgA20idFX9jooSE9fkZWOylF +8R0FCc62QzDcRZAQMEyka4aLPz0vMZFx7ya59r6dsGzfEe5YP0N5hjmA8SYXB5jw +maowLENZFM7t4kAThQIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FJrOrCrsAfplcN6XnfHSAIylo2S7MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw +FoAUms6sKuwB+mVw3ped8dIAjKWjZLswDQYJKoZIhvcNAQEMBQADggIBAONQ/fVA +FiIJljoNqe+B5y4y8KHxSV57iA0Ecte+Z6i6He5Qu3JuetG7DHIwRsjV1wISFplO +Ht9alu6Pkb6uhvgQd6XEbkdhwPIm2U9haAVIdQgVpaF71biziXnm7fHzYQCGey4x +/qNc+Hk9tFuIe+Ajuw2hF/rLaA2Yd3EI4m1DdGvENsWUQaQA1lctmYqLIBIVAjIO +0knsgUjFaidS17JzVVOWPJ5PTLWg0E9X0GcoSGS+xri67GTPyHvFaucq5llXttbU +1sBnXNmeKAlAv/OpNTFlYAPLGWyClQMeXz/hvepJceVbtwtHFhsgiW2UmQx+iGwd +DfS3IRpZl6zL6L4XH5V8U5uvUFKqjQsur1rXYPIqaSq57lRwGKq99aE/0t2hYxkA ++KcM66N58nBZo/iiEgPsE//kAoY218HDpLXUpMI3RbaUcD3FveujFR3jNnoVaSpW +NDnPpZo2qsjtebzP9s4EUwvaslAjfLw+Jq3wDkO7JsuuwkDeNx8KoFHNY522T9jG +R3y82LTtnovzEeKotT7srnA+fiK7NUgXYGIUkTCjdj2mUTaLHw3dajEcpe3dlqNu +cg8TTaqnqVx4+QMSGJM3RRKJPfi+yr3ZvgzZGGSnyEE+dYIhOH1l9KDUE0sHeCn5 +nX7Mhz/E2i6I3eML3FpRWunZEk+eAtv3BSVR +-----END CERTIFICATE-----`)) + + // Telekom Security TLS ECC Root 2020 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE-----`)) + + // Telekom Security TLS RSA Root 2023 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE-----`)) + + // DigiCert Assured ID Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE-----`)) + + // DigiCert Assured ID Root G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE-----`)) + + // DigiCert Assured ID Root G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE-----`)) + + // DigiCert Global Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE-----`)) + + // DigiCert Global Root G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE-----`)) + + // DigiCert Global Root G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE-----`)) + + // DigiCert High Assurance EV Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE-----`)) + + // DigiCert SMIME ECC P384 Root G5 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICHDCCAaOgAwIBAgIQBT9uoAYBcn3tP8OjtqPW7zAKBggqhkjOPQQDAzBQMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKDAmBgNVBAMTH0Rp +Z2lDZXJ0IFNNSU1FIEVDQyBQMzg0IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBQMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xKDAmBgNVBAMTH0RpZ2lDZXJ0IFNNSU1FIEVDQyBQMzg0IFJvb3QgRzUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQWnVXlttT7+2drGtShqtJ3lT6I5QeftnBm +ICikiOxwNa+zMv83E0qevAED3oTBuMbmZUeJ8hNVv82lHghgf61/6GGSKc8JR14L +HMAfpL/yW7yY75lMzHBrtrrQKB2/vgSjQjBAMB0GA1UdDgQWBBRzemuW20IHi1Jm +wmQyF/7gZ5AurTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNnADBkAjA3RPUygONx6/Rtz3zMkZrDbnHY0iNdkk2CQm1cYZX2kfWn +CPZql+mclC2YcP0ztgkCMAc8L7lYgl4Po2Kok2fwIMNpvwMsO1CnO69BOMlSSJHW +Dvu8YDB8ZD8SHkV/UT70pg== +-----END CERTIFICATE-----`)) + + // DigiCert SMIME RSA4096 Root G5 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQBfa6BCODRst9XOa5W7ocVTANBgkqhkiG9w0BAQwFADBP +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJzAlBgNVBAMT +HkRpZ2lDZXJ0IFNNSU1FIFJTQTQwOTYgUm9vdCBHNTAeFw0yMTAxMTUwMDAwMDBa +Fw00NjAxMTQyMzU5NTlaME8xCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy +dCwgSW5jLjEnMCUGA1UEAxMeRGlnaUNlcnQgU01JTUUgUlNBNDA5NiBSb290IEc1 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4Gpb2fj5fey1e+9f3Vw0 +2Npd0ctldashfFsA1IJvRYVBiqkSAnIy8BT1A3W7Y5dJD0CZCxoeVqfS0OGr3eUE +G+MfFBICiPWggAn2J5pQ8LrjouCsahSRtWs4EHqiMeGRG7e58CtbyHcJdrdRxDYK +mVNURCW3CTWGFwVWkz1BtwLXYh+KkhGH6hFt6ggR3LF4SEmS9rRRgHgj2P7hVho6 +kBNWNInV4pWLX96yzPs/OLeF9+qevy6hLi9NfWoRLjag/xEIBJVV4Bs7Z5OplFXq +Mu0GOn/Cf+OtEyfRNEGzMMO/tIj4A4Kk3z6reHegWZNx593rAAR7zEg5KOAeoxVp +yDayoQuX31XW75GcpPYW91EK7gMjkdwE/+DdOPYiAwDCB3EaEsnXRiqUG83Wuxvu +v75NUFiwC80wdin1z+W2ai92sLBpatBtZRg1fpO8chfBVULNL8Ilu/T9HaFkIlRd +4p5yQYRucZbqRQe2XnpKhp1zZHc4A9IPU6VVIMRN/2hvVanq3XHkT9mFo3xOKQKe +CwnyGlPMAKbd0TT2DcEwsZwCZKw17aWwKbHSlTMP0iAzvewjS/IZ+dqYZOQsMR8u +4Y0cBJUoTYxYzUvlc4KGjOyo1nlc+2S73AxMKPYXr+Jo1haGmNv8AdwxuvicDvko +Rkrh/ZYGRXkRaBdlXIsmh1sCAwEAAaNCMEAwHQYDVR0OBBYEFNGj1FcdT1XbdUxc +Qp5jFs60xjsfMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBDAUAA4ICAQAHpwreU7ua63C/sjaQzeSnuPEM5F1aHXhl/Mm4HiMRV3xp +NW0B/1NQvwcOuscBP1gqlHUDqxwLI9wbih43PR1Yj3PZsypv3xCgWwynyrB/uSSi +ATUy5V5GQevYf3PnQumkUSZ3gQqo6w8KUJ1+iiBn/AuOOhHTxYxgGNlLsfzU8bRJ +Tq6H4dH7dqFf8wbPl5YM6Z51gVxTDSL8NuZJbnTbAIWNfCKgjvsQTNRiE1vvS3Im +i/xOio/+lxBTxXiLQmQbX+CJ/bsJf1DgVIUmEWodZflJKdx8Nt/7PffSrO4yjW6m +fTmcRcTKDfU7tHlTpS9Wx1HFikxkXZBDI45rTBd4zOi/9TvkqEjPrZsM3zJK09kS +jiN4DS2vn6+ePAnClwDtOmkccT8539OPxGb17zaUD/PdkraWX5Cm3XOqpiCUlCVq +CQxy5BMjYEyjyhcue2cA29DN6nofOSZXiTB3y07llUVPX/s2XD35ILU6ECVPkzJa +7sGW6OlWBLBJYU3seKidGMH/2OovVu+VK3sEXmfjVUDtOQT5C3n1aoxcD4makMfN +i97bJjWhbs2zQvKiDzsMjpP/FM/895P35EEIbhlSEQ9TGXN4DM/YhYH4rVXIsJ5G +Y6+cUu5cv/DAWzceCSDSPiPGoRVKDjZ+MMV5arwiiNkMUkAf3U4PZyYW0q0XHA== +-----END CERTIFICATE-----`)) + + // DigiCert TLS ECC P384 Root G5 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----`)) + + // DigiCert TLS RSA4096 Root G5 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE-----`)) + + // DigiCert Trusted Root G4 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE-----`)) + + // QuoVadis Root CA 1 G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE-----`)) + + // QuoVadis Root CA 2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE-----`)) + + // QuoVadis Root CA 2 G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE-----`)) + + // QuoVadis Root CA 3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE-----`)) + + // QuoVadis Root CA 3 G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE-----`)) + + // DIGITALSIGN GLOBAL ROOT ECDSA CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICajCCAfCgAwIBAgIUNi2PcoiiKCfkAP8kxi3k6/qdtuEwCgYIKoZIzj0EAwMw +ZDELMAkGA1UEBhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmljYWRv +cmEgRGlnaXRhbDEpMCcGA1UEAwwgRElHSVRBTFNJR04gR0xPQkFMIFJPT1QgRUNE +U0EgQ0EwHhcNMjEwMTIxMTEwNzUwWhcNNDYwMTE1MTEwNzUwWjBkMQswCQYDVQQG +EwJQVDEqMCgGA1UECgwhRGlnaXRhbFNpZ24gQ2VydGlmaWNhZG9yYSBEaWdpdGFs +MSkwJwYDVQQDDCBESUdJVEFMU0lHTiBHTE9CQUwgUk9PVCBFQ0RTQSBDQTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABG4Lo6szTRzqSuj8BI0UoH3wCCxfg6uT0dJ7utdJ +fY/sElBf1LnL5fD5M2MfyVfsQNgRC5foUhbMKY70BoYeONw9V8Tuqr3IVAQmWicT +UUc9Hx8ajqiVpDPQzEfMbbj8SKNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBTOr0qLGnXi8TjnAvAWrV7qZNV7tDAdBgNVHQ4EFgQUzq9Kixp14vE45wLw +Fq1e6mTVe7QwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMAqIxHGc +RANNjbTHvKiu2TAnNWprFmPX/OdZ4aeJG0wxmiNVRObzQyHVRydvbVcBqgIxAPuy +6uKXf1G1n0jrvG81iahkcKtXds3AxhRgyn/iggBz98w16o4km+UIWccEjHN4/g== +-----END CERTIFICATE-----`)) + + // DIGITALSIGN GLOBAL ROOT RSA CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIUXVnIyqsJV/XmtdoplARq/8XUlYcwDQYJKoZIhvcNAQEN +BQAwYjELMAkGA1UEBhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmlj +YWRvcmEgRGlnaXRhbDEnMCUGA1UEAwweRElHSVRBTFNJR04gR0xPQkFMIFJPT1Qg +UlNBIENBMB4XDTIxMDEyMTEwNTAzNFoXDTQ2MDExNTEwNTAzNFowYjELMAkGA1UE +BhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmljYWRvcmEgRGlnaXRh +bDEnMCUGA1UEAwweRElHSVRBTFNJR04gR0xPQkFMIFJPT1QgUlNBIENBMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyIe2ONMc8N4S+IPHxIriibi0Inp4 ++AxmUWh2NwrVT8JaCLgWXPdyAQk3hIEqVGvXktBs+qinQxI06w7bNw8p/ooxUULo +S5yQqMgsEdP9oCl+zt6U9oLgWLRORSXxIvI90w97VBrcMrbWUU5+QbRXuCzGuQ4u +ylfx1cjTWOel6UIRrtMgJZRp14/Kog3D058HaD8V0mcuU/12gpsLc6kpDZ4RkxQI +mOyeVBJKVqIGFexrbC6SYC6GDa6CH1FN47IH1xAZVyL2qWlEhPPZPaAGv8yIfn/1 +zlulwipqdELqb6b/+Wix0F+9kdJVbzNXTB6d5OKLwYVloOBqnAAAiJLdWAgW8nAx +qBzh3r1OcenWvn61oVrDTfe/m72UpP31qlOTRskmAQRwxKBxus4lZvuRflVw7kkK +TWJ/wlCacvIYZ53pRag0hOj4gfbRWiIeB087s3/dEaVz3L6pGTppqW0bMuKJqqUn +C1p+dOIPZDldfly5wRf8x41eyewk7dLyP3qERTcCvj5rWcTmWxZtwKqeqrVZLixw +VZzMmZaYJFTRjtrKtBG0t3BDH2+QCyCgqHYTZdvbI1p1S6ELMXcK7n1oYRoTjOpR +flxWo1dMXaHrE2W/VBTM8+7c1+w8l/J4Vrjfclxw/M4G3Z/SBzHv51KRns2618AY +RAcxZUkyaRNK648CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAW +gBS1Nrw8jBqrLPZZGS2DFNqTJRXWhjAdBgNVHQ4EFgQUtTa8PIwaqyz2WRktgxTa +kyUV1oYwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDQUAA4ICAQAU+zElODH4 +ygiyI3Y4rfjTWfXMtFcl4US+fvwW7K76Jp9PZxZKVvD97ccZATSOkFot1oBc7HHS +gSWCHgBx35rR1R0iu9Gl82IPtOvcJHP+plbNmhTFBDUWMaIH66UA4rb4X3L9P2FJ +jt5+TTjXeh50N2xR3L4ABLg4FPMgwe2bpyP9DUKEHX/yc8PQeGPxn+zXW+nxvmyg +SwOejWnhFNqIEIEjU//aVCsLxrmWlQQYRvN7qJfYW2ik5DgcDkXlmNMJrppe7LN5 +DTly8vSUnQ6eYCLmqPZMhc0HgjpoOc09X+M49LavO2tKn2BRRaJAAuWqDOM+0XjU +onScJroFmihwSj6mC9AdSfC6+K5BEH6kBxK9qM8pPVe7x/FDRwA+rnAYWiB7Ccs6 +OnCA5UxgmMEVwR1K98jwm+FyreddaFgLBLGMvJ+3+26LWwRV++sjVdd4UNoly74n +NrskGnkcUdH+E7v/eCzcpL4v9sVLU8+nTJlecKxZiASuZAS/e6Z6TdPod72hflAV +8+9JMIVNIVeq2yx1l62BAYeisXCdHgZaA2CxP6ZtgizUFLGBpeg9iB20cixYN4qO +OJS4c92p4Lj2d6KzfFjermk6tYulGrvy2HQGnP1icyAhdrF+cJ4Z1OsXYhk4mc02 +K0f+McvfueSsCNPYpuvUnn5LZKRVXSsXyQ== +-----END CERTIFICATE-----`)) + + // CA Disig Root R2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE-----`)) + + // GLOBALTRUST 2020 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG +A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw +FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx +MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u +aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b +RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z +YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 +QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw +yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ +BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ +SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH +r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 +4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me +dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw +q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 +nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu +H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC +XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd +6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf ++I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi +kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 +wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB +TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C +MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn +4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I +aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy +qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE-----`)) + + // emSign ECC Root CA - C3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE-----`)) + + // emSign ECC Root CA - G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE-----`)) + + // emSign Root CA - C1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE-----`)) + + // emSign Root CA - G1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE-----`)) + + // AffirmTrust Commercial + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE-----`)) + + // AffirmTrust Networking + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE-----`)) + + // AffirmTrust Premium + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE-----`)) + + // AffirmTrust Premium ECC + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE-----`)) + + // Atos TrustedRoot 2011 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE-----`)) + + // Atos TrustedRoot Root CA ECC G2 2020 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICMTCCAbagAwIBAgIMC3MoERh0MBzvbwiEMAoGCCqGSM49BAMDMEsxCzAJBgNV +BAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRSb290 +IFJvb3QgQ0EgRUNDIEcyIDIwMjAwHhcNMjAxMjE1MDgzOTEwWhcNNDAxMjEwMDgz +OTA5WjBLMQswCQYDVQQGEwJERTENMAsGA1UECgwEQXRvczEtMCsGA1UEAwwkQXRv +cyBUcnVzdGVkUm9vdCBSb290IENBIEVDQyBHMiAyMDIwMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEyFyAyk7CKB9XvzjmYSP80KlblhYWwwxeFaWQCf84KLR6HgrWUyrB +u5BAdDfpgeiNL2gBNXxSLtj0WLMRHFvZhxiTkS3sndpsnm2ESPzCiQXrmBMCAWxT +Hg5JY1hHsa/Co2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFFsfxHFs +shufvlwfjP2ztvuzDgmHMB0GA1UdDgQWBBRbH8RxbLIbn75cH4z9s7b7sw4JhzAO +BgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAOzgmf3d5FTByx/oPijX +FVlKgspTMOzrNqW5yM6TR1bIYabhbZJTlY/241VT8N165wIxALCH1RuzYPyRjYDK +ohtRSzhUy6oee9flRJUWLzxEeC4luuqQ5OxS7lfsA4TzXtsWDQ== +-----END CERTIFICATE-----`)) + + // Atos TrustedRoot Root CA ECC TLS 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE-----`)) + + // Atos TrustedRoot Root CA RSA G2 2020 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIMR7opRlU+FpKXsKtAMA0GCSqGSIb3DQEBDAUAMEsxCzAJ +BgNVBAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRS +b290IFJvb3QgQ0EgUlNBIEcyIDIwMjAwHhcNMjAxMjE1MDg0MTIzWhcNNDAxMjEw +MDg0MTIyWjBLMQswCQYDVQQGEwJERTENMAsGA1UECgwEQXRvczEtMCsGA1UEAwwk +QXRvcyBUcnVzdGVkUm9vdCBSb290IENBIFJTQSBHMiAyMDIwMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAljGFSqoPMv554UOHnPsjt45/DVS9x2KTd+Qc +NQR2owOLIu7EhN2lk25uso4JA+tRFjEXqmkVGA5ndCNe6pp9tTk+PYKpa+H+qRyw +rVpNTHiDQYvP8h1impgEnGPpq2X+SB0kZQdHPrmRLumdm38aNak0sLflcDPvSnJR +tge/YD8qn51U3/PXlElRA1pAqWjdEVlc+HamvFBSEO2s7JXg1INrSdoKT5mD3jKD +SINnlbJ+54GFPc2C98oC7W2IXQiNuDW/KmkwmbtL0UHbRaCTmVGBkDYIqoq26I+z +y+7lRg1ydfVJbOGify+87YSmN+7ewk85Tvae8MnRmzCdSW3h2v8SEIzW5Zl7BbZ9 +sAnHpPiyHDmVOTP0Nc4lYnuwXyDzy234bFIUZESP08ipdgflr3GZLS0EJUh2r8Pn +zEPyB7xKJCQ33fpulAlvTF4BtP5U7COWpV7dhv/pRirx6NzspT2vb6oOD7R1+j4I +uSZFT2aGTLwZuOHVNe6ChMjTqxLnzXMzYnf0F8u9NHYqBc6V5Xh5S56wjfk8WDiR +6l6HOMC3Qv2qTIcjrQQgsX52Qtq7tha6V8iOE/p11QhMrziRqu+P+p9JLlR8Clax +evrETi/Uo/oWitCV5Zem/8P8fA5HWPN/B3sS3Fc/LeOhTVtSTDOHmagJe2x+DvLP +VkKe6wUCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQgJfMH +/adv8ZbukRBpzJrvfchoeDAdBgNVHQ4EFgQUICXzB/2nb/GW7pEQacya733IaHgw +DgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQAkK06Y8h0X7dl2JrYw +M+hpRaFRS1LYejowtuQS6r+fTOAEpPY1xv6hMPdThZKtVAVXX5LlKt42J557E0fJ +anWv/PM35wz1PQFztWlR+L1Z0boL+Lq6ZCdDs3yDlYrnnhOW129KlkFJiw4grRbG +96aHW4gSiYuJyhLSVq8iASFG6auYP6eI3uTLKpp1Gfo5XgkF1wMyGrgXUQjHAEB9 +9L74DFn0aXZu06RYW14mc+RCVQZeeEAP0zif7yZRcHSR8XdiAejZy+uh3zkyHbtr +/XH+68+l5hT9AIATxpoASLCZBemugEj7CT9RFLW552BNTcovgSHuUgxletz1iUlM +MJI0WIAyWbEN/yRhD+cKQtB7vPiOJ0c/cJ0n2bYGPaW7y16Prg5Tx5xqbztMD6NA +cKiaB87UblsHotLiVLa9bzNyY61RmOGPdvFqBzgl/vZizl/bY8Jume8G3LneGRro +VD190nZ12V4+MkinjPKecgz4uFi4FyOlFId1WHoAgQciOWpMlKC1otunLMGw8aOb +wEz3bXDqMZ/xrn0+cyjZod/6k/CbsPDizSUgde/ifTIFyZt27su9MR75lJhLJFhW +SMDeBky9pjRd7RZhY3P7GeL6W9iXddRtnmA5XpSLAizrmc5gKm4bjKdLvP025pgf +ZfJ/8eOPTIBGNli2oWXLzhxEdQ== +-----END CERTIFICATE-----`)) + + // Atos TrustedRoot Root CA RSA TLS 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE-----`)) + + // GlobalSign + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE-----`)) + + // GlobalSign + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE-----`)) + + // GlobalSign + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE-----`)) + + // GlobalSign Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE-----`)) + + // GlobalSign Root E46 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE-----`)) + + // GlobalSign Root R46 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE-----`)) + + // GlobalSign Secure Mail Root E45 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICITCCAaegAwIBAgIQdlP+qicdlUZd1vGe5biQCjAKBggqhkjOPQQDAzBSMQsw +CQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UEAxMf +R2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IEU0NTAeFw0yMDAzMTgwMDAwMDBa +Fw00NTAzMTgwMDAwMDBaMFIxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxT +aWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFNlY3VyZSBNYWlsIFJvb3Qg +RTQ1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+XmLgUc3iZY/RUlQfxomC5Myfi7A +wKcImsNuj5s+CyLsN1O3b4qwvCc3S22pRjvZH/+loUS7LXO/nkEHXFObUQg6Wrtv +OMcWkXjCShNpHYLfWi8AiJaiLhx0+Z1+ZjeKo0IwQDAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3xNei1/CQAL9VreUTLYe1aaxFJYw +CgYIKoZIzj0EAwMDaAAwZQIwE7C+13EgPuSrnM42En1fTB8qtWlFM1/TLVqy5IjH +3go2QjJ5naZruuH5RCp7isMSAjEAoGYcToedh8ntmUwbCu4tYMM3xx3NtXKw2cbv +vPL/P/BS3QjnqmR5w+RpV5EvpMt8 +-----END CERTIFICATE-----`)) + + // GlobalSign Secure Mail Root R45 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIQdlP+qExQq5+NMrUdA49X3DANBgkqhkiG9w0BAQwFADBS +MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UE +AxMfR2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IFI0NTAeFw0yMDAzMTgwMDAw +MDBaFw00NTAzMTgwMDAwMDBaMFIxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFNlY3VyZSBNYWlsIFJv +b3QgUjQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3HnMbQb5bbvg +VgRsf+B1zC0FSehL3FTsW3eVcr9/Yp2FqYokUF9T5dt0b6QpWxMqCa2axS/C93Y7 +oUVGqkPmJP4rsG8ycBlGWnkmL/w9fV9ky1fMYWGo2ZVu45Wgbn9HEhjW7wPJ+4r6 +mr2CFalVd0sRT1nga8Nx8wzYVNWBaD4TuRUuh4o8RCc2YiRu+CwFcjBhvUKRI8Sd +JafZVJoUozGtgHkMp2NsmKOsV0czH2WW4dDSNdr5cfehpiW1QV3fPmDY0fafpfK4 +zBOqj/mybuGDLZPdPoUa3eixXCYBy0mF/PzS1H+FYoZ0+cvsNSKiDDCPO6t561by ++kLz7fkfRYlAKa3qknTqUv1WtCvaou11wm6rzlKQS/be8EmPmkjUiBltRebMjLnd +ZGBgAkD4uc+8WOs9hbnGCtOcB2aPxxg5I0bhPB6jL1Bhkgs9K2zxo0c4V5GrDY/G +nU0E0iZSXOWl/SotFioBaeepfeE2t7Eqxdmxjb25i87Mi6E+C0jNUJU0xNgIWdhr +JvS+9dQiFwBXya6bBDAznwv731aiyW5Udtqxl2InWQ8RiiIbZJY/qPG3JEqNPFN8 +bYN2PbImSHP1RBYBLQkqjhaWUNBzBl27IkiCTApGWj+A/1zy8pqsLAjg1urwEjiB +T6YQ7UarzBacC89kppkChURnRq39TecCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGG +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKCTFShu7o8IsjXGnmJ5dKexDit7 +MA0GCSqGSIb3DQEBDAUAA4ICAQBFCvjRXKxigdAE17b/V1GJCwzL3iRlN/urnu1m +9OoMGWmJuBmxMFa02fb3vsaul8tF9hGMOjBkTMGfWcBGQggGR2QXeOCVBwbWjKKs +qdk/03tWT/zEhyjftisWI8CfH1vj1kReIk8jBIw1FrV5B4ZcL5fi9ghkptzbqIrj +pHt3DdEpkyggtFOjS05f3sH2dSP8Hzx4T3AxeC+iNVRxBKzIxG3D9pGx/s3uRG6B +9kDFPioBv6tMsQM/DRHkD9Ik4yKIm59fRz1RSeAJN34XITF2t2dxSChLJdcQ6J9h +WRbFPjJOHwzOo8wP5McRByIvOAjdW5frQmxZmpruetCd38XbCUMuCqoZPWvoajB6 +V+a/s2o5qY/j8U9laLa9nyiPoRZaCVA6Mi4dL0QRQqYA5jGY/y2hD+akYFbPedey +Ttew+m4MVyPHzh+lsUxtGUmeDn9wj3E/WCifdd1h4Dq3Obbul9Q1UfuLSWDIPGau +l+6NJllXu3jwelAwCbBgqp9O3Mk+HjrcYpMzsDpUdG8sMUXRaxEyamh29j32ahNe +JJjn6h2az3iCB2D3TRDTgZpFjZ6vm9yAx0OylWikww7oCkcVv1Qz3AHn1aYec9h6 +sr8vreNVMJ7fDkG84BH1oQyoIuHjAKNOcHyS4wTRekKKdZBZ45vRTKJkvXN5m2/y +s8H2PA== +-----END CERTIFICATE-----`)) + + // Go Daddy Class 2 Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE-----`)) + + // Go Daddy Root Certificate Authority - G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE-----`)) + + // Starfield Class 2 Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE-----`)) + + // Starfield Root Certificate Authority - G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE-----`)) + + // GlobalSign + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE-----`)) + + // GTS Root R1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE-----`)) + + // GTS Root R2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE-----`)) + + // GTS Root R3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE-----`)) + + // GTS Root R4 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE-----`)) + + // Hongkong Post Root CA 3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE-----`)) + + // ACCVRAIZ1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE-----`)) + + // AC RAIZ FNMT-RCM + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE-----`)) + + // AC RAIZ FNMT-RCM SERVIDORES SEGUROS + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE-----`)) + + // Staat der Nederlanden Root CA - G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE-----`)) + + // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE-----`)) + + // HARICA Client ECC Root CA 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICWjCCAeGgAwIBAgIQMWjZ2OFiVx7SGUSI5hB98DAKBggqhkjOPQQDAzBvMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEnMCUGA1UEAwweSEFSSUNBIENsaWVudCBFQ0Mg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDMzNFoXDTQ1MDIxMzExMDMzM1owbzEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJzAlBgNVBAMMHkhBUklDQSBDbGllbnQgRUND +IFJvb3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAcYrZWWlNBcD4L3 +KkD6AsnJPTamowRqwW2VAYhgElRsXKIrbhM6iJUMHCaGNkqJGbcY3jvoqFAfyt9b +v0mAFdvjMOEdWscqigEH/m0sNO8oKJe8wflXhpWLNc+eWtFolaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUUgjSvjKBJf31GpfsTl8au1PNkK0wDgYDVR0P +AQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMEwxRUZPqOa+w3eyGhhLLYh7WOar +lGtEA7AX/9+Cc0RRLP2THQZ7FNKJ7EAM7yEBLgIwL8kuWmwsHdmV4J6wuVxSfPb4 +OMou8dQd8qJJopX4wVheT/5zCu8xsKsjWBOMi947 +-----END CERTIFICATE-----`)) + + // HARICA Client RSA Root CA 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFqjCCA5KgAwIBAgIQVVL4HtsbJCyeu5YYzQIoPjANBgkqhkiG9w0BAQsFADBv +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEnMCUGA1UEAwweSEFSSUNBIENsaWVudCBS +U0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTg0NloXDTQ1MDIxMzEwNTg0NVow +bzELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBS +ZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJzAlBgNVBAMMHkhBUklDQSBDbGllbnQg +UlNBIFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AIHbV0KQLHQ19Pi4dBlNqwlad0WBc2KwNZ/40LczAIcTtparDlQSMAe8m7dI19EZ +g66O2KnxqQCEsIxenugMj1Rpv/bUCE8mcP4YQWMaszKLQPgHq1cx8MYWdmeatN0v +8tFrxdCShJFxbg8uY+kfU6TdUhPMCYMpgQzFU3VEsQ5nUxjQwx+IS5+UJLQpvLvo +Tv1v0hUdSdyNcPIRGiBRVRG6iG/E91B51qox4oQ9XjLIdypQceULL+m26u+rCjM5 +Dv2PpWdDgo6YaQkJG0DNOGdH6snsl3ES3iT1cjzR90NMJveQsonpRUtVPTEFekHi +lbpDwBfFtoU9GY1kcPNbrM2f0yl1h0uVZ2qm+NHdvJCGiUMpqTdb9V2wJlpTQnaQ +K8+eVmwrVM9cmmXfW4tIYDh8+8ULz3YEYwIzKn31g2fn+sZD/SsP1CYvd6QywSTq +ZJ2/szhxMUTyR7iiZkGh+5t7vMdGanW/WqKM6GpEwbiWtcAyCC17dDVzssrG/q8R +chj258jCz6Uq6nvWWeh8oLJqQAlpDqWW29EAufGIbjbwiLKd8VLyw3y/MIk8Cmn5 +IqRl4ZvgdMaxhZeWLK6Uj1CmORIfvkfygXjTdTaefVogl+JSrpmfxnybZvP+2M/u +vZcGHS2F3D42U5Z7ILroyOGtlmI+EXyzAISep0xxq0o3AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFKDWBz1eJPd7oEQuJFINGaorBJGnMA4GA1Ud +DwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEADUf5CWYxUux57sKo8mg+7ZZF +yzqmmGM/6itNTgPQHILhy9Pl1qtbZyi8nf4MmQqAVafOGyNhDbBX8P7gyr7mkNuD +LL6DjvR5tv7QDUKnWB9p6oH1BaX+RmjrbHjJ4Orn5t4xxdLVLIJjKJ1dqBp+iObn +K/Es1dAFntwtvTdm1ASip62/OsKoO63/jZ0z4LmahKGHH3b0gnTXDvkwSD5biD6q +XGvWLwzojnPCGJGDObZmWtAfYCddTeP2Og1mUJx4e6vzExCuDy+r6GSzGCCdRjVk +JXPqmxBcWDWJsUZIp/Ss1B2eW8yppRoTTyRQqtkbbbFA+53dWHTEwm8UcuzbNZ+4 +VHVFw6bIGig1Oq5l8qmYzq9byTiMMTt/zNyW/eJb1tBZ9Ha6C8tPgxDHQNAdYOkq +5UhYdwxFab4ZcQQk4uMkH0rIwT6Z9ZaYOEgloRWwG9fihBhb9nE1mmh7QMwYXAwk +ndSV9ZmqRuqurL/0FBkk6Izs4/W8BmiKKgwFXwqXdafcfsD913oY3zDROEsfsJhw +v8x8c/BuxDGlpJcdrL/ObCFKvicjZ/MGVoEKkY624QMFMyzaNAhNTlAjrR+lxdR6 +/uoJ7KcoYItGfLXqm91P+edrFcaIz0Pb5SfcBFZub0YV8VYt6FwMc8MjgTggy8kM +ac8sqzuEYDMZUv1pFDM= +-----END CERTIFICATE-----`)) + + // HARICA TLS ECC Root CA 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE-----`)) + + // HARICA TLS RSA Root CA 2021 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE-----`)) + + // Hellenic Academic and Research Institutions ECC RootCA 2015 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE-----`)) + + // Hellenic Academic and Research Institutions RootCA 2015 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE-----`)) + + // IdenTrust Commercial Root CA 1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE-----`)) + + // IdenTrust Public Sector Root CA 1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE-----`)) + + // ISRG Root X1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE-----`)) + + // ISRG Root X2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE-----`)) + + // Izenpe.com + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE-----`)) + + // SZAFIR ROOT CA2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE-----`)) + + // LAWtrust Root CA2 (4096) + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFmDCCA4CgAwIBAgIEVRpusTANBgkqhkiG9w0BAQsFADBDMQswCQYDVQQGEwJa +QTERMA8GA1UEChMITEFXdHJ1c3QxITAfBgNVBAMTGExBV3RydXN0IFJvb3QgQ0Ey +ICg0MDk2KTAgFw0yMzAyMTQwOTE5MzhaGA8yMDUzMDIxNDA5NDkzOFowQzELMAkG +A1UEBhMCWkExETAPBgNVBAoTCExBV3RydXN0MSEwHwYDVQQDExhMQVd0cnVzdCBS +b290IENBMiAoNDA5NikwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +F8srQ7ps+cmTimUNEkzsJxS3E3ng1NUtGFbx+eoqEBZObETHamVG85qJNdGH+DOJ +L4gJGpIQkZDBa58Obn8mihNdGKxoAQ0QeGVw2I6PhFqXMBjQEQ5KjVIQpYErUSj1 +Y8S27ECzAeWtd73lOO+8jbPdGaB7DY2022r7JTNa+pGvxHFFMPiIKXvLv9W6JwSO +3bIA98pcmTUU6v11BhUIu8pXaPs/+7Q0c2PR1ePIOFppfWp6RAwNik7tkh0Qjzsi +LLbf7cXG8Il5VGVeXxu9j33fubft6+TFB9FnPJU7kf5CelJAgATSOVdL9JJ9/5vv +5Z3JCbKREjimKQg7ruvKzO1N504hAQf8bzLOaYyEUsZ36icwCt6lrzAraB+s1Owh +rSJJds4PwvIHKvlqEoOaOwSuGXr+oYYk+kFeJXxArCe24yk2bzXiV9AZWN//ZPbD +AUl22yu+vLlPFArVG1gh9hwuAHz4lLXLNxoU5DK5FtRg7AWqXzL6aiMSrNQQu9Ki +grRLDotwJ6rWB8FniPqEwwjJioTI0jdygQ+NFkrk1zVRpTgPjIRLlTbA9ded4F2P +q5HuAAi5nVIf7PiZu3lWsUna0uXYYYtbr/CrN8V7Go6Gvn7FexUeYWjoC4eLc0mh +F3N+KXiOyuBBL3VzdKKXOn/3LnQJuExgi0Y2GRAtnQIDAQABo4GRMIGOMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMCsGA1UdEAQkMCKADzIwMjMwMjE0 +MDkxOTM4WoEPMjA1MzAyMTQwOTQ5MzhaMB8GA1UdIwQYMBaAFNfWVmJcPxeB5nNE +KfVRBe8LYDesMB0GA1UdDgQWBBTX1lZiXD8XgeZzRCn1UQXvC2A3rDANBgkqhkiG +9w0BAQsFAAOCAgEASZwp/j3snkV/qz48/iNvNz53p1P/eJ/8SUSAV2acbtp5/81F +rUyTv7VZxukQt+X4jPuHxR6L2LM/ApYKu4qO79e0wIMgOJdZRWT89ncT8gnXocg4 +dAjq+UhM+h8EnLT/7G5WNnKTbJU+LF/eDwurycwVPhaPZvyyELih0bTewGMZzO9T +qnU2IoslH7+byNfBX+ymNwmqe2K89iIt8dZY3Yy7UvQLp3apensajdytmoFiLoYF +kHJHL6HJZ4SwDWywuJsWt9CZFC+cEpsjqI2mQx7p5S3leKcfZJRktneyqFz7Casp +6x5tddH20MWlwx2fHvMaLbLIH+UoCm7zX/3a5iOhdpBcS5gBgizuRy0CGl9/NMVp +tXKtPvPPnm34KegRJyvgWQsbYetKymmlpNXNURuUjnnN3/audF2xLBuGU/7RMAZB +NAdigkz0fseHdA6wIR4JIIDBsxU9Rm3T8QaSP++glYocbncxtut4KQx77oKlT36k +KV6eqi34jsDz/A0GhZtO3PfiCXzQFFEeerMjr/rRYSpltQHZuOMHyiR20vBKvu+G +BIBCFXARaH7Xx7v+506bnJWlHEqkydAJjKrOSNIekpfXEentZsw33PXXG3SbpupC +rF0y4Fj0gUf/0hLifhzcSXaWwx2fS8pcKjdbPYrROJsh2uO/RUPT4Fh3Hyg= +-----END CERTIFICATE-----`)) + + // e-Szigno Root CA 2017 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE-----`)) + + // Microsec e-Szigno Root CA 2009 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE-----`)) + + // Microsoft ECC Root Certificate Authority 2017 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE-----`)) + + // Microsoft RSA Root Certificate Authority 2017 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE-----`)) + + // NAVER Global Root Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE-----`)) + + // NetLock Arany (Class Gold) Főtanúsítvány + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE-----`)) + + // OISTE Client Root ECC G1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICNDCCAbqgAwIBAgIQVOyX1ou0xAshbg6y0FPIejAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgQ2xpZW50IFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0MzE0MFoXDTQ4MDUy +NDE0MzEzOVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIENsaWVudCBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABIhOaB/Jnr46BFsVwzX0zFDFCK04bqg80gK6zKsl/XVA/WcZ +nxsKXfbLFnv5XB6C3BVE1Jw8bWGTRfRPz2K53z5TjZrUSt6Iqgum8dRh1h501Riy +xU1M74B77A3rgzlUlqNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSZ +Vzs5sS0AjCFmjJVpnG117Iw/+jAdBgNVHQ4EFgQUmVc7ObEtAIwhZoyVaZxtdeyM +P/owDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMQCW/+SCThYiW6CF +GDw9Oo8gBggl5/WRNhmte7TfW2YSN3Nw7c0FKAdeCM4NQl8ZkQICMGdJh64GQR0g +0zGmqiY38SeKYQ3+mgZDpy6eJkejMhiL6F5QBfGwekh23tuhYkq6dw== +-----END CERTIFICATE-----`)) + + // OISTE Client Root RSA G1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQNBdvWQGIG6ql3chIu7Q7czANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgQ2xpZW50IFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MjMyOVoXDTQ4 +MDUyNDE0MjMyOFowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIENsaWVudCBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALpP/v5UE7WEPLzg0zHxHW7cxFNx+uQ5 +UUN2fZIfgX8Aa0HC5trcGE1sF1lwCTNi7GmILbDdWflhYGBW8ba07+uH0BP+w89v +j345WFGziQKOVJUeIl+rKAVDJ/hF9AlCJpT+vRN4u5HyEBCcDWd82mQg63owGrpI +DXhUKpkxNKvLpmrnDGc5ZqQmqCco5/PmPHPkK8xvMS4TdGHLaObSM85SvH5lJFoh +gTFDqrKc0RjnYTxSr4CJ6TRG3vlNmVptHb3GJdGTVY74J5JDOoyVRUDjiRinhsFZ +mMrbJhwTwIyBuZiwrWmtbhjje2JB9a02/gu0eyBfn6lu+ZmCElLSisRUeLR890Gb +A+cHXrPCuUlkZ5IWxGCQDrCCfTOt0Dbq0XZrfIhHmKwb+bRQjGGBadgx8436PvL1 +S6/Owx3vXygb6xjWoFhSMr5Cb81JlyLBcLnT42BP3oOCoE4wvXNTwr0X/aDAmI/q +DhcH5kOVIE7bEaj549O4J0cMJ9sS64FVzHXbn9MXQ8T764oobemvRFBaQ/vxOeKT +UM+Y/ESWWDilpe1Fw1JCBafv5TykrD3n1qlWBaqww6cZ5OU911dEbZQRH8pwyPy5 +TMxBWoN0U5B4z9bULk+xqk0u9dEIWzpk78inqHph7Oym1YhOtlTUWJHCJWSRvAoU +PZIUmrULBukvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +KYIlNQo6vpIr5AkD5OyPjThyOcswHQYDVR0OBBYEFCmCJTUKOr6SK+QJA+Tsj404 +cjnLMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAbSOGwv/14MjA +VYpgMcyXQ0dwQ9Pj7FL608Ke+4kyGspGk08Elyvb0JyEDZUHQlT+72kh35IDLo83 +ISN3qXc3bKDErpynWDlKFZdiRoNRIO0/wqPxw2In0KwTHv48Uh2Q1WPxqV7qf+fn +65ZaUezUqRvjDJRmrMuIkkm+c1yK4Gq8poHNs1zUI5LITfkgjHCUS2ht8o8ebDX3 +6F/U170gN1Jm/yu7SWa3cagsX3MPB5LnTl+lBtvJijyXxULqfQ+BG1frngwP/6Mn +IElTprM6TMttMDXa8vCa/lDfbVwkPU13an2GX0zQ4aa0rgQTAZDxgGiEB5SCB4Pr +keWTDnWRrqMjIElk1Lo5lldw7lU0KHzWr8qpnubJAckHwdBEsYC0UVCqj/ac5Wdz +0BvqgzUXL1DG3lbHu6MDy+KhGOj4zlEGo9IDQGEap2dXg/zRErkoqtpOa9Wc2IU3 +2r0i1zRZnBqmznjWlHgHBg+xkyGgSccQngquUXca+XGQw62YD4opamABqk+tIAMt +ao6jC2rW/ZMMimHLvSjxX3H9uDM51krx9rJoUj5lj0OdgSQk9ihMNaf9MwqleMEE +H+xJasSu1UQWpqeNf9ohlj6ouhZn1Kmh58Ka+BDZO5ruaPYvAO7Lu2aNIjiG9L9f +eKnIoB1au3VQ+VILDx0CLBQa84dqd/M= +-----END CERTIFICATE-----`)) + + // OISTE Server Root ECC G1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE-----`)) + + // OISTE Server Root RSA G1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE-----`)) + + // OISTE WISeKey Global Root GA CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE-----`)) + + // OISTE WISeKey Global Root GB CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE-----`)) + + // OISTE WISeKey Global Root GC CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE-----`)) + + // Security Communication ECC RootCA1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE-----`)) + + // Security Communication RootCA2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE-----`)) + + // AAA Certificate Services + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE-----`)) + + // COMODO Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE-----`)) + + // COMODO ECC Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE-----`)) + + // COMODO RSA Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE-----`)) + + // Entrust Root Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE-----`)) + + // Entrust Root Certification Authority - EC1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE-----`)) + + // Entrust Root Certification Authority - G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE-----`)) + + // Entrust Root Certification Authority - G4 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE-----`)) + + // Entrust.net Certification Authority (2048) + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE-----`)) + + // Sectigo Public Email Protection Root E46 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIQbvXTp0GOoFlApzBr0kBlVjAKBggqhkjOPQQDAzBaMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQDEyhT +ZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgRTQ2MB4XDTIxMDMy +MjAwMDAwMFoXDTQ2MDMyMTIzNTk1OVowWjELMAkGA1UEBhMCR0IxGDAWBgNVBAoT +D1NlY3RpZ28gTGltaXRlZDExMC8GA1UEAxMoU2VjdGlnbyBQdWJsaWMgRW1haWwg +UHJvdGVjdGlvbiBSb290IEU0NjB2MBAGByqGSM49AgEGBSuBBAAiA2IABLinUpT1 +PgWwG/YfsdN+ueQFZlSAzmylaH3kU1LbgvrEht9DePfIrRa8P3gyy2vTSdZE5bN+ +n3umxizy4rbTibCaPEvOiUvGxss6SWAPRrxtTnqcyZuFewq2sEfCiOPU0aNCMEAw +HQYDVR0OBBYEFC1OjKfCI7JXqQZrPmsrifPDXkfOMA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCSnRpZY0VYjhsW5H16 +bDZIMB8rcueQMzT9JKLGBoxvOzJXWvj+xkkSU5rZELKZUXICMAUlKjMh/JPmIqLM +cFUoNVaiB8QhhCMaTEyZUJmSFMtK3Fb79dOPaiz1cTr4izsDng== +-----END CERTIFICATE-----`)) + + // Sectigo Public Email Protection Root R46 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIQHUSeuQ2DkXSu3fLriLemozANBgkqhkiG9w0BAQwFADBa +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQD +EyhTZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgUjQ2MB4XDTIx +MDMyMjAwMDAwMFoXDTQ2MDMyMTIzNTk1OVowWjELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1NlY3RpZ28gTGltaXRlZDExMC8GA1UEAxMoU2VjdGlnbyBQdWJsaWMgRW1h +aWwgUHJvdGVjdGlvbiBSb290IFI0NjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAJHlG/qqbTcrdccuXxSl2yyXtixGj2nZ7JYt8x1avtMdI+ZoCf9KEXMa +rmefdprS5+y42V8r+SZWUa92nan8F+8yCtAjPLosT0eD7J0FaEJeBuDV6CtoSJey ++vOkcTV9NJsXi39NDdvcTwVMlGK/NfovyKccZtlxX+XmWlXKq/S4dxlFUEVOSqvb +nmbBGbc3QshWpUAS+TPoOEU6xoSjAo4vJLDDQYUHSZzP3NHyJm/tMxwzZypFN9mF +ZSIasbUQUglrA8YfcD2RxH2QPe1m+JD/JeDtkqKLMSmtnBJmeGOdV+z7C96O3IvL +Oql39Lrl7DiMi+YTZqdpWMOCGhrN8Z/YU5JOSX2pRefxQyFatz5AzWOJz9m/x1AL +4bzniJatntQX2l3P4JH9phDUuQOBm2ms+4SogTXrG+tobHxgPsPfybSudB1Ird1u +EYbhKmo2Fq7IzrzbWPxAk0DYjlOXwqwiOOWIMbMuoe/s4EIN6v+TVkoGpJtMAmhk +j1ZQwYEF/cvbxdcV8mu1dsOj+TLOyrVKqRt9Gdx/x2p+ley2uI39lUqcoytti/Fw +5UcrAFzkuZ7U+NlYKdDL4ChibK6cYuLMvDaTQfXv/kZilbBXSnQsR1Ipnd2ioU9C +wpLOLVBSXowKoffYncX4/TaHTlf9aKFfmYMc8LXd6JLTZUBVypaFAgMBAAGjQjBA +MB0GA1UdDgQWBBSn15V360rDJ82TvjdMJoQhFH1dmDAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAgEANNLxFfOTAdRyi/Cr +CB8TPHO0sKvoeNlsupqvJuwQgOUNUzHd4/qMUSIkMze4GH46+ljoNOWM4KEfCUHS +Nz/Mywk1Qojp/BHXz0KqpHC2ccFTvcV0r8QiJGPPYoJ9yctRwYiQbVtcvvuZqLq2 +hrDpZgvlG2uv6iuGp9+oI0yWP09XQhgVg0Pxhia3KgPOC53opWgejG+9heMbUY/n +Fy8r0NZ4wi3dcojUZZ76mdR+55cKkgGapamEOgwqdD0zGMiH9+ik9YZCOf1rdSn8 +AAasoqUaVI7pUEkXZq9LBC2blIClVKuMVxdEnw/WaGRytEseAcfZm5TZg5mvEgUR +o5gi0vJXyiT5ujgVEki6Yzv8i5V41nIHVszN/J0c0MVkO2M0zwSZircweXq28sbV +2VR6hwt+TveE7BTziBYS8dWuChoJ7oat5av9rsMpeXTDAV8Rm991mcZK95uPbEns +IS+0AlmzLdBykLoLFHR4S8/BX1VyjlQrE876WAzTuyzZqZFh+PjxtnvevKnMkgTM +S2tfc4C2Ie1QT9d2h27O39K3vWKhfVhiaEVStj/eEtvtBGmedoiqAW3ahsdgG8NS +rDfsUHGAciohRQpTRzwZ643SWQTeJbDrHzVvYH3Xtca7CyeN4E1U5c8dJgFuOzXI +IBKJg/DS7Vg7NJ27MfUy/THzVho= +-----END CERTIFICATE-----`)) + + // Sectigo Public Server Authentication Root E46 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE-----`)) + + // Sectigo Public Server Authentication Root R46 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE-----`)) + + // USERTrust ECC Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE-----`)) + + // USERTrust RSA Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE-----`)) + + // SSL.com Client ECC Root CA 2022 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICQDCCAcagAwIBAgIQdvhIHq7wPHAf4D8lVAGD1TAKBggqhkjOPQQDAzBRMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9T +U0wuY29tIENsaWVudCBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzAzMloX +DTQ2MDgxOTE2MzAzMVowUTELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjEoMCYGA1UEAwwfU1NMLmNvbSBDbGllbnQgRUNDIFJvb3QgQ0EgMjAy +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABC1Tfp+LPrM2ulDizOvcuiaK04wGP2cP +7/UX5dSumkYqQQEHaedncfHCAzbG8CtSjs8UkmikPnBREmmNeKKCyikUwOSUIrJE +kmBvyASkZ9Wi0PPQ1+qOPA+60kBHkDTufaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAf +BgNVHSMEGDAWgBS3/i1ixYFTzVIaL11goMNd+7IcHDAdBgNVHQ4EFgQUt/4tYsWB +U81SGi9dYKDDXfuyHBwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUC +ME0HES0R+7kmwyHdcuEX/MHPFOpJznGHjtZT3BHNXVSKr9kt9IxR6rxmR+J/lYNg +ZQIxAIwhTE+75bBQ35BiSebMkdv4P11xkQiOT5LJf6Zc6hN+7W3E6MMqb1wR4aXz +alqaTQ== +-----END CERTIFICATE-----`)) + + // SSL.com Client RSA Root CA 2022 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFjzCCA3egAwIBAgIQdq/uiJMVRbZQU5uAnKTfmjANBgkqhkiG9w0BAQsFADBR +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSgwJgYDVQQD +DB9TU0wuY29tIENsaWVudCBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzEw +N1oXDTQ2MDgxOTE2MzEwNlowUTELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBD +b3Jwb3JhdGlvbjEoMCYGA1UEAwwfU1NMLmNvbSBDbGllbnQgUlNBIFJvb3QgQ0Eg +MjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALhY20Yw+8k/48jw +ATM04tpIqBjpIG6a1wHh1SmPMLQjauTLYrC+4p8gvT5UoDlox4Y3ZnQGBu90K9rc +n4SpUi+Q0u5+fPulIq1vcEZnlj0p1KO7VnsUBFnBIWNEHrIfElyQh2UNiPYeiCLi +Y1S78zb41n/c2v8pNanGbg5pWz/YvoKHFXBdsMdcEg9jpjjNz3O5ww6JJjcbP2Ic +MmnRm9n/VZAx3rFj3c/FdHf874ghU78AMRomLAAwpV9s4+T2AIrKmIecdAN6i2bs +fv2jjzUlXHils6T7PW2pivBsiIKL/UrQb+TXo7SONEk4vs5F5dIcyl7CNxSLzWZW +Mzed5WvsQ5JkoELadW/AFez5ab00uYp7+hb7Vf5SIOgEBFZWZfU3RJjIikbpt6y4 +6L5ijlQ2W/c7cL9d7i26X95CGYbwf4vrCMvYvuoOQkKgNnNXF+0y6tCN6Acbm5no +xJpiBA5I9zwSuvdYwZqM6cewIzZWNB3LbNq6B4Qd/dGsn+bCie/DuWwYs2mHV1+1 +DDhbpyEkKjunNJGetFTqKE/TwaOL5OYr1fKdv5thACLd1ktEHz9dVv7enHjMmVuq +5L2620NLrUwmTKNNNIpsdDYT22L8m7IFgf+uPwzN9hui9DnnyvVMXPtUdzWAWsAS +oRMBM2c9nYGhqfWFJFiIeOf042hVAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8w +HwYDVR0jBBgwFoAU8DhClDSpPAB/Uu45pfdLDbxqfSMwHQYDVR0OBBYEFPA4QpQ0 +qTwAf1LuOaX3Sw28an0jMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AgEAmU/b8OrWEfoq/cirbeQOc2LSQp8V/nxwUj9kh4IxP0VALuEinwZmKfyW0y2N +tjjH2fMnwVkpoIz2cyQPKCLXTmHdE93bnzJSk/tPzOo4PJhqA6sWryHRQq59RSvq +xM+KWZ+CcHY6+GImyRCXWEAkpC25LymAJ+GJa3LKSQhxN1MF8YDO00IC0vzC0ZQG +7gfi9oPif5/nu1bDW7/dlZMJHiTBzybNraSuwrRp56q17TeU6d3RY4VrmnpKVnbc +GYUo1OTGpNi4lkF30LRZ8UYFh4cCH2m5ghjQQ9km2hpnqNZ1durybQ5C/4gmom6E +/n5iG/DGPe3AHGrHkda4ADdJm7mEBaHNbjHWROpTi7pTmB2hkIrphfgb8pNYw8jc +miZPPiDPT0PzEIx/EGF6NsqqC33Mn0dEWa6llcaZU+MHaz1JELAY/10OhUMUS+dr +00q1smBh3GlJAiNd6JJxw5yfRWd5HtwyhrqqVTxkbzK1EEAV3nJAeOBucLtu6wno +OdmsupJ13UPKugGVrRqBKzrw48UvDBhNEMauwO3+BVJ/GQXLqa81CAw4IuT+VuVT +Pr/k1rPZCMM91TMygSTFqeFlEbgyMzBxGEkdGkXGmhSKWDkobvPLUblJJmR4A8eR +EYOpuZA0tm+qBZ6FKFeZvn8nBkliTaH8CeErRglMFJtWj0U= +-----END CERTIFICATE-----`)) + + // SSL.com EV Root Certification Authority ECC + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE-----`)) + + // SSL.com EV Root Certification Authority RSA R2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE-----`)) + + // SSL.com Root Certification Authority ECC + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE-----`)) + + // SSL.com Root Certification Authority RSA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE-----`)) + + // SSL.com TLS ECC Root CA 2022 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE-----`)) + + // SSL.com TLS RSA Root CA 2022 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE-----`)) + + // SwissSign Gold CA - G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE-----`)) + + // SwissSign RSA SMIME Root CA 2022 - 1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFlzCCA3+gAwIBAgIURg7UAXGQoBqDLEpCECgV0mEbrTIwDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEtMCsGA1UE +AxMkU3dpc3NTaWduIFJTQSBTTUlNRSBSb290IENBIDIwMjIgLSAxMB4XDTIyMDYw +ODEwNTMxM1oXDTQ3MDYwODEwNTMxM1owUzELMAkGA1UEBhMCQ0gxFTATBgNVBAoT +DFN3aXNzU2lnbiBBRzEtMCsGA1UEAxMkU3dpc3NTaWduIFJTQSBTTUlNRSBSb290 +IENBIDIwMjIgLSAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Pv6 +P4aimXAJOsnWoU4Bzka1LSRIDUXprMka1zKApObTytbyKTfsmizWgc7mG52xD0Hf +WNNfqqB5WQuMrfnF+Rz7w+k1QHTDwQzLZ/ucXgwj+dAv+kyCRRy19R/4GW7ak7dO +aIN+Yi0djJUfcNnOWowhXai+CKlWbdn3uZCZxzvXvZ4uyWdXLiHO8DKD+wQB+beC +RA2yy3oJoUg+T8ALahsb7M8dnn8GkKwoBQuo5lQ7oqcsOROZqPs06/XwvQHYiBHI +rroZAkkC3IostL1hYOydeFxqiy8Xhl7yT5MAa13FsqmlGOrmbX5XBfsH/Lx8oUOx +ZhyoZ/urN/aqqrh6Qfc51YyfrnI2J+RixkOZ8aFB6f+Jnw9Jr8kUBhcnZDkNpbQq +W+w8+5/FX8Y7XSYZ8oQpuJVECVL9bDDQYo8opYGWK5QvJnXkCYwK3zjzfl04joKa +jNyers4SQjoi8jWNT9IayEkzC/o2P/8sa2ogcUzNrRA/aTKEjlzuU4hE4t3MAzCS +hnmQKkt1+1JixPRvTffbI6EY3UVTF5pjJEiJIs1+mwEcgCgDj1sr+h/jfBm95o+x +QHag8sc3sjKUEDLNpxOX8TssejQie3Q6QOKvgvjBwXj8X+Q1f8D0TPBMsuqHA3Il +WYMqCKRR3s/uqOfoQD+I8DarCU7YoKh/8+EJ27kCAwEAAaNjMGEwDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHwYDVR0jBBgwFoAUzC6tiYyD40CjJWml +6pJ90jc6x8YwHQYDVR0OBBYEFMwurYmMg+NAoyVppeqSfdI3OsfGMA0GCSqGSIb3 +DQEBCwUAA4ICAQAAB2YWpe3Hub+8yJGtWO1eEgWz9kabe+SEEC8HsVpeMm5tAPBe +x5piOYdN5Dzzvva6alNshG0H1GHKZ2a+mz5FMJ1R0tdaQq6dkg4jq9AVlD6omsqb +7cHCXyGjmYD8uaZhDlCAgCfH6H2g1JR6mAPn7kKL81JQXO++sHZaHAmhv4PAHnZl +0CVBW2mRk3f5jEvwLNubBgAXg/palLSGie+8CgsS+AZN0nPikThduWpLT6ev2iYl +kiMafB8nDZGE7xdy9kbrazs3qdTVmmO6XnmMKrWbojS1zJYn+XkIPH9t4P983MUm +r8OhemkW3Yc1c8ZrMWtWAS1PmdnuyuHQg962hecW+NGuM0j7Gs9dX4qEYXQHbxmw +USGyoQSxe1OP76JFrR+Y3flqBGyqNsWvjOopSUrn/1ezxjwRSRgX5maF4egj8osO +PJPEP3ZOfmKiKcsWMN4saa+Rp+JX5TNMv9iOB6J/oTVGaUqoICn/694glVmxrk0w +a9iatAMfwjjkINUO1howTGicjODtoQ+OQl3rgCoSeaYXF7SVKo40kae90ayoGkMh +i97v4KxGJWUKxiuhmz4i6Bg4tSb2LMoIIN4w0a1U/dxIFZ/Np0HXNziFME8SiEM0 +g9cqTdQAV1zlyvDd4ZIoKxh1vUekQhPpVlqNSl7ODnU1gHMZDywpi7uVuA== +-----END CERTIFICATE-----`)) + + // SwissSign RSA TLS Root CA 2022 - 1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE-----`)) + + // TWCA CYBER Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE-----`)) + + // TWCA Global Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE-----`)) + + // TWCA Global Root CA G2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFlTCCA32gAwIBAgIQQAE0jMIAAAAAAAAAAZdY9DANBgkqhkiG9w0BAQwFADBU +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMR8wHQYDVQQDExZUV0NBIEdsb2JhbCBSb290IENBIEcyMB4XDTIyMTEyMjA2 +NDIyMVoXDTQ3MTEyMjE1NTk1OVowVDELMAkGA1UEBhMCVFcxEjAQBgNVBAoTCVRB +SVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEfMB0GA1UEAxMWVFdDQSBHbG9iYWwg +Um9vdCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKoO1SCS +Aa2C+QwIkTRrihbQRhb/A7jYjeqTNPv/K739bqrcm/KGgVX1iRzEjXVqWHiREx4C +E3A9774K5wCPuDHldMUwvv991pnlwkKjzyHWswh/kdVh5qKVEA3vXpcLSTjVIrDX +i1lvnzWbf9KRzHp/u6Cf3lUz9kuNCup9CcB53L1E4v4c52QhKM8ESuK0v4Z5KrsO +k8mPXqwwOVKQB7nqnCZCFMRnRv7RGmihPlAZoyYKJymQwva063OaeB7hmPRlDDUh +BvgL3mLlTcGzXdm5+mGXKuPqx0RVJJL+Eqc/xHfgLQKBB9X7feYQnjq0qO/s+1Dq +Nc/MfrtCuURsUum/KnIfP96bcOncWsU7u7/wWYWvL8GwFHkFrHWfJfURJwZgIcdt +Zb6oiZzlrEbf+F1EA41gvfexDcwv70FUL+5rlblOfDTfO/l3nX3NBz0cBjMSgOxy +nPItgtrVO8TH+QTDZAJ89TVgp7RGKS4b76VYgC56iVE4Njz9oXe4gDDQit6NpzQm +7CO7GFUYNkXu7QEGqk2/ZAzKmJcaMQJm+HhoW4jfCajnm/o0bXAcIa0Ii/Khtqx2 +ar/xgCUAvjweTa65PLaVY71rfkcSkFVFEY3sFx/BvieBk1djaQAmd4vDWeV70Q1E +8qjw94WaBffCLnCak4XYlZAxkFSm7AufN0UPAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFJKM1DbRW0dTxHENhN1k +KvU2ZEDnMB0GA1UdDgQWBBSSjNQ20VtHU8RxDYTdZCr1NmRA5zANBgkqhkiG9w0B +AQwFAAOCAgEAJfxL2pC02nXnQTqB0ab+oGrzGHFiaiQIi6l6TclVzs8QKC4EGZYF +z10CICo7s1U/Ac1CzbJ37f9183x325alz4xnBvSkm3L2IUkJmKMyXndaYwnvYkOX +Aji16jwYUGj8WVvZedTx5FZIE1bY03ELXniUOBFF+gUX9Q51HmJSYUa6LhmthrSI +D7FQ5kAANBqVnZPgUfnUVUbplTwlhi6X1wExGETsHGDpfWmvMviXQCUkto0aVTzF +t/e8BlI7cTBwPnEXfvFmBF5dvIoxQ6aSHXtU0qU2i2+N1l7a1MMuHd85VWCCMJ4n +/46A3WNMplU12NAzqYBtPl6dzKhngGb6mVcMUsoZdbA4NVUqgcWMHlbXX5DyINja +4GZx6bJ4q2e5JG5rNnL8b439f3I5KGdSkQUfV2XSo6cNYfqh59U1RpXJBof2MOwy +UamsVsAhTqMUdAU6vOO/bT1OP16lpG0pv4RRdVOOhhr1UXAqDRxOQOH9o+OlK2eQ +ksdsroW/OpsXFcqcKpPUTTkNvCAIo42IbAkNjK5EIU3JcezYJtcXni0RGDyjIn24 +J1S/aMg7QsyPXk7n3MLF+mpED41WiHrfiYRsoLM+PfFlAAmI6irrQM6zXawyF67B +m+nQwfVJlN2nznxaB+uuIJwXMJJpk3Lzmltxm/5q33owaY6zLtsPLN0= +-----END CERTIFICATE-----`)) + + // TWCA Root Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE-----`)) + + // Telia Root CA v2 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE-----`)) + + // TeliaSonera Root CA v1 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE-----`)) + + // TrustAsia Global Root CA G3 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE-----`)) + + // TrustAsia Global Root CA G4 + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE-----`)) + + // TrustAsia SMIME ECC Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICNjCCAbugAwIBAgIUWsL4KU/jfcVeHRhvO5MgH/97ui0wCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBFQ0MgUm9vdCBDQTAeFw0y +NDA1MTUwNTQxNTlaFw00NDA1MTUwNTQxNThaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDExtUcnVz +dEFzaWEgU01JTUUgRUNDIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATN +2fsnvWnshsmQQ7FwF5SnyXcjOj8jZdMcox0eQlQg69BCu1m5i6zyho1Ljh2qliIj +OXZtkpvrIst6Q6Jz/XNLwiUPKrFpxv9F36k8lYC7qR5Kky/sHB2I9BGSN583mHKj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDFn5nKyDeYioKzPfiKnWTLj +ZiOlMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNpADBmAjEA3TpMjaTGf+29 +pcZPPv0xSyjWilbfZRZ3h037ujIIgeCeM0iLn5SG7wErlOaM1tSOAjEAn4GcsCb9 +K9by9XGEnqjHiozWWBFStbgEy8xxdWPixhk42W1sGXGkFhkhk7oGRChs +-----END CERTIFICATE-----`)) + + // TrustAsia SMIME RSA Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFhDCCA2ygAwIBAgIUWu5x394MV4W1uzYi17h2RgJzyv8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBSU0EgUm9vdCBDQTAe +Fw0yNDA1MTUwNTQyMDFaFw00NDA1MTUwNTQyMDBaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDExtU +cnVzdEFzaWEgU01JTUUgUlNBIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCYlZytPFlz05N2pkhUphyIckxN4YL/GhMfUN6M2ZBC0byZ0zej +13E6yt1eG5BhQm6PQAFzfR8xutQdbgTSqpCESjMKRJ9aGR+0bi1o/K/An0oQEr8+ +gsKCsC/nkG+QZBCD7Ow2lAx8T+ACDT2HeUJNAOUwrnAfFf36z1IlNk15ILvxEJjg +YIfJ9XgMIu0C5hFs8ZtakRF0htD+eJKWBMOY78Zwr6mQqhb2Iu3Y+kYoceLJCMBQ +vHajui2W8hH5pL0QVvgnbStLZIjcF13PAAiKkq4azBLX3/AQKPPNOuo6Eowb52EJ +Q2rkOOn+dDnbzQo7w09T1q5x1TiDhx/O50zzEVWH37ev9+sahhBtqO1I3TLQ26oq +C3J3KXf9eug/eCAqaL7ebwjmtYVHgDf5cZaLpZhWl3wRZRaO1M7YJ9T5WsWnjbvR +Nw2lq2Vd2nSTiF7bdfZ/m8KasW0IAgyYSrvNMK92NQKFViNRCUAJBffwPR7CyHoa +usVBFbkNdrS0pLhF/Y2jOz0DKs2zlX80e92hT9k6/yf1DcIBnP9ZdVoayefS/X9P +D7X+DTzmoNb7tXZctDBNED/+4utaDrFPT1B+CDMCkVcySYmnQBBQF2ufY7qyslaY +dvT/cukEnNSnTE/2Oh9aVDFvy7oyrfhtr0XHe2NE38L9eOhKirB0dRbejwIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSAGqpDwcl/ixqWRbw9u2tI +UmxbqzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACp1gaGCIOp/ +Vq4JMJcQePTZQBRSpO5qf/AJKNYQY+BOe8kxxwilF+uvhuKXB0+pDqKFzO2kgIEd +WlMGPEwaqbeEhs989YUKcJnQ7TaRjed3Ls6EnCiGLSU1jEwB5n3bYV3id4TTAdFi +3QyiCmSk/PDtOkjyOew11qF6F3Hs09LsuCb7rRVwVkrPZMC5YFv35s2gwgMr+bLl +2rqlNxzYjdp5dCpn5KJ6xyyNpcFqgWzM9ak5aiJ9ouIIzemT27rLH3V3nveYrxTk +O6BMp3LntV5TScz/klfxWSsJuulSk8APRQth1mxZcwvY+QEv2gNPNxz034NeC0Gg +sXw5AKFs0Ni0kXIrGz+imtHE3yvVyJV9hM12G9zkJMY/FSI9hadCK+1+cVlhSMI9 +kWNAfCmzgBYKJfwYYA5TrQ4qzvxBOs2x5GprzDltyE1luKqTiHhuDwKL4JaOdB/Q +fuF0t/aBauQjrI79jzUdmnEKTypVL/4YwQD3e0iKZa9vCB1D51q4H6ToA+v9TLW0 +k6gx3kOdEr3n6aTS32/8b0aj7zFOjRerG6ng+Kk0VqEO53TsqIeF2Hc1S40+bnJ8 +SMwfcrNxdNQkhrzIwON5FAHO2fqBxlyz+V0MOL7O8o6NXz0l4VE5I6jqAI4Es79y +oMK6g/vNpJd1IJq/p1Di3a0sH/Q/o8gx +-----END CERTIFICATE-----`)) + + // TrustAsia TLS ECC Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE-----`)) + + // TrustAsia TLS RSA Root CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE-----`)) + + // Secure Global CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE-----`)) + + // SecureTrust CA + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE-----`)) + + // Trustwave Global Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE-----`)) + + // Trustwave Global ECC P256 Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE-----`)) + + // Trustwave Global ECC P384 Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE-----`)) + + // XRamp Global Certification Authority + mozillaIncluded.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE-----`)) +} diff --git a/common/certificate/store.go b/common/certificate/store.go new file mode 100644 index 00000000..cfced463 --- /dev/null +++ b/common/certificate/store.go @@ -0,0 +1,192 @@ +package certificate + +import ( + "context" + "crypto/x509" + "io/fs" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" +) + +var _ adapter.CertificateStore = (*Store)(nil) + +type Store struct { + access sync.RWMutex + systemPool *x509.CertPool + currentPool *x509.CertPool + certificate string + certificatePaths []string + certificateDirectoryPaths []string + watcher *fswatch.Watcher +} + +func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) { + var systemPool *x509.CertPool + switch options.Store { + case C.CertificateStoreSystem, "": + systemPool = x509.NewCertPool() + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + var systemValid bool + if platformInterface != nil { + for _, cert := range platformInterface.SystemCertificates() { + if systemPool.AppendCertsFromPEM([]byte(cert)) { + systemValid = true + } + } + } + if !systemValid { + certPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + systemPool = certPool + } + case C.CertificateStoreMozilla: + systemPool = mozillaIncluded + case C.CertificateStoreChrome: + systemPool = chromeIncluded + case C.CertificateStoreNone: + systemPool = nil + default: + return nil, E.New("unknown certificate store: ", options.Store) + } + store := &Store{ + systemPool: systemPool, + certificate: strings.Join(options.Certificate, "\n"), + certificatePaths: options.CertificatePath, + certificateDirectoryPaths: options.CertificateDirectoryPath, + } + var watchPaths []string + for _, target := range options.CertificatePath { + watchPaths = append(watchPaths, target) + } + for _, target := range options.CertificateDirectoryPath { + watchPaths = append(watchPaths, target) + } + if len(watchPaths) > 0 { + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: watchPaths, + Logger: logger, + Callback: func(_ string) { + err := store.update() + if err != nil { + logger.Error(E.Cause(err, "reload certificates")) + } + }, + }) + if err != nil { + return nil, E.Cause(err, "fswatch: create fsnotify watcher") + } + store.watcher = watcher + } + err := store.update() + if err != nil { + return nil, E.Cause(err, "initializing certificate store") + } + return store, nil +} + +func (s *Store) Name() string { + return "certificate" +} + +func (s *Store) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if s.watcher != nil { + return s.watcher.Start() + } + return nil +} + +func (s *Store) Close() error { + if s.watcher != nil { + return s.watcher.Close() + } + return nil +} + +func (s *Store) Pool() *x509.CertPool { + s.access.RLock() + defer s.access.RUnlock() + return s.currentPool +} + +func (s *Store) update() error { + s.access.Lock() + defer s.access.Unlock() + var currentPool *x509.CertPool + if s.systemPool == nil { + currentPool = x509.NewCertPool() + } else { + currentPool = s.systemPool.Clone() + } + if s.certificate != "" { + if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) { + return E.New("invalid certificate PEM strings") + } + } + for _, path := range s.certificatePaths { + pemContent, err := os.ReadFile(path) + if err != nil { + return err + } + if !currentPool.AppendCertsFromPEM(pemContent) { + return E.New("invalid certificate PEM file: ", path) + } + } + var firstErr error + for _, directoryPath := range s.certificateDirectoryPaths { + directoryEntries, err := readUniqueDirectoryEntries(directoryPath) + if err != nil { + if firstErr == nil && !os.IsNotExist(err) { + firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath) + } + continue + } + for _, directoryEntry := range directoryEntries { + pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name())) + if err == nil { + currentPool.AppendCertsFromPEM(pemContent) + } + } + } + if firstErr != nil { + return firstErr + } + s.currentPool = currentPool + return nil +} + +func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + uniq := files[:0] + for _, f := range files { + if !isSameDirSymlink(f, dir) { + uniq = append(uniq, f) + } + } + return uniq, nil +} + +func isSameDirSymlink(f fs.DirEntry, dir string) bool { + if f.Type()&fs.ModeSymlink == 0 { + return false + } + target, err := os.Readlink(filepath.Join(dir, f.Name())) + return err == nil && !strings.Contains(target, "/") +} diff --git a/common/compatible/map.go b/common/compatible/map.go new file mode 100644 index 00000000..199edd89 --- /dev/null +++ b/common/compatible/map.go @@ -0,0 +1,58 @@ +package compatible + +import "sync" + +// Map is a generics sync.Map +type Map[K comparable, V any] struct { + m sync.Map +} + +func (m *Map[K, V]) Len() int { + var count int + m.m.Range(func(key, value any) bool { + count++ + return true + }) + return count +} + +func (m *Map[K, V]) Load(key K) (V, bool) { + v, ok := m.m.Load(key) + if !ok { + return *new(V), false + } + + return v.(V), ok +} + +func (m *Map[K, V]) Store(key K, value V) { + m.m.Store(key, value) +} + +func (m *Map[K, V]) Delete(key K) { + m.m.Delete(key) +} + +func (m *Map[K, V]) Range(f func(key K, value V) bool) { + m.m.Range(func(key, value any) bool { + return f(key.(K), value.(V)) + }) +} + +func (m *Map[K, V]) LoadOrStore(key K, value V) (V, bool) { + v, ok := m.m.LoadOrStore(key, value) + return v.(V), ok +} + +func (m *Map[K, V]) LoadAndDelete(key K) (V, bool) { + v, ok := m.m.LoadAndDelete(key) + if !ok { + return *new(V), false + } + + return v.(V), ok +} + +func New[K comparable, V any]() *Map[K, V] { + return &Map[K, V]{m: sync.Map{}} +} diff --git a/common/convertor/adguard/convertor.go b/common/convertor/adguard/convertor.go new file mode 100644 index 00000000..3e6d0254 --- /dev/null +++ b/common/convertor/adguard/convertor.go @@ -0,0 +1,458 @@ +package adguard + +import ( + "bufio" + "bytes" + "io" + "net/netip" + "os" + "strconv" + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" +) + +type agdguardRuleLine struct { + ruleLine string + isRawDomain bool + isExclude bool + isSuffix bool + hasStart bool + hasEnd bool + isRegexp bool + isImportant bool +} + +func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, error) { + scanner := bufio.NewScanner(reader) + var ( + ruleLines []agdguardRuleLine + ignoredLines int + ) +parseLine: + for scanner.Scan() { + ruleLine := scanner.Text() + if ruleLine == "" { + continue + } + if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") { + continue + } + originRuleLine := ruleLine + if M.IsDomainName(ruleLine) { + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: ruleLine, + isRawDomain: true, + }) + continue + } + hostLine, err := parseAdGuardHostLine(ruleLine) + if err == nil { + if hostLine != "" { + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: hostLine, + isRawDomain: true, + hasStart: true, + hasEnd: true, + }) + } + continue + } + if strings.HasSuffix(ruleLine, "|") { + ruleLine = ruleLine[:len(ruleLine)-1] + } + var ( + isExclude bool + isSuffix bool + hasStart bool + hasEnd bool + isRegexp bool + isImportant bool + ) + if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") { + params := common.SubstringAfter(ruleLine, "$") + for _, param := range strings.Split(params, ",") { + paramParts := strings.Split(param, "=") + var ignored bool + if len(paramParts) > 0 && len(paramParts) <= 2 { + switch paramParts[0] { + case "app", "network": + // maybe support by package_name/process_name + case "dnstype": + // maybe support by query_type + case "important": + ignored = true + isImportant = true + case "dnsrewrite": + if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() { + ignored = true + } + } + } + if !ignored { + ignoredLines++ + logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine) + continue parseLine + } + } + ruleLine = common.SubstringBefore(ruleLine, "$") + } + if strings.HasPrefix(ruleLine, "@@") { + ruleLine = ruleLine[2:] + isExclude = true + } + if strings.HasSuffix(ruleLine, "|") { + ruleLine = ruleLine[:len(ruleLine)-1] + } + if strings.HasPrefix(ruleLine, "||") { + ruleLine = ruleLine[2:] + isSuffix = true + } else if strings.HasPrefix(ruleLine, "|") { + ruleLine = ruleLine[1:] + hasStart = true + } + if strings.HasSuffix(ruleLine, "^") { + ruleLine = ruleLine[:len(ruleLine)-1] + hasEnd = true + } + if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") { + ruleLine = ruleLine[1 : len(ruleLine)-1] + if ignoreIPCIDRRegexp(ruleLine) { + ignoredLines++ + logger.Debug("ignored unsupported rule with IPCIDR regexp: ", originRuleLine) + continue + } + isRegexp = true + } else { + if strings.Contains(ruleLine, "://") { + ruleLine = common.SubstringAfter(ruleLine, "://") + isSuffix = true + } + if strings.Contains(ruleLine, "/") { + ignoredLines++ + logger.Debug("ignored unsupported rule with path: ", originRuleLine) + continue + } + if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") { + ignoredLines++ + logger.Debug("ignored unsupported rule with query: ", originRuleLine) + continue + } + if strings.Contains(ruleLine, "[") || strings.Contains(ruleLine, "]") || + strings.Contains(ruleLine, "(") || strings.Contains(ruleLine, ")") || + strings.Contains(ruleLine, "!") || strings.Contains(ruleLine, "#") { + ignoredLines++ + logger.Debug("ignored unsupported cosmetic filter: ", originRuleLine) + continue + } + if strings.Contains(ruleLine, "~") { + ignoredLines++ + logger.Debug("ignored unsupported rule modifier: ", originRuleLine) + continue + } + var domainCheck string + if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") { + domainCheck = "r" + ruleLine + } else { + domainCheck = ruleLine + } + if ruleLine == "" { + ignoredLines++ + logger.Debug("ignored unsupported rule with empty domain", originRuleLine) + continue + } else { + domainCheck = strings.ReplaceAll(domainCheck, "*", "x") + if !M.IsDomainName(domainCheck) { + _, ipErr := parseADGuardIPCIDRLine(ruleLine) + if ipErr == nil { + ignoredLines++ + logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine) + continue + } + if M.ParseSocksaddr(domainCheck).Port != 0 { + logger.Debug("ignored unsupported rule with port: ", originRuleLine) + } else { + logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine) + } + ignoredLines++ + continue + } + } + } + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: ruleLine, + isExclude: isExclude, + isSuffix: isSuffix, + hasStart: hasStart, + hasEnd: hasEnd, + isRegexp: isRegexp, + isImportant: isImportant, + }) + } + if len(ruleLines) == 0 { + return nil, E.New("AdGuard rule-set is empty or all rules are unsupported") + } + if common.All(ruleLines, func(it agdguardRuleLine) bool { + return it.isRawDomain + }) { + return []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + Domain: common.Map(ruleLines, func(it agdguardRuleLine) string { + return it.ruleLine + }), + }, + }, + }, nil + } + mapDomain := func(it agdguardRuleLine) string { + ruleLine := it.ruleLine + if it.isSuffix { + ruleLine = "||" + ruleLine + } else if it.hasStart { + ruleLine = "|" + ruleLine + } + if it.hasEnd { + ruleLine += "^" + } + return ruleLine + } + + importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) + importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) + importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) + importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain) + domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) + domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) + excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) + excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain) + currentRule := option.HeadlessRule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: domain, + DomainRegex: domainRegex, + }, + } + if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: excludeDomain, + DomainRegex: excludeDomainRegex, + Invert: true, + }, + }, + currentRule, + }, + }, + } + } + if len(importantDomain) > 0 || len(importantDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeOr, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: importantDomain, + DomainRegex: importantDomainRegex, + }, + }, + currentRule, + }, + }, + } + } + if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: importantExcludeDomain, + DomainRegex: importantExcludeDomainRegex, + Invert: true, + }, + }, + currentRule, + }, + }, + } + } + if ignoredLines > 0 { + logger.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines) + } + return []option.HeadlessRule{currentRule}, nil +} + +var ErrInvalid = E.New("invalid binary AdGuard rule-set") + +func FromOptions(rules []option.HeadlessRule) ([]byte, error) { + if len(rules) != 1 { + return nil, ErrInvalid + } + rule := rules[0] + var ( + importantDomain []string + importantDomainRegex []string + importantExcludeDomain []string + importantExcludeDomainRegex []string + domain []string + domainRegex []string + excludeDomain []string + excludeDomainRegex []string + ) +parse: + for { + switch rule.Type { + case C.RuleTypeLogical: + if !(len(rule.LogicalOptions.Rules) == 2 && rule.LogicalOptions.Rules[0].Type == C.RuleTypeDefault) { + return nil, ErrInvalid + } + if rule.LogicalOptions.Mode == C.LogicalTypeAnd && rule.LogicalOptions.Rules[0].DefaultOptions.Invert { + if len(importantExcludeDomain) == 0 && len(importantExcludeDomainRegex) == 0 { + importantExcludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain + importantExcludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex + if len(importantExcludeDomain)+len(importantExcludeDomainRegex) == 0 { + return nil, ErrInvalid + } + } else { + excludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain + excludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex + if len(excludeDomain)+len(excludeDomainRegex) == 0 { + return nil, ErrInvalid + } + } + } else if rule.LogicalOptions.Mode == C.LogicalTypeOr && !rule.LogicalOptions.Rules[0].DefaultOptions.Invert { + importantDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain + importantDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex + if len(importantDomain)+len(importantDomainRegex) == 0 { + return nil, ErrInvalid + } + } else { + return nil, ErrInvalid + } + rule = rule.LogicalOptions.Rules[1] + case C.RuleTypeDefault: + domain = rule.DefaultOptions.AdGuardDomain + domainRegex = rule.DefaultOptions.DomainRegex + if len(domain)+len(domainRegex) == 0 { + return nil, ErrInvalid + } + break parse + } + } + var output bytes.Buffer + for _, ruleLine := range importantDomain { + output.WriteString(ruleLine) + output.WriteString("$important\n") + } + for _, ruleLine := range importantDomainRegex { + output.WriteString("/") + output.WriteString(ruleLine) + output.WriteString("/$important\n") + + } + for _, ruleLine := range importantExcludeDomain { + output.WriteString("@@") + output.WriteString(ruleLine) + output.WriteString("$important\n") + } + for _, ruleLine := range importantExcludeDomainRegex { + output.WriteString("@@/") + output.WriteString(ruleLine) + output.WriteString("/$important\n") + } + for _, ruleLine := range domain { + output.WriteString(ruleLine) + output.WriteString("\n") + } + for _, ruleLine := range domainRegex { + output.WriteString("/") + output.WriteString(ruleLine) + output.WriteString("/\n") + } + for _, ruleLine := range excludeDomain { + output.WriteString("@@") + output.WriteString(ruleLine) + output.WriteString("\n") + } + for _, ruleLine := range excludeDomainRegex { + output.WriteString("@@/") + output.WriteString(ruleLine) + output.WriteString("/\n") + } + return output.Bytes(), nil +} + +func ignoreIPCIDRRegexp(ruleLine string) bool { + if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") { + ruleLine = ruleLine[12:] + } else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") { + ruleLine = ruleLine[13:] + } else if strings.HasPrefix(ruleLine, "^") { + ruleLine = ruleLine[1:] + } + return common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)) == nil || + common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "."), 10, 8)) == nil +} + +func parseAdGuardHostLine(ruleLine string) (string, error) { + idx := strings.Index(ruleLine, " ") + if idx == -1 { + return "", os.ErrInvalid + } + address, err := netip.ParseAddr(ruleLine[:idx]) + if err != nil { + return "", err + } + if !address.IsUnspecified() { + return "", nil + } + domain := ruleLine[idx+1:] + if !M.IsDomainName(domain) { + return "", E.New("invalid domain name: ", domain) + } + return domain, nil +} + +func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) { + var isPrefix bool + if strings.HasSuffix(ruleLine, ".") { + isPrefix = true + ruleLine = ruleLine[:len(ruleLine)-1] + } + ruleStringParts := strings.Split(ruleLine, ".") + if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix { + return netip.Prefix{}, os.ErrInvalid + } + ruleParts := make([]uint8, 0, len(ruleStringParts)) + for _, part := range ruleStringParts { + rulePart, err := strconv.ParseUint(part, 10, 8) + if err != nil { + return netip.Prefix{}, err + } + ruleParts = append(ruleParts, uint8(rulePart)) + } + bitLen := len(ruleParts) * 8 + for len(ruleParts) < 4 { + ruleParts = append(ruleParts, 0) + } + return netip.PrefixFrom(netip.AddrFrom4([4]byte(ruleParts)), bitLen), nil +} diff --git a/common/convertor/adguard/convertor_test.go b/common/convertor/adguard/convertor_test.go new file mode 100644 index 00000000..b91d9bef --- /dev/null +++ b/common/convertor/adguard/convertor_test.go @@ -0,0 +1,145 @@ +package adguard + +import ( + "context" + "strings" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing/common/logger" + + "github.com/stretchr/testify/require" +) + +func TestConverter(t *testing.T) { + t.Parallel() + ruleString := `||sagernet.org^$important +@@|sing-box.sagernet.org^$important +||example.org^ +|example.com^ +example.net^ +||example.edu +||example.edu.tw^ +|example.gov +example.arpa +@@|sagernet.example.org^ +` + rules, err := ToOptions(strings.NewReader(ruleString), logger.NOP()) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "example.org", + "www.example.org", + "example.com", + "example.net", + "isexample.net", + "www.example.net", + "example.edu", + "example.edu.cn", + "example.edu.tw", + "www.example.edu", + "www.example.edu.cn", + "example.gov", + "example.gov.cn", + "example.arpa", + "www.example.arpa", + "isexample.arpa", + "example.arpa.cn", + "www.example.arpa.cn", + "isexample.arpa.cn", + "sagernet.org", + "www.sagernet.org", + } + notMatchDomain := []string{ + "example.org.cn", + "notexample.org", + "example.com.cn", + "www.example.com.cn", + "example.net.cn", + "notexample.edu", + "notexample.edu.cn", + "www.example.gov", + "notexample.gov", + "sagernet.example.org", + "sing-box.sagernet.org", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + ruleFromOptions, err := FromOptions(rules) + require.NoError(t, err) + require.Equal(t, ruleString, string(ruleFromOptions)) +} + +func TestHosts(t *testing.T) { + t.Parallel() + rules, err := ToOptions(strings.NewReader(` +127.0.0.1 localhost +::1 localhost #[IPv6] +0.0.0.0 google.com +`), logger.NOP()) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "google.com", + } + notMatchDomain := []string{ + "www.google.com", + "notgoogle.com", + "localhost", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} + +func TestSimpleHosts(t *testing.T) { + t.Parallel() + rules, err := ToOptions(strings.NewReader(` +example.com +www.example.org +`), logger.NOP()) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "example.com", + "www.example.org", + } + notMatchDomain := []string{ + "example.com.cn", + "www.example.com", + "notexample.com", + "example.org", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} diff --git a/common/dialer/default.go b/common/dialer/default.go new file mode 100644 index 00000000..4ffe00c1 --- /dev/null +++ b/common/dialer/default.go @@ -0,0 +1,390 @@ +package dialer + +import ( + "context" + "errors" + "net" + "net/netip" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/listener" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + 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/service" + + "github.com/database64128/tfo-go/v2" +) + +var ( + _ ParallelInterfaceDialer = (*DefaultDialer)(nil) + _ WireGuardListener = (*DefaultDialer)(nil) +) + +type DefaultDialer struct { + dialer4 tfo.Dialer + dialer6 tfo.Dialer + udpDialer4 net.Dialer + udpDialer6 net.Dialer + udpListener net.ListenConfig + udpAddr4 string + udpAddr6 string + netns string + connectionManager adapter.ConnectionManager + networkManager adapter.NetworkManager + networkStrategy *C.NetworkStrategy + defaultNetworkStrategy bool + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType + networkFallbackDelay time.Duration + networkLastFallback common.TypedValue[time.Time] +} + +func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { + connectionManager := service.FromContext[adapter.ConnectionManager](ctx) + networkManager := service.FromContext[adapter.NetworkManager](ctx) + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + + var ( + dialer net.Dialer + listener net.ListenConfig + interfaceFinder control.InterfaceFinder + networkStrategy *C.NetworkStrategy + defaultNetworkStrategy bool + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType + networkFallbackDelay time.Duration + ) + if networkManager != nil { + interfaceFinder = networkManager.InterfaceFinder() + } else { + interfaceFinder = control.NewDefaultInterfaceFinder() + } + if options.BindInterface != "" { + if !(C.IsLinux || C.IsDarwin || C.IsWindows) { + return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows") + } + bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } + if options.RoutingMark > 0 { + if !C.IsLinux { + return nil, E.New("`routing_mark` is only supported on Linux") + } + 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 { + if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 { + return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`") + } + } + + if networkManager != nil { + defaultOptions := networkManager.DefaultOptions() + if defaultOptions.BindInterface != "" && !disableDefaultBind { + bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } else if networkManager.AutoDetectInterface() && !disableDefaultBind { + if platformInterface != nil { + networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) + networkType = common.Map(options.NetworkType, option.InterfaceType.Build) + fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) + if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { + networkStrategy = defaultOptions.NetworkStrategy + networkType = defaultOptions.NetworkType + fallbackNetworkType = defaultOptions.FallbackNetworkType + } + networkFallbackDelay = time.Duration(options.FallbackDelay) + if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 { + networkFallbackDelay = defaultOptions.FallbackDelay + } + if networkStrategy == nil { + networkStrategy = common.Ptr(C.NetworkStrategyDefault) + defaultNetworkStrategy = true + } + bindFunc := networkManager.ProtectFunc() + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } else { + bindFunc := networkManager.AutoDetectInterfaceFunc() + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } + } + if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { + dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) + listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) + } + } + if networkManager != nil { + markFunc := networkManager.AutoRedirectOutputMarkFunc() + dialer.Control = control.Append(dialer.Control, markFunc) + listener.Control = control.Append(listener.Control, markFunc) + } + if options.ReuseAddr { + listener.Control = control.Append(listener.Control, control.ReuseAddr()) + } + if options.ProtectPath != "" { + dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath)) + listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath)) + } + if options.BindAddressNoPort { + if !C.IsLinux { + return nil, E.New("`bind_address_no_port` is only supported on Linux") + } + dialer.Control = control.Append(dialer.Control, control.BindAddressNoPort()) + } + if options.ConnectTimeout != 0 { + dialer.Timeout = time.Duration(options.ConnectTimeout) + } else { + dialer.Timeout = C.TCPConnectTimeout + } + if options.DisableTCPKeepAlive { + dialer.KeepAlive = -1 + dialer.KeepAliveConfig.Enable = false + } else { + keepIdle := time.Duration(options.TCPKeepAlive) + if keepIdle == 0 { + keepIdle = C.TCPKeepAliveInitial + } + keepInterval := time.Duration(options.TCPKeepAliveInterval) + if keepInterval == 0 { + keepInterval = C.TCPKeepAliveInterval + } + dialer.KeepAliveConfig = net.KeepAliveConfig{ + Enable: true, + Idle: keepIdle, + Interval: keepInterval, + } + } + var udpFragment bool + if options.UDPFragment != nil { + udpFragment = *options.UDPFragment + } else { + udpFragment = options.UDPFragmentDefault + } + if !udpFragment { + dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment()) + listener.Control = control.Append(listener.Control, control.DisableUDPFragment()) + } + var ( + dialer4 = dialer + udpDialer4 = dialer + udpAddr4 string + ) + if options.Inet4BindAddress != nil { + bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified()) + dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} + udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} + udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String() + } + var ( + dialer6 = dialer + udpDialer6 = dialer + udpAddr6 string + ) + if options.Inet6BindAddress != nil { + bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified()) + dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} + udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} + udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() + } + if options.TCPMultiPath { + dialer4.SetMultipathTCP(true) + } + tcpDialer4 := tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen} + tcpDialer6 := tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen} + return &DefaultDialer{ + dialer4: tcpDialer4, + dialer6: tcpDialer6, + udpDialer4: udpDialer4, + udpDialer6: udpDialer6, + udpListener: listener, + udpAddr4: udpAddr4, + udpAddr6: udpAddr6, + netns: options.NetNs, + connectionManager: connectionManager, + networkManager: networkManager, + networkStrategy: networkStrategy, + defaultNetworkStrategy: defaultNetworkStrategy, + networkType: networkType, + fallbackNetworkType: fallbackNetworkType, + networkFallbackDelay: networkFallbackDelay, + }, 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") + } else if address.IsDomain() { + return nil, E.New("domain not resolved") + } + if d.networkStrategy == nil { + return d.trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkUDP: + if !address.IsIPv6() { + return d.udpDialer4.DialContext(ctx, network, address.String()) + } else { + return d.udpDialer6.DialContext(ctx, network, address.String()) + } + } + if !address.IsIPv6() { + return DialSlowContext(&d.dialer4, ctx, network, address) + } else { + return DialSlowContext(&d.dialer6, ctx, network, address) + } + })) + } else { + return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) + } +} + +func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + if strategy == nil { + strategy = d.networkStrategy + } + if strategy == nil { + return d.DialContext(ctx, network, address) + } + if len(interfaceType) == 0 { + interfaceType = d.networkType + } + if len(fallbackInterfaceType) == 0 { + fallbackInterfaceType = d.fallbackNetworkType + } + if fallbackDelay == 0 { + fallbackDelay = d.networkFallbackDelay + } + var dialer net.Dialer + if N.NetworkName(network) == N.NetworkTCP { + dialer = d.dialer4.Dialer + } else { + dialer = d.udpDialer4 + } + fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout + var ( + conn net.Conn + isPrimary bool + err error + ) + if !fastFallback { + conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + } else { + conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) + } + if err != nil { + // bind interface failed on legacy xiaomi systems + if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) { + d.networkStrategy = nil + return d.DialContext(ctx, network, address) + } else { + return nil, err + } + } + if !fastFallback && !isPrimary { + d.networkLastFallback.Store(time.Now()) + } + return d.trackConn(conn, nil) +} + +func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if d.networkStrategy == nil { + return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) { + if destination.IsIPv6() { + return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6) + } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { + return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4) + } else { + return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4) + } + })) + } else { + return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) + } +} + +func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer { + if !destination.Is6() { + return d.dialer4.Dialer + } else { + return d.dialer6.Dialer + } +} + +func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { + if strategy == nil { + strategy = d.networkStrategy + } + if strategy == nil { + return d.ListenPacket(ctx, destination) + } + if len(interfaceType) == 0 { + interfaceType = d.networkType + } + if len(fallbackInterfaceType) == 0 { + fallbackInterfaceType = d.fallbackNetworkType + } + if fallbackDelay == 0 { + fallbackDelay = d.networkFallbackDelay + } + network := N.NetworkUDP + if destination.IsIPv4() && !destination.Addr.IsUnspecified() { + network += "4" + } + packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err != nil { + // bind interface failed on legacy xiaomi systems + if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) { + d.networkStrategy = nil + return d.ListenPacket(ctx, destination) + } else { + return nil, err + } + } + return d.trackPacketConn(packetConn, nil) +} + +func (d *DefaultDialer) WireGuardControl() control.Func { + return d.udpListener.Control +} + +func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) { + if d.connectionManager == nil || err != nil { + return conn, err + } + return d.connectionManager.TrackConn(conn), nil +} + +func (d *DefaultDialer) trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) { + if d.connectionManager == nil || err != nil { + return conn, err + } + return d.connectionManager.TrackPacketConn(conn), nil +} diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go new file mode 100644 index 00000000..ca374b2e --- /dev/null +++ b/common/dialer/default_parallel_interface.go @@ -0,0 +1,244 @@ +package dialer + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, bool, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) + if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { + return nil, false, E.New("no available network interface") + } + defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface() + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + returned := make(chan struct{}) + defer close(returned) + type dialResult struct { + net.Conn + error + primary bool + } + results := make(chan dialResult) // unbuffered + startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { + perNetDialer := dialer + if defaultInterface == nil || iif.Index != defaultInterface.Index { + perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) + } + conn, err := perNetDialer.DialContext(ctx, network, addr) + if err != nil { + select { + case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: + case <-returned: + } + } else { + select { + case results <- dialResult{Conn: conn, primary: primary}: + case <-returned: + conn.Close() + } + } + } + primaryCtx, primaryCancel := context.WithCancel(ctx) + defer primaryCancel() + for _, iif := range primaryInterfaces { + go startRacer(primaryCtx, true, iif) + } + var ( + fallbackTimer *time.Timer + fallbackChan <-chan time.Time + ) + if len(fallbackInterfaces) > 0 { + fallbackTimer = time.NewTimer(fallbackDelay) + defer fallbackTimer.Stop() + fallbackChan = fallbackTimer.C + } + var errors []error + for { + select { + case <-fallbackChan: + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + for _, iif := range fallbackInterfaces { + go startRacer(fallbackCtx, false, iif) + } + case res := <-results: + if res.error == nil { + return res.Conn, res.primary, nil + } + errors = append(errors, res.error) + if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) { + return nil, false, E.Errors(errors...) + } + if res.primary && fallbackTimer != nil && fallbackTimer.Stop() { + fallbackTimer.Reset(0) + } + } + } +} + +func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) + if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { + return nil, false, E.New("no available network interface") + } + defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface() + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + returned := make(chan struct{}) + defer close(returned) + type dialResult struct { + net.Conn + error + primary bool + } + startAt := time.Now() + results := make(chan dialResult) // unbuffered + startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { + perNetDialer := dialer + if defaultInterface == nil || iif.Index != defaultInterface.Index { + perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) + } + conn, err := perNetDialer.DialContext(ctx, network, addr) + if err != nil { + select { + case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: + case <-returned: + } + } else { + select { + case results <- dialResult{Conn: conn, primary: primary}: + case <-returned: + if primary && time.Since(startAt) <= fallbackDelay { + resetFastFallback(time.Time{}) + } + conn.Close() + } + } + } + for _, iif := range primaryInterfaces { + go startRacer(ctx, true, iif) + } + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + for _, iif := range fallbackInterfaces { + go startRacer(fallbackCtx, false, iif) + } + var errors []error + for { + select { + case res := <-results: + if res.error == nil { + return res.Conn, res.primary, nil + } + errors = append(errors, res.error) + if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) { + return nil, false, E.Errors(errors...) + } + } + } +} + +func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) + if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { + return nil, E.New("no available network interface") + } + defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface() + var errors []error + for _, primaryInterface := range primaryInterfaces { + perNetListener := listener + if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index { + perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index)) + } + conn, err := perNetListener.ListenPacket(ctx, network, addr) + if err == nil { + return conn, nil + } + errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")")) + } + for _, fallbackInterface := range fallbackInterfaces { + perNetListener := listener + if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index { + perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index)) + } + conn, err := perNetListener.ListenPacket(ctx, network, addr) + if err == nil { + return conn, nil + } + errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")")) + } + return nil, E.Errors(errors...) +} + +func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { + interfaces := networkManager.NetworkInterfaces() + switch strategy { + case C.NetworkStrategyDefault: + if len(interfaceType) == 0 { + defaultIf := networkManager.InterfaceMonitor().DefaultInterface() + if defaultIf != nil { + for _, iif := range interfaces { + if iif.Index == defaultIf.Index { + primaryInterfaces = append(primaryInterfaces, iif) + } + } + } else { + primaryInterfaces = interfaces + } + } else { + primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return common.Contains(interfaceType, it.Type) + }) + } + case C.NetworkStrategyHybrid: + if len(interfaceType) == 0 { + primaryInterfaces = interfaces + } else { + primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return common.Contains(interfaceType, it.Type) + }) + } + case C.NetworkStrategyFallback: + if len(interfaceType) == 0 { + defaultIf := networkManager.InterfaceMonitor().DefaultInterface() + if defaultIf != nil { + for _, iif := range interfaces { + if iif.Index == defaultIf.Index { + primaryInterfaces = append(primaryInterfaces, iif) + break + } + } + } else { + primaryInterfaces = interfaces + } + } else { + primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return common.Contains(interfaceType, it.Type) + }) + } + if len(fallbackInterfaceType) == 0 { + fallbackInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return !common.Any(primaryInterfaces, func(iif adapter.NetworkInterface) bool { + return it.Index == iif.Index + }) + }) + } else { + fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { + return common.Contains(fallbackInterfaceType, iif.Type) + }) + } + } + return primaryInterfaces, fallbackInterfaces +} diff --git a/common/dialer/default_parallel_network.go b/common/dialer/default_parallel_network.go new file mode 100644 index 00000000..006e5747 --- /dev/null +++ b/common/dialer/default_parallel_network.go @@ -0,0 +1,161 @@ +package dialer + +import ( + "context" + "net" + "net/netip" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + if len(destinationAddresses) == 0 { + if !destination.IsIP() { + panic("invalid usage") + } + destinationAddresses = []netip.Addr{destination.Addr} + } + if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { + return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + } + var errors []error + if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { + for _, address := range destinationAddresses { + conn, err := parallelDialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err == nil { + return conn, nil + } + errors = append(errors, err) + } + } else { + for _, address := range destinationAddresses { + conn, err := dialer.DialContext(ctx, network, M.SocksaddrFrom(address, destination.Port)) + if err == nil { + return conn, nil + } + errors = append(errors, err) + } + } + return nil, E.Errors(errors...) +} + +func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + if len(destinationAddresses) == 0 { + if !destination.IsIP() { + panic("invalid usage") + } + destinationAddresses = []netip.Addr{destination.Addr} + } + + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + + returned := make(chan struct{}) + defer close(returned) + + addresses4 := common.Filter(destinationAddresses, func(address netip.Addr) bool { + return address.Is4() || address.Is4In6() + }) + addresses6 := common.Filter(destinationAddresses, func(address netip.Addr) bool { + return address.Is6() && !address.Is4In6() + }) + if len(addresses4) == 0 || len(addresses6) == 0 { + return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + } + var primaries, fallbacks []netip.Addr + if preferIPv6 { + primaries = addresses6 + fallbacks = addresses4 + } else { + primaries = addresses4 + fallbacks = addresses6 + } + type dialResult struct { + net.Conn + error + primary bool + done bool + } + results := make(chan dialResult) // unbuffered + startRacer := func(ctx context.Context, primary bool) { + ras := primaries + if !primary { + ras = fallbacks + } + c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + select { + case results <- dialResult{Conn: c, error: err, primary: primary, done: true}: + case <-returned: + if c != nil { + c.Close() + } + } + } + var primary, fallback dialResult + primaryCtx, primaryCancel := context.WithCancel(ctx) + defer primaryCancel() + go startRacer(primaryCtx, true) + fallbackTimer := time.NewTimer(fallbackDelay) + defer fallbackTimer.Stop() + for { + select { + case <-fallbackTimer.C: + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + go startRacer(fallbackCtx, false) + + case res := <-results: + if res.error == nil { + return res.Conn, nil + } + if res.primary { + primary = res + } else { + fallback = res + } + if primary.done && fallback.done { + return nil, primary.error + } + if res.primary && fallbackTimer.Stop() { + fallbackTimer.Reset(0) + } + } + } +} + +func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { + if len(destinationAddresses) == 0 { + if !destination.IsIP() { + panic("invalid usage") + } + destinationAddresses = []netip.Addr{destination.Addr} + } + if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { + return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + } + var errors []error + if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { + for _, address := range destinationAddresses { + conn, err := parallelDialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err == nil { + return conn, address, nil + } + errors = append(errors, err) + } + } else { + for _, address := range destinationAddresses { + conn, err := dialer.ListenPacket(ctx, M.SocksaddrFrom(address, destination.Port)) + if err == nil { + return conn, address, nil + } + errors = append(errors, err) + } + } + return nil, netip.Addr{}, E.Errors(errors...) +} diff --git a/common/dialer/detour.go b/common/dialer/detour.go new file mode 100644 index 00000000..5c0b552b --- /dev/null +++ b/common/dialer/detour.go @@ -0,0 +1,85 @@ +package dialer + +import ( + "context" + "net" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type DirectDialer interface { + IsEmpty() bool +} + +type DetourDialer struct { + outboundManager adapter.OutboundManager + detour string + legacyDNSDialer bool + dialer N.Dialer + initOnce sync.Once + initErr error +} + +func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer { + return &DetourDialer{ + outboundManager: outboundManager, + detour: detour, + legacyDNSDialer: legacyDNSDialer, + } +} + +func InitializeDetour(dialer N.Dialer) error { + detourDialer, isDetour := common.Cast[*DetourDialer](dialer) + if !isDetour { + return nil + } + return common.Error(detourDialer.Dialer()) +} + +func (d *DetourDialer) Dialer() (N.Dialer, error) { + d.initOnce.Do(d.init) + return d.dialer, d.initErr +} + +func (d *DetourDialer) init() { + dialer, loaded := d.outboundManager.Outbound(d.detour) + if !loaded { + d.initErr = E.New("outbound detour not found: ", d.detour) + return + } + if !d.legacyDNSDialer { + if directDialer, isDirect := dialer.(DirectDialer); isDirect { + if directDialer.IsEmpty() { + d.initErr = E.New("detour to an empty direct outbound makes no sense") + return + } + } + } + d.dialer = dialer +} + +func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + dialer, err := d.Dialer() + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) +} + +func (d *DetourDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + dialer, err := d.Dialer() + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) +} + +func (d *DetourDialer) Upstream() any { + detour, _ := d.Dialer() + return detour +} diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go new file mode 100644 index 00000000..ca6f905f --- /dev/null +++ b/common/dialer/dialer.go @@ -0,0 +1,152 @@ +package dialer + +import ( + "context" + "net" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/option" + 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/service" +) + +type Options struct { + Context context.Context + Options option.DialerOptions + RemoteIsDomain bool + DirectResolver bool + ResolverOnDetour bool + NewDialer bool + LegacyDNSDialer bool + DirectOutbound bool +} + +// TODO: merge with NewWithOptions +func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) { + return NewWithOptions(Options{ + Context: ctx, + Options: options, + RemoteIsDomain: remoteIsDomain, + }) +} + +func NewWithOptions(options Options) (N.Dialer, error) { + dialOptions := options.Options + var ( + dialer N.Dialer + err error + ) + if dialOptions.Detour != "" { + outboundManager := service.FromContext[adapter.OutboundManager](options.Context) + if outboundManager == nil { + return nil, E.New("missing outbound manager") + } + dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer) + } else { + dialer, err = NewDefault(options.Context, dialOptions) + if err != nil { + return nil, err + } + } + if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") { + networkManager := service.FromContext[adapter.NetworkManager](options.Context) + dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context) + var defaultOptions adapter.NetworkOptions + if networkManager != nil { + defaultOptions = networkManager.DefaultOptions() + } + var ( + server string + dnsQueryOptions adapter.DNSQueryOptions + resolveFallbackDelay time.Duration + ) + if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" { + var transport adapter.DNSTransport + if !options.DirectResolver { + var loaded bool + transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server) + if !loaded { + return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server) + } + } + var strategy C.DomainStrategy + if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) { + strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy) + } else if + //nolint:staticcheck + dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { + //nolint:staticcheck + strategy = C.DomainStrategy(dialOptions.DomainStrategy) + deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) + } + server = dialOptions.DomainResolver.Server + dnsQueryOptions = adapter.DNSQueryOptions{ + Transport: transport, + Strategy: strategy, + DisableCache: dialOptions.DomainResolver.DisableCache, + DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache, + RewriteTTL: dialOptions.DomainResolver.RewriteTTL, + ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), + } + resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) + } else if options.DirectResolver { + return nil, E.New("missing domain resolver for domain server address") + } else { + if defaultOptions.DomainResolver != "" { + dnsQueryOptions = defaultOptions.DomainResolveOptions + transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver) + if !loaded { + return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver) + } + dnsQueryOptions.Transport = transport + resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) + } else { + transports := dnsTransport.Transports() + if len(transports) < 2 { + dnsQueryOptions.Transport = dnsTransport.Default() + } else if options.NewDialer { + return nil, E.New("missing domain resolver for domain server address") + } else { + deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) + } + } + if + //nolint:staticcheck + dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { + //nolint:staticcheck + dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy) + deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) + } + } + dialer = NewResolveDialer( + options.Context, + dialer, + dialOptions.Detour == "" && !dialOptions.TCPFastOpen, + server, + dnsQueryOptions, + resolveFallbackDelay, + ) + } + return dialer, nil +} + +type ParallelInterfaceDialer interface { + N.Dialer + DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) +} + +type ParallelNetworkDialer interface { + DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) +} + +type PacketDialerWithDestination interface { + ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) +} diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go new file mode 100644 index 00000000..21fe38d5 --- /dev/null +++ b/common/dialer/resolve.go @@ -0,0 +1,194 @@ +package dialer + +import ( + "context" + "net" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + 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" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +var ( + _ N.Dialer = (*resolveDialer)(nil) + _ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil) +) + +type ResolveDialer interface { + N.Dialer + QueryOptions() adapter.DNSQueryOptions +} + +type ParallelInterfaceResolveDialer interface { + ParallelInterfaceDialer + QueryOptions() adapter.DNSQueryOptions +} + +type resolveDialer struct { + transport adapter.DNSTransportManager + router adapter.DNSRouter + dialer N.Dialer + parallel bool + server string + initOnce sync.Once + initErr error + queryOptions adapter.DNSQueryOptions + fallbackDelay time.Duration +} + +func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer { + if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { + return &resolveParallelNetworkDialer{ + resolveDialer{ + transport: service.FromContext[adapter.DNSTransportManager](ctx), + router: service.FromContext[adapter.DNSRouter](ctx), + dialer: dialer, + parallel: parallel, + server: server, + queryOptions: queryOptions, + fallbackDelay: fallbackDelay, + }, + parallelDialer, + } + } + return &resolveDialer{ + transport: service.FromContext[adapter.DNSTransportManager](ctx), + router: service.FromContext[adapter.DNSRouter](ctx), + dialer: dialer, + parallel: parallel, + server: server, + queryOptions: queryOptions, + fallbackDelay: fallbackDelay, + } +} + +type resolveParallelNetworkDialer struct { + resolveDialer + dialer ParallelInterfaceDialer +} + +func (d *resolveDialer) initialize() error { + d.initOnce.Do(d.initServer) + return d.initErr +} + +func (d *resolveDialer) initServer() { + if d.server == "" { + return + } + transport, loaded := d.transport.Transport(d.server) + if !loaded { + d.initErr = E.New("domain resolver not found: " + d.server) + return + } + d.queryOptions.Transport = transport +} + +func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + err := d.initialize() + if err != nil { + return nil, err + } + if !destination.IsDomain() { + return d.dialer.DialContext(ctx, network, destination) + } + ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) + addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) + if err != nil { + return nil, err + } + if d.parallel { + return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay) + } else { + return N.DialSerial(ctx, d.dialer, network, destination, addresses) + } +} + +func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + err := d.initialize() + if err != nil { + return nil, err + } + if !destination.IsDomain() { + return d.dialer.ListenPacket(ctx, destination) + } + ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) + addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) + if err != nil { + return nil, err + } + conn, destinationAddress, err := N.ListenSerial(ctx, d.dialer, destination, addresses) + if err != nil { + return nil, err + } + return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil +} + +func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions { + return d.queryOptions +} + +func (d *resolveDialer) Upstream() any { + return d.dialer +} + +func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + err := d.initialize() + if err != nil { + return nil, err + } + if !destination.IsDomain() { + return d.dialer.DialContext(ctx, network, destination) + } + ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) + addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) + if err != nil { + return nil, err + } + if fallbackDelay == 0 { + fallbackDelay = d.fallbackDelay + } + if d.parallel { + return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + } else { + return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + } +} + +func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { + err := d.initialize() + if err != nil { + return nil, err + } + if !destination.IsDomain() { + return d.dialer.ListenPacket(ctx, destination) + } + ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) + addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) + if err != nil { + return nil, err + } + if fallbackDelay == 0 { + fallbackDelay = d.fallbackDelay + } + conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err != nil { + return nil, err + } + return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil +} + +func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions { + return d.queryOptions +} + +func (d *resolveParallelNetworkDialer) Upstream() any { + return d.dialer +} diff --git a/common/dialer/router.go b/common/dialer/router.go new file mode 100644 index 00000000..801a36b1 --- /dev/null +++ b/common/dialer/router.go @@ -0,0 +1,33 @@ +package dialer + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +type DefaultOutboundDialer struct { + outbound adapter.OutboundManager +} + +func NewDefaultOutbound(ctx context.Context) N.Dialer { + return &DefaultOutboundDialer{ + outbound: service.FromContext[adapter.OutboundManager](ctx), + } +} + +func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d.outbound.Default().DialContext(ctx, network, destination) +} + +func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return d.outbound.Default().ListenPacket(ctx, destination) +} + +func (d *DefaultOutboundDialer) Upstream() any { + return d.outbound.Default() +} diff --git a/common/dialer/tfo.go b/common/dialer/tfo.go new file mode 100644 index 00000000..e8e93083 --- /dev/null +++ b/common/dialer/tfo.go @@ -0,0 +1,180 @@ +package dialer + +import ( + "context" + "io" + "net" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/database64128/tfo-go/v2" +) + +type slowOpenConn struct { + dialer *tfo.Dialer + ctx context.Context + network string + destination M.Socksaddr + conn atomic.Pointer[net.TCPConn] + create chan struct{} + done chan struct{} + access sync.Mutex + closeOnce sync.Once + err error +} + +func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP { + switch N.NetworkName(network) { + case N.NetworkTCP, N.NetworkUDP: + return dialer.Dialer.DialContext(ctx, network, destination.String()) + default: + return dialer.Dialer.DialContext(ctx, network, destination.AddrString()) + } + } + return &slowOpenConn{ + dialer: dialer, + ctx: ctx, + network: network, + destination: destination, + create: make(chan struct{}), + done: make(chan struct{}), + }, nil +} + +func (c *slowOpenConn) Read(b []byte) (n int, err error) { + conn := c.conn.Load() + if conn != nil { + return conn.Read(b) + } + select { + case <-c.create: + if c.err != nil { + return 0, c.err + } + return c.conn.Load().Read(b) + case <-c.done: + return 0, os.ErrClosed + } +} + +func (c *slowOpenConn) Write(b []byte) (n int, err error) { + tcpConn := c.conn.Load() + if tcpConn != nil { + return tcpConn.Write(b) + } + c.access.Lock() + defer c.access.Unlock() + select { + case <-c.create: + if c.err != nil { + return 0, c.err + } + return c.conn.Load().Write(b) + case <-c.done: + return 0, os.ErrClosed + default: + } + conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b) + if err != nil { + c.err = err + } else { + c.conn.Store(conn.(*net.TCPConn)) + } + n = len(b) + close(c.create) + return +} + +func (c *slowOpenConn) Close() error { + c.closeOnce.Do(func() { + close(c.done) + conn := c.conn.Load() + if conn != nil { + conn.Close() + } + }) + return nil +} + +func (c *slowOpenConn) LocalAddr() net.Addr { + conn := c.conn.Load() + if conn == nil { + return M.Socksaddr{} + } + return conn.LocalAddr() +} + +func (c *slowOpenConn) RemoteAddr() net.Addr { + conn := c.conn.Load() + if conn == nil { + return M.Socksaddr{} + } + return conn.RemoteAddr() +} + +func (c *slowOpenConn) SetDeadline(t time.Time) error { + conn := c.conn.Load() + if conn == nil { + return os.ErrInvalid + } + return conn.SetDeadline(t) +} + +func (c *slowOpenConn) SetReadDeadline(t time.Time) error { + conn := c.conn.Load() + if conn == nil { + return os.ErrInvalid + } + return conn.SetReadDeadline(t) +} + +func (c *slowOpenConn) SetWriteDeadline(t time.Time) error { + conn := c.conn.Load() + if conn == nil { + return os.ErrInvalid + } + return conn.SetWriteDeadline(t) +} + +func (c *slowOpenConn) Upstream() any { + return common.PtrOrNil(c.conn.Load()) +} + +func (c *slowOpenConn) ReaderReplaceable() bool { + return c.conn.Load() != nil +} + +func (c *slowOpenConn) WriterReplaceable() bool { + return c.conn.Load() != nil +} + +func (c *slowOpenConn) LazyHeadroom() bool { + return c.conn.Load() == nil +} + +func (c *slowOpenConn) NeedHandshake() bool { + return c.conn.Load() == nil +} + +func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { + conn := c.conn.Load() + if conn == nil { + select { + case <-c.create: + if c.err != nil { + return 0, c.err + } + case <-c.done: + return 0, c.err + } + } + return bufio.Copy(w, c.conn.Load()) +} diff --git a/common/dialer/wireguard.go b/common/dialer/wireguard.go new file mode 100644 index 00000000..8a916a59 --- /dev/null +++ b/common/dialer/wireguard.go @@ -0,0 +1,9 @@ +package dialer + +import ( + "github.com/sagernet/sing/common/control" +) + +type WireGuardListener interface { + WireGuardControl() control.Func +} diff --git a/common/geoip/reader.go b/common/geoip/reader.go new file mode 100644 index 00000000..9e225f75 --- /dev/null +++ b/common/geoip/reader.go @@ -0,0 +1,38 @@ +package geoip + +import ( + "net/netip" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" +) + +type Reader struct { + reader *maxminddb.Reader +} + +func Open(path string) (*Reader, []string, error) { + database, err := maxminddb.Open(path) + if err != nil { + return nil, nil, err + } + if database.Metadata.DatabaseType != "sing-geoip" { + database.Close() + return nil, nil, E.New("incorrect database type, expected sing-geoip, got ", database.Metadata.DatabaseType) + } + return &Reader{database}, database.Metadata.Languages, nil +} + +func (r *Reader) Lookup(addr netip.Addr) string { + var code string + _ = r.reader.Lookup(addr.AsSlice(), &code) + if code != "" { + return code + } + return "unknown" +} + +func (r *Reader) Close() error { + return r.reader.Close() +} diff --git a/common/geosite/compat_test.go b/common/geosite/compat_test.go new file mode 100644 index 00000000..1a55c644 --- /dev/null +++ b/common/geosite/compat_test.go @@ -0,0 +1,234 @@ +package geosite + +import ( + "bufio" + "bytes" + "encoding/binary" + "strings" + "testing" + + "github.com/sagernet/sing/common/varbin" + + "github.com/stretchr/testify/require" +) + +// Old implementation using varbin reflection-based serialization + +func oldWriteString(writer varbin.Writer, value string) error { + //nolint:staticcheck + return varbin.Write(writer, binary.BigEndian, value) +} + +func oldWriteItem(writer varbin.Writer, item Item) error { + //nolint:staticcheck + return varbin.Write(writer, binary.BigEndian, item) +} + +func oldReadString(reader varbin.Reader) (string, error) { + //nolint:staticcheck + return varbin.ReadValue[string](reader, binary.BigEndian) +} + +func oldReadItem(reader varbin.Reader) (Item, error) { + //nolint:staticcheck + return varbin.ReadValue[Item](reader, binary.BigEndian) +} + +func TestStringCompat(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input string + }{ + {"empty", ""}, + {"single_char", "a"}, + {"ascii", "example.com"}, + {"utf8", "测试域名.中国"}, + {"special_chars", "\x00\xff\n\t"}, + {"127_bytes", strings.Repeat("x", 127)}, + {"128_bytes", strings.Repeat("x", 128)}, + {"16383_bytes", strings.Repeat("x", 16383)}, + {"16384_bytes", strings.Repeat("x", 16384)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Old write + var oldBuf bytes.Buffer + err := oldWriteString(&oldBuf, tc.input) + require.NoError(t, err) + + // New write + var newBuf bytes.Buffer + err = writeString(&newBuf, tc.input) + require.NoError(t, err) + + // Bytes must match + require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), + "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) + + // New write -> old read + readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + require.Equal(t, tc.input, readBack) + + // Old write -> new read + readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) + require.NoError(t, err) + require.Equal(t, tc.input, readBack2) + }) + } +} + +func TestItemCompat(t *testing.T) { + t.Parallel() + + // Note: varbin.Write has a bug where struct values (not pointers) don't write their fields + // because field.CanSet() returns false for non-addressable values. + // The old geosite code passed Item values to varbin.Write, which silently wrote nothing. + // The new code correctly writes Type + Value using manual serialization. + // This test verifies the new serialization format and round-trip correctness. + + cases := []struct { + name string + input Item + }{ + {"domain_empty", Item{Type: RuleTypeDomain, Value: ""}}, + {"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}}, + {"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}}, + {"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}}, + {"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}}, + {"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}}, + {"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}}, + {"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // New write + var newBuf bytes.Buffer + err := newBuf.WriteByte(byte(tc.input.Type)) + require.NoError(t, err) + err = writeString(&newBuf, tc.input.Value) + require.NoError(t, err) + + // Verify format: Type (1 byte) + Value (uvarint len + bytes) + require.True(t, len(newBuf.Bytes()) >= 1, "output too short") + require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch") + + // New write -> old read (varbin can read correctly when given addressable target) + readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + require.Equal(t, tc.input, readBack) + + // New write -> new read + reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes())) + typeByte, err := reader.ReadByte() + require.NoError(t, err) + value, err := readString(reader) + require.NoError(t, err) + require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value}) + }) + } +} + +func TestGeositeWriteReadCompat(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input map[string][]Item + }{ + { + "empty_map", + map[string][]Item{}, + }, + { + "single_code_empty_items", + map[string][]Item{"test": {}}, + }, + { + "single_code_single_item", + map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}}, + }, + { + "single_code_multi_items", + map[string][]Item{ + "test": { + {Type: RuleTypeDomain, Value: "a.com"}, + {Type: RuleTypeDomainSuffix, Value: ".b.com"}, + {Type: RuleTypeDomainKeyword, Value: "keyword"}, + {Type: RuleTypeDomainRegex, Value: `^.*$`}, + }, + }, + }, + { + "multi_code", + map[string][]Item{ + "cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}}, + "us": {{Type: RuleTypeDomain, Value: "google.com"}}, + "jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}}, + }, + }, + { + "utf8_values", + map[string][]Item{ + "test": { + {Type: RuleTypeDomain, Value: "测试.中国"}, + {Type: RuleTypeDomainSuffix, Value: ".テスト"}, + }, + }, + }, + { + "large_items", + generateLargeItems(1000), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Write using new implementation + var buf bytes.Buffer + err := Write(&buf, tc.input) + require.NoError(t, err) + + // Read back and verify + reader, codes, err := NewReader(bytes.NewReader(buf.Bytes())) + require.NoError(t, err) + + // Verify all codes exist + codeSet := make(map[string]bool) + for _, code := range codes { + codeSet[code] = true + } + for code := range tc.input { + require.True(t, codeSet[code], "missing code: %s", code) + } + + // Verify items match + for code, expectedItems := range tc.input { + items, err := reader.Read(code) + require.NoError(t, err) + require.Equal(t, expectedItems, items, "items mismatch for code: %s", code) + } + }) + } +} + +func generateLargeItems(count int) map[string][]Item { + items := make([]Item, count) + for i := 0; i < count; i++ { + items[i] = Item{ + Type: ItemType(i % 4), + Value: strings.Repeat("x", i%200) + ".com", + } + } + return map[string][]Item{"large": items} +} diff --git a/common/geosite/geosite_test.go b/common/geosite/geosite_test.go new file mode 100644 index 00000000..bdcb7a7a --- /dev/null +++ b/common/geosite/geosite_test.go @@ -0,0 +1,34 @@ +package geosite_test + +import ( + "bytes" + "testing" + + "github.com/sagernet/sing-box/common/geosite" + + "github.com/stretchr/testify/require" +) + +func TestGeosite(t *testing.T) { + t.Parallel() + + var buffer bytes.Buffer + err := geosite.Write(&buffer, map[string][]geosite.Item{ + "test": { + { + Type: geosite.RuleTypeDomain, + Value: "example.org", + }, + }, + }) + require.NoError(t, err) + reader, codes, err := geosite.NewReader(bytes.NewReader(buffer.Bytes())) + require.NoError(t, err) + require.Equal(t, []string{"test"}, codes) + items, err := reader.Read("test") + require.NoError(t, err) + require.Equal(t, []geosite.Item{{ + Type: geosite.RuleTypeDomain, + Value: "example.org", + }}, items) +} diff --git a/common/geosite/reader.go b/common/geosite/reader.go new file mode 100644 index 00000000..ef99837d --- /dev/null +++ b/common/geosite/reader.go @@ -0,0 +1,158 @@ +package geosite + +import ( + "bufio" + "encoding/binary" + "io" + "os" + "sync" + "sync/atomic" + + E "github.com/sagernet/sing/common/exceptions" +) + +type Reader struct { + access sync.Mutex + reader io.ReadSeeker + bufferedReader *bufio.Reader + metadataIndex int64 + domainIndex map[string]int + domainLength map[string]int +} + +func Open(path string) (*Reader, []string, error) { + content, err := os.Open(path) + if err != nil { + return nil, nil, err + } + reader, codes, err := NewReader(content) + if err != nil { + content.Close() + return nil, nil, err + } + return reader, codes, nil +} + +func NewReader(readSeeker io.ReadSeeker) (*Reader, []string, error) { + reader := &Reader{ + reader: readSeeker, + } + err := reader.readMetadata() + if err != nil { + return nil, nil, err + } + codes := make([]string, 0, len(reader.domainIndex)) + for code := range reader.domainIndex { + codes = append(codes, code) + } + return reader, codes, nil +} + +type geositeMetadata struct { + Code string + Index uint64 + Length uint64 +} + +func (r *Reader) readMetadata() error { + counter := &readCounter{Reader: r.reader} + reader := bufio.NewReader(counter) + version, err := reader.ReadByte() + if err != nil { + return err + } + if version != 0 { + return E.New("unknown version") + } + entryLength, err := binary.ReadUvarint(reader) + if err != nil { + return err + } + keys := make([]string, entryLength) + domainIndex := make(map[string]int) + domainLength := make(map[string]int) + for i := 0; i < int(entryLength); i++ { + var ( + code string + codeIndex uint64 + codeLength uint64 + ) + code, err = readString(reader) + if err != nil { + return err + } + keys[i] = code + codeIndex, err = binary.ReadUvarint(reader) + if err != nil { + return err + } + codeLength, err = binary.ReadUvarint(reader) + if err != nil { + return err + } + domainIndex[code] = int(codeIndex) + domainLength[code] = int(codeLength) + } + r.domainIndex = domainIndex + r.domainLength = domainLength + r.metadataIndex = counter.count - int64(reader.Buffered()) + r.bufferedReader = reader + return nil +} + +func (r *Reader) Read(code string) ([]Item, error) { + index, exists := r.domainIndex[code] + if !exists { + return nil, E.New("code ", code, " not exists!") + } + _, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart) + if err != nil { + return nil, err + } + r.bufferedReader.Reset(r.reader) + itemList := make([]Item, r.domainLength[code]) + for i := range itemList { + typeByte, err := r.bufferedReader.ReadByte() + if err != nil { + return nil, err + } + itemList[i].Type = ItemType(typeByte) + itemList[i].Value, err = readString(r.bufferedReader) + if err != nil { + return nil, err + } + } + return itemList, nil +} + +func (r *Reader) Upstream() any { + return r.reader +} + +type readCounter struct { + io.Reader + count int64 +} + +func (r *readCounter) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + if n > 0 { + atomic.AddInt64(&r.count, int64(n)) + } + return +} + +func readString(reader io.ByteReader) (string, error) { + length, err := binary.ReadUvarint(reader) + if err != nil { + return "", err + } + bytes := make([]byte, length) + for i := range bytes { + bytes[i], err = reader.ReadByte() + if err != nil { + return "", err + } + } + return string(bytes), nil +} diff --git a/common/geosite/rule.go b/common/geosite/rule.go new file mode 100644 index 00000000..55287156 --- /dev/null +++ b/common/geosite/rule.go @@ -0,0 +1,103 @@ +package geosite + +import "github.com/sagernet/sing-box/option" + +type ItemType = uint8 + +const ( + RuleTypeDomain ItemType = iota + RuleTypeDomainSuffix + RuleTypeDomainKeyword + RuleTypeDomainRegex +) + +type Item struct { + Type ItemType + Value string +} + +func Compile(code []Item) option.DefaultRule { + var domainLength int + var domainSuffixLength int + var domainKeywordLength int + var domainRegexLength int + for _, item := range code { + switch item.Type { + case RuleTypeDomain: + domainLength++ + case RuleTypeDomainSuffix: + domainSuffixLength++ + case RuleTypeDomainKeyword: + domainKeywordLength++ + case RuleTypeDomainRegex: + domainRegexLength++ + } + } + var codeRule option.DefaultRule + if domainLength > 0 { + codeRule.Domain = make([]string, 0, domainLength) + } + if domainSuffixLength > 0 { + codeRule.DomainSuffix = make([]string, 0, domainSuffixLength) + } + if domainKeywordLength > 0 { + codeRule.DomainKeyword = make([]string, 0, domainKeywordLength) + } + if domainRegexLength > 0 { + codeRule.DomainRegex = make([]string, 0, domainRegexLength) + } + for _, item := range code { + switch item.Type { + case RuleTypeDomain: + codeRule.Domain = append(codeRule.Domain, item.Value) + case RuleTypeDomainSuffix: + codeRule.DomainSuffix = append(codeRule.DomainSuffix, item.Value) + case RuleTypeDomainKeyword: + codeRule.DomainKeyword = append(codeRule.DomainKeyword, item.Value) + case RuleTypeDomainRegex: + codeRule.DomainRegex = append(codeRule.DomainRegex, item.Value) + } + } + return codeRule +} + +func Merge(rules []option.DefaultRule) option.DefaultRule { + var domainLength int + var domainSuffixLength int + var domainKeywordLength int + var domainRegexLength int + for _, subRule := range rules { + domainLength += len(subRule.Domain) + domainSuffixLength += len(subRule.DomainSuffix) + domainKeywordLength += len(subRule.DomainKeyword) + domainRegexLength += len(subRule.DomainRegex) + } + var rule option.DefaultRule + if domainLength > 0 { + rule.Domain = make([]string, 0, domainLength) + } + if domainSuffixLength > 0 { + rule.DomainSuffix = make([]string, 0, domainSuffixLength) + } + if domainKeywordLength > 0 { + rule.DomainKeyword = make([]string, 0, domainKeywordLength) + } + if domainRegexLength > 0 { + rule.DomainRegex = make([]string, 0, domainRegexLength) + } + for _, subRule := range rules { + if len(subRule.Domain) > 0 { + rule.Domain = append(rule.Domain, subRule.Domain...) + } + if len(subRule.DomainSuffix) > 0 { + rule.DomainSuffix = append(rule.DomainSuffix, subRule.DomainSuffix...) + } + if len(subRule.DomainKeyword) > 0 { + rule.DomainKeyword = append(rule.DomainKeyword, subRule.DomainKeyword...) + } + if len(subRule.DomainRegex) > 0 { + rule.DomainRegex = append(rule.DomainRegex, subRule.DomainRegex...) + } + } + return rule +} diff --git a/common/geosite/writer.go b/common/geosite/writer.go new file mode 100644 index 00000000..52f2f7b9 --- /dev/null +++ b/common/geosite/writer.go @@ -0,0 +1,73 @@ +package geosite + +import ( + "bytes" + "sort" + + "github.com/sagernet/sing/common/varbin" +) + +func Write(writer varbin.Writer, domains map[string][]Item) error { + keys := make([]string, 0, len(domains)) + for code := range domains { + keys = append(keys, code) + } + sort.Strings(keys) + + content := &bytes.Buffer{} + index := make(map[string]int) + for _, code := range keys { + index[code] = content.Len() + for _, item := range domains[code] { + err := content.WriteByte(byte(item.Type)) + if err != nil { + return err + } + err = writeString(content, item.Value) + if err != nil { + return err + } + } + } + + err := writer.WriteByte(0) + if err != nil { + return err + } + + _, err = varbin.WriteUvarint(writer, uint64(len(keys))) + if err != nil { + return err + } + + for _, code := range keys { + err = writeString(writer, code) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(index[code])) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(domains[code]))) + if err != nil { + return err + } + } + + _, err = writer.Write(content.Bytes()) + if err != nil { + return err + } + + return nil +} + +func writeString(writer varbin.Writer, value string) error { + _, err := varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + _, err = writer.Write([]byte(value)) + return err +} diff --git a/common/interrupt/conn.go b/common/interrupt/conn.go new file mode 100644 index 00000000..6a6d31c6 --- /dev/null +++ b/common/interrupt/conn.go @@ -0,0 +1,75 @@ +package interrupt + +import ( + "net" + + "github.com/sagernet/sing/common/x/list" +) + +/*type GroupedConn interface { + MarkAsInternal() +} + +func MarkAsInternal(conn any) { + if groupedConn, isGroupConn := common.Cast[GroupedConn](conn); isGroupConn { + groupedConn.MarkAsInternal() + } +}*/ + +type Conn struct { + net.Conn + group *Group + element *list.Element[*groupConnItem] +} + +/*func (c *Conn) MarkAsInternal() { + c.element.Value.internal = true +}*/ + +func (c *Conn) Close() error { + c.group.access.Lock() + defer c.group.access.Unlock() + c.group.connections.Remove(c.element) + return c.Conn.Close() +} + +func (c *Conn) ReaderReplaceable() bool { + return true +} + +func (c *Conn) WriterReplaceable() bool { + return true +} + +func (c *Conn) Upstream() any { + return c.Conn +} + +type PacketConn struct { + net.PacketConn + group *Group + element *list.Element[*groupConnItem] +} + +/*func (c *PacketConn) MarkAsInternal() { + c.element.Value.internal = true +}*/ + +func (c *PacketConn) Close() error { + c.group.access.Lock() + defer c.group.access.Unlock() + c.group.connections.Remove(c.element) + return c.PacketConn.Close() +} + +func (c *PacketConn) ReaderReplaceable() bool { + return true +} + +func (c *PacketConn) WriterReplaceable() bool { + return true +} + +func (c *PacketConn) Upstream() any { + return c.PacketConn +} diff --git a/common/interrupt/context.go b/common/interrupt/context.go new file mode 100644 index 00000000..44726b2d --- /dev/null +++ b/common/interrupt/context.go @@ -0,0 +1,13 @@ +package interrupt + +import "context" + +type contextKeyIsExternalConnection struct{} + +func ContextWithIsExternalConnection(ctx context.Context) context.Context { + return context.WithValue(ctx, contextKeyIsExternalConnection{}, true) +} + +func IsExternalConnectionFromContext(ctx context.Context) bool { + return ctx.Value(contextKeyIsExternalConnection{}) != nil +} diff --git a/common/interrupt/group.go b/common/interrupt/group.go new file mode 100644 index 00000000..ba2e7f73 --- /dev/null +++ b/common/interrupt/group.go @@ -0,0 +1,52 @@ +package interrupt + +import ( + "io" + "net" + "sync" + + "github.com/sagernet/sing/common/x/list" +) + +type Group struct { + access sync.Mutex + connections list.List[*groupConnItem] +} + +type groupConnItem struct { + conn io.Closer + isExternal bool +} + +func NewGroup() *Group { + return &Group{} +} + +func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn { + g.access.Lock() + defer g.access.Unlock() + item := g.connections.PushBack(&groupConnItem{conn, isExternal}) + return &Conn{Conn: conn, group: g, element: item} +} + +func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn { + g.access.Lock() + defer g.access.Unlock() + item := g.connections.PushBack(&groupConnItem{conn, isExternal}) + return &PacketConn{PacketConn: conn, group: g, element: item} +} + +func (g *Group) Interrupt(interruptExternalConnections bool) { + g.access.Lock() + defer g.access.Unlock() + var toDelete []*list.Element[*groupConnItem] + for element := g.connections.Front(); element != nil; element = element.Next() { + if !element.Value.isExternal || interruptExternalConnections { + element.Value.conn.Close() + toDelete = append(toDelete, element) + } + } + for _, element := range toDelete { + g.connections.Remove(element) + } +} diff --git a/common/ja3/LICENSE b/common/ja3/LICENSE new file mode 100644 index 00000000..b42ec95d --- /dev/null +++ b/common/ja3/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Open Systems AG +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/common/ja3/README.md b/common/ja3/README.md new file mode 100644 index 00000000..5c0bd8ae --- /dev/null +++ b/common/ja3/README.md @@ -0,0 +1,3 @@ +# JA3 + +mod from: https://github.com/open-ch/ja3 \ No newline at end of file diff --git a/common/ja3/error.go b/common/ja3/error.go new file mode 100644 index 00000000..cab85492 --- /dev/null +++ b/common/ja3/error.go @@ -0,0 +1,31 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import "fmt" + +// Error types +const ( + LengthErr string = "length check %v failed" + ContentTypeErr string = "content type not matching" + VersionErr string = "version check %v failed" + HandshakeTypeErr string = "handshake type not matching" + SNITypeErr string = "SNI type not supported" +) + +// ParseError can be encountered while parsing a segment +type ParseError struct { + errType string + check int +} + +func (e *ParseError) Error() string { + if e.errType == LengthErr || e.errType == VersionErr { + return fmt.Sprintf(e.errType, e.check) + } + return fmt.Sprint(e.errType) +} diff --git a/common/ja3/ja3.go b/common/ja3/ja3.go new file mode 100644 index 00000000..608819fe --- /dev/null +++ b/common/ja3/ja3.go @@ -0,0 +1,83 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import ( + "crypto/md5" + "encoding/hex" + + "golang.org/x/exp/slices" +) + +type ClientHello struct { + Version uint16 + CipherSuites []uint16 + Extensions []uint16 + EllipticCurves []uint16 + EllipticCurvePF []uint8 + Versions []uint16 + SignatureAlgorithms []uint16 + ServerName string + ja3ByteString []byte + ja3Hash string +} + +func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool { + if j.Version != another.Version { + return false + } + if !slices.Equal(j.CipherSuites, another.CipherSuites) { + return false + } + if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) { + return false + } + if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) { + return false + } + if !slices.Equal(j.EllipticCurves, another.EllipticCurves) { + return false + } + if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) { + return false + } + if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) { + return false + } + return true +} + +func (j *ClientHello) sortedExtensions() []uint16 { + extensions := make([]uint16, len(j.Extensions)) + copy(extensions, j.Extensions) + slices.Sort(extensions) + return extensions +} + +func Compute(payload []byte) (*ClientHello, error) { + ja3 := ClientHello{} + err := ja3.parseSegment(payload) + return &ja3, err +} + +func (j *ClientHello) String() string { + if j.ja3ByteString == nil { + j.marshalJA3() + } + return string(j.ja3ByteString) +} + +func (j *ClientHello) Hash() string { + if j.ja3ByteString == nil { + j.marshalJA3() + } + if j.ja3Hash == "" { + h := md5.Sum(j.ja3ByteString) + j.ja3Hash = hex.EncodeToString(h[:]) + } + return j.ja3Hash +} diff --git a/common/ja3/parser.go b/common/ja3/parser.go new file mode 100644 index 00000000..f9cca603 --- /dev/null +++ b/common/ja3/parser.go @@ -0,0 +1,357 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import ( + "encoding/binary" + "strconv" +) + +const ( + // Constants used for parsing + recordLayerHeaderLen int = 5 + handshakeHeaderLen int = 6 + randomDataLen int = 32 + sessionIDHeaderLen int = 1 + cipherSuiteHeaderLen int = 2 + compressMethodHeaderLen int = 1 + extensionsHeaderLen int = 2 + extensionHeaderLen int = 4 + sniExtensionHeaderLen int = 5 + ecExtensionHeaderLen int = 2 + ecpfExtensionHeaderLen int = 1 + versionExtensionHeaderLen int = 1 + signatureAlgorithmsExtensionHeaderLen int = 2 + contentType uint8 = 22 + handshakeType uint8 = 1 + sniExtensionType uint16 = 0 + sniNameDNSHostnameType uint8 = 0 + ecExtensionType uint16 = 10 + ecpfExtensionType uint16 = 11 + versionExtensionType uint16 = 43 + signatureAlgorithmsExtensionType uint16 = 13 + + // Versions + // The bitmask covers the versions SSL3.0 to TLS1.2 + tlsVersionBitmask uint16 = 0xFFFC + tls13 uint16 = 0x0304 + + // GREASE values + // The bitmask covers all GREASE values + GreaseBitmask uint16 = 0x0F0F + + // Constants used for marshalling + dashByte = byte(45) + commaByte = byte(44) +) + +// parseSegment to populate the corresponding ClientHello object or return an error +func (j *ClientHello) parseSegment(segment []byte) error { + // Check if we can decode the next fields + if len(segment) < recordLayerHeaderLen { + return &ParseError{LengthErr, 1} + } + + // Check if we have "Content Type: Handshake (22)" + contType := uint8(segment[0]) + if contType != contentType { + return &ParseError{errType: ContentTypeErr} + } + + // Check if TLS record layer version is supported + tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2]) + if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 { + return &ParseError{VersionErr, 1} + } + + // Check that the Handshake is as long as expected from the length field + segmentLen := uint16(segment[3])<<8 | uint16(segment[4]) + if len(segment[recordLayerHeaderLen:]) < int(segmentLen) { + return &ParseError{LengthErr, 2} + } + // Keep the Handshake messege, ignore any additional following record types + hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)] + + err := j.parseHandshake(hs) + + return err +} + +// parseHandshake body +func (j *ClientHello) parseHandshake(hs []byte) error { + // Check if we can decode the next fields + if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { + return &ParseError{LengthErr, 3} + } + + // Check if we have "Handshake Type: Client Hello (1)" + handshType := uint8(hs[0]) + if handshType != handshakeType { + return &ParseError{errType: HandshakeTypeErr} + } + + // Check if actual length of handshake matches (this is a great exclusion criterion for false positives, + // as these fields have to match the actual length of the rest of the segment) + handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3]) + if len(hs[4:]) != int(handshakeLen) { + return &ParseError{LengthErr, 4} + } + + // Check if Client Hello version is supported + tlsVersion := uint16(hs[4])<<8 | uint16(hs[5]) + if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { + return &ParseError{VersionErr, 2} + } + j.Version = tlsVersion + + // Check if we can decode the next fields + sessionIDLen := uint8(hs[38]) + if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) { + return &ParseError{LengthErr, 5} + } + + // Cipher Suites + cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):] + + // Check if we can decode the next fields + if len(cs) < cipherSuiteHeaderLen { + return &ParseError{LengthErr, 6} + } + + csLen := uint16(cs[0])<<8 | uint16(cs[1]) + numCiphers := int(csLen / 2) + cipherSuites := make([]uint16, 0, numCiphers) + + // Check if we can decode the next fields + if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { + return &ParseError{LengthErr, 7} + } + + for i := 0; i < numCiphers; i++ { + cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1]) + cipherSuites = append(cipherSuites, cipherSuite) + } + j.CipherSuites = cipherSuites + + // Check if we can decode the next fields + compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)]) + if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) { + return &ParseError{LengthErr, 8} + } + + // Extensions + exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):] + + err := j.parseExtensions(exs) + + return err +} + +// parseExtensions of the handshake +func (j *ClientHello) parseExtensions(exs []byte) error { + // Check for no extensions, this fields header is nonexistent if no body is used + if len(exs) == 0 { + return nil + } + + // Check if we can decode the next fields + if len(exs) < extensionsHeaderLen { + return &ParseError{LengthErr, 9} + } + + exsLen := uint16(exs[0])<<8 | uint16(exs[1]) + exs = exs[extensionsHeaderLen:] + + // Check if we can decode the next fields + if len(exs) < int(exsLen) { + return &ParseError{LengthErr, 10} + } + + var sni []byte + var extensions, ellipticCurves []uint16 + var ellipticCurvePF []uint8 + var versions []uint16 + var signatureAlgorithms []uint16 + for len(exs) > 0 { + + // Check if we can decode the next fields + if len(exs) < extensionHeaderLen { + return &ParseError{LengthErr, 11} + } + + exType := uint16(exs[0])<<8 | uint16(exs[1]) + exLen := uint16(exs[2])<<8 | uint16(exs[3]) + // Ignore any GREASE extensions + extensions = append(extensions, exType) + // Check if we can decode the next fields + if len(exs) < extensionHeaderLen+int(exLen) { + return &ParseError{LengthErr, 12} + } + + sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)] + + switch exType { + case sniExtensionType: // Extensions: server_name + + // Check if we can decode the next fields + if len(sex) < sniExtensionHeaderLen { + return &ParseError{LengthErr, 13} + } + + sniType := uint8(sex[2]) + sniLen := uint16(sex[3])<<8 | uint16(sex[4]) + sex = sex[sniExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != int(sniLen) { + return &ParseError{LengthErr, 14} + } + + switch sniType { + case sniNameDNSHostnameType: + sni = sex + default: + return &ParseError{errType: SNITypeErr} + } + case ecExtensionType: // Extensions: supported_groups + + // Check if we can decode the next fields + if len(sex) < ecExtensionHeaderLen { + return &ParseError{LengthErr, 15} + } + + ecsLen := uint16(sex[0])<<8 | uint16(sex[1]) + numCurves := int(ecsLen / 2) + ellipticCurves = make([]uint16, 0, numCurves) + sex = sex[ecExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != int(ecsLen) { + return &ParseError{LengthErr, 16} + } + + for i := 0; i < numCurves; i++ { + ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2]) + ellipticCurves = append(ellipticCurves, ecType) + } + + case ecpfExtensionType: // Extensions: ec_point_formats + + // Check if we can decode the next fields + if len(sex) < ecpfExtensionHeaderLen { + return &ParseError{LengthErr, 17} + } + + ecpfsLen := uint8(sex[0]) + numPF := int(ecpfsLen) + ellipticCurvePF = make([]uint8, numPF) + sex = sex[ecpfExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != numPF { + return &ParseError{LengthErr, 18} + } + + for i := 0; i < numPF; i++ { + ellipticCurvePF[i] = uint8(sex[i]) + } + case versionExtensionType: + if len(sex) < versionExtensionHeaderLen { + return &ParseError{LengthErr, 19} + } + versionsLen := int(sex[0]) + for i := 0; i < versionsLen; i += 2 { + versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:])) + } + case signatureAlgorithmsExtensionType: + if len(sex) < signatureAlgorithmsExtensionHeaderLen { + return &ParseError{LengthErr, 20} + } + ssaLen := binary.BigEndian.Uint16(sex) + for i := 0; i < int(ssaLen); i += 2 { + signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:])) + } + } + exs = exs[4+exLen:] + } + j.ServerName = string(sni) + j.Extensions = extensions + j.EllipticCurves = ellipticCurves + j.EllipticCurvePF = ellipticCurvePF + j.Versions = versions + j.SignatureAlgorithms = signatureAlgorithms + return nil +} + +// marshalJA3 into a byte string +func (j *ClientHello) marshalJA3() { + // An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we + // also need a byte for each separating character, except at the end. + byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1 + byteString := make([]byte, 0, byteStringLen) + + // Version + byteString = strconv.AppendUint(byteString, uint64(j.Version), 10) + byteString = append(byteString, commaByte) + + // Cipher Suites + if len(j.CipherSuites) != 0 { + for _, val := range j.CipherSuites { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // Extensions + if len(j.Extensions) != 0 { + for _, val := range j.Extensions { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // Elliptic curves + if len(j.EllipticCurves) != 0 { + for _, val := range j.EllipticCurves { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // ECPF + if len(j.EllipticCurvePF) != 0 { + for _, val := range j.EllipticCurvePF { + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Remove last dash + byteString = byteString[:len(byteString)-1] + } + + j.ja3ByteString = byteString +} diff --git a/common/ktls/ktls.go b/common/ktls/ktls.go new file mode 100644 index 00000000..33a59b13 --- /dev/null +++ b/common/ktls/ktls.go @@ -0,0 +1,133 @@ +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "io" + "net" + "os" + "syscall" + + "github.com/sagernet/sing-box/common/badtls" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + + "golang.org/x/sys/unix" +) + +type Conn struct { + aTLS.Conn + ctx context.Context + logger logger.ContextLogger + conn net.Conn + rawConn *badtls.RawConn + syscallConn syscall.Conn + rawSyscallConn syscall.RawConn + readWaitOptions N.ReadWaitOptions + kernelTx bool + kernelRx bool + pendingRxSplice bool +} + +func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { + err := Load() + if err != nil { + return nil, err + } + syscallConn, isSyscallConn := N.CastReader[interface { + io.Reader + syscall.Conn + }](conn.NetConn()) + if !isSyscallConn { + return nil, os.ErrInvalid + } + rawSyscallConn, err := syscallConn.SyscallConn() + if err != nil { + return nil, err + } + rawConn, err := badtls.NewRawConn(conn) + if err != nil { + return nil, err + } + if *rawConn.Vers != tls.VersionTLS13 { + return nil, os.ErrInvalid + } + for rawConn.RawInput.Len() > 0 { + err = rawConn.ReadRecord() + if err != nil { + return nil, err + } + for rawConn.Hand.Len() > 0 { + err = rawConn.HandlePostHandshakeMessage() + if err != nil { + return nil, E.Cause(err, "handle post-handshake messages") + } + } + } + kConn := &Conn{ + Conn: conn, + ctx: ctx, + logger: logger, + conn: conn.NetConn(), + rawConn: rawConn, + syscallConn: syscallConn, + rawSyscallConn: rawSyscallConn, + } + err = kConn.setupKernel(txOffload, rxOffload) + if err != nil { + return nil, err + } + return kConn, nil +} + +func (c *Conn) Upstream() any { + return c.Conn +} + +func (c *Conn) SyscallConnForRead() syscall.RawConn { + if !c.kernelRx { + return nil + } + if !*c.rawConn.IsClient { + c.logger.WarnContext(c.ctx, "ktls: RX splice is unavailable on the server size, since it will cause an unknown failure") + return nil + } + c.logger.DebugContext(c.ctx, "ktls: RX splice requested") + return c.rawSyscallConn +} + +func (c *Conn) HandleSyscallReadError(inputErr error) ([]byte, error) { + if errors.Is(inputErr, unix.EINVAL) { + c.pendingRxSplice = true + err := c.readRecord() + if err != nil { + return nil, E.Cause(err, "ktls: handle non-application-data record") + } + var input bytes.Buffer + if c.rawConn.Input.Len() > 0 { + _, err = c.rawConn.Input.WriteTo(&input) + if err != nil { + return nil, err + } + } + return input.Bytes(), nil + } else if errors.Is(inputErr, unix.EBADMSG) { + return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertBadRecordMAC)) + } else { + return nil, E.Cause(inputErr, "ktls: unexpected errno") + } +} + +func (c *Conn) SyscallConnForWrite() syscall.RawConn { + if !c.kernelTx { + return nil + } + c.logger.DebugContext(c.ctx, "ktls: TX splice requested") + return c.rawSyscallConn +} diff --git a/common/ktls/ktls_alert.go b/common/ktls/ktls_alert.go new file mode 100644 index 00000000..e755feae --- /dev/null +++ b/common/ktls/ktls_alert.go @@ -0,0 +1,80 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "crypto/tls" + "net" +) + +const ( + // alert level + alertLevelWarning = 1 + alertLevelError = 2 +) + +const ( + alertCloseNotify = 0 + alertUnexpectedMessage = 10 + alertBadRecordMAC = 20 + alertDecryptionFailed = 21 + alertRecordOverflow = 22 + alertDecompressionFailure = 30 + alertHandshakeFailure = 40 + alertBadCertificate = 42 + alertUnsupportedCertificate = 43 + alertCertificateRevoked = 44 + alertCertificateExpired = 45 + alertCertificateUnknown = 46 + alertIllegalParameter = 47 + alertUnknownCA = 48 + alertAccessDenied = 49 + alertDecodeError = 50 + alertDecryptError = 51 + alertExportRestriction = 60 + alertProtocolVersion = 70 + alertInsufficientSecurity = 71 + alertInternalError = 80 + alertInappropriateFallback = 86 + alertUserCanceled = 90 + alertNoRenegotiation = 100 + alertMissingExtension = 109 + alertUnsupportedExtension = 110 + alertCertificateUnobtainable = 111 + alertUnrecognizedName = 112 + alertBadCertificateStatusResponse = 113 + alertBadCertificateHashValue = 114 + alertUnknownPSKIdentity = 115 + alertCertificateRequired = 116 + alertNoApplicationProtocol = 120 + alertECHRequired = 121 +) + +func (c *Conn) sendAlertLocked(err uint8) error { + switch err { + case alertNoRenegotiation, alertCloseNotify: + c.rawConn.Tmp[0] = alertLevelWarning + default: + c.rawConn.Tmp[0] = alertLevelError + } + c.rawConn.Tmp[1] = byte(err) + + _, writeErr := c.writeRecordLocked(recordTypeAlert, c.rawConn.Tmp[0:2]) + if err == alertCloseNotify { + // closeNotify is a special case in that it isn't an error. + return writeErr + } + + return c.rawConn.Out.SetErrorLocked(&net.OpError{Op: "local error", Err: tls.AlertError(err)}) +} + +// sendAlert sends a TLS alert message. +func (c *Conn) sendAlert(err uint8) error { + c.rawConn.Out.Lock() + defer c.rawConn.Out.Unlock() + return c.sendAlertLocked(err) +} diff --git a/common/ktls/ktls_cipher_suites_linux.go b/common/ktls/ktls_cipher_suites_linux.go new file mode 100644 index 00000000..571f0251 --- /dev/null +++ b/common/ktls/ktls_cipher_suites_linux.go @@ -0,0 +1,326 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "crypto/tls" + "unsafe" + + "github.com/sagernet/sing-box/common/badtls" +) + +type kernelCryptoCipherType uint16 + +const ( + TLS_CIPHER_AES_GCM_128 kernelCryptoCipherType = 51 + TLS_CIPHER_AES_GCM_128_IV_SIZE kernelCryptoCipherType = 8 + TLS_CIPHER_AES_GCM_128_KEY_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_AES_GCM_128_SALT_SIZE kernelCryptoCipherType = 4 + TLS_CIPHER_AES_GCM_128_TAG_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + TLS_CIPHER_AES_GCM_256 kernelCryptoCipherType = 52 + TLS_CIPHER_AES_GCM_256_IV_SIZE kernelCryptoCipherType = 8 + TLS_CIPHER_AES_GCM_256_KEY_SIZE kernelCryptoCipherType = 32 + TLS_CIPHER_AES_GCM_256_SALT_SIZE kernelCryptoCipherType = 4 + TLS_CIPHER_AES_GCM_256_TAG_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + TLS_CIPHER_AES_CCM_128 kernelCryptoCipherType = 53 + TLS_CIPHER_AES_CCM_128_IV_SIZE kernelCryptoCipherType = 8 + TLS_CIPHER_AES_CCM_128_KEY_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_AES_CCM_128_SALT_SIZE kernelCryptoCipherType = 4 + TLS_CIPHER_AES_CCM_128_TAG_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + TLS_CIPHER_CHACHA20_POLY1305 kernelCryptoCipherType = 54 + TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE kernelCryptoCipherType = 12 + TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE kernelCryptoCipherType = 32 + TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE kernelCryptoCipherType = 0 + TLS_CIPHER_CHACHA20_POLY1305_TAG_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + // TLS_CIPHER_SM4_GCM kernelCryptoCipherType = 55 + // TLS_CIPHER_SM4_GCM_IV_SIZE kernelCryptoCipherType = 8 + // TLS_CIPHER_SM4_GCM_KEY_SIZE kernelCryptoCipherType = 16 + // TLS_CIPHER_SM4_GCM_SALT_SIZE kernelCryptoCipherType = 4 + // TLS_CIPHER_SM4_GCM_TAG_SIZE kernelCryptoCipherType = 16 + // TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + // TLS_CIPHER_SM4_CCM kernelCryptoCipherType = 56 + // TLS_CIPHER_SM4_CCM_IV_SIZE kernelCryptoCipherType = 8 + // TLS_CIPHER_SM4_CCM_KEY_SIZE kernelCryptoCipherType = 16 + // TLS_CIPHER_SM4_CCM_SALT_SIZE kernelCryptoCipherType = 4 + // TLS_CIPHER_SM4_CCM_TAG_SIZE kernelCryptoCipherType = 16 + // TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + TLS_CIPHER_ARIA_GCM_128 kernelCryptoCipherType = 57 + TLS_CIPHER_ARIA_GCM_128_IV_SIZE kernelCryptoCipherType = 8 + TLS_CIPHER_ARIA_GCM_128_KEY_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_ARIA_GCM_128_SALT_SIZE kernelCryptoCipherType = 4 + TLS_CIPHER_ARIA_GCM_128_TAG_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8 + + TLS_CIPHER_ARIA_GCM_256 kernelCryptoCipherType = 58 + TLS_CIPHER_ARIA_GCM_256_IV_SIZE kernelCryptoCipherType = 8 + TLS_CIPHER_ARIA_GCM_256_KEY_SIZE kernelCryptoCipherType = 32 + TLS_CIPHER_ARIA_GCM_256_SALT_SIZE kernelCryptoCipherType = 4 + TLS_CIPHER_ARIA_GCM_256_TAG_SIZE kernelCryptoCipherType = 16 + TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8 +) + +type kernelCrypto interface { + String() string +} + +type kernelCryptoInfo struct { + version uint16 + cipher_type kernelCryptoCipherType +} + +var _ kernelCrypto = &kernelCryptoAES128GCM{} + +type kernelCryptoAES128GCM struct { + kernelCryptoInfo + iv [TLS_CIPHER_AES_GCM_128_IV_SIZE]byte + key [TLS_CIPHER_AES_GCM_128_KEY_SIZE]byte + salt [TLS_CIPHER_AES_GCM_128_SALT_SIZE]byte + rec_seq [TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE]byte +} + +func (crypto *kernelCryptoAES128GCM) String() string { + crypto.cipher_type = TLS_CIPHER_AES_GCM_128 + return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +} + +var _ kernelCrypto = &kernelCryptoAES256GCM{} + +type kernelCryptoAES256GCM struct { + kernelCryptoInfo + iv [TLS_CIPHER_AES_GCM_256_IV_SIZE]byte + key [TLS_CIPHER_AES_GCM_256_KEY_SIZE]byte + salt [TLS_CIPHER_AES_GCM_256_SALT_SIZE]byte + rec_seq [TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE]byte +} + +func (crypto *kernelCryptoAES256GCM) String() string { + crypto.cipher_type = TLS_CIPHER_AES_GCM_256 + return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +} + +var _ kernelCrypto = &kernelCryptoAES128CCM{} + +type kernelCryptoAES128CCM struct { + kernelCryptoInfo + iv [TLS_CIPHER_AES_CCM_128_IV_SIZE]byte + key [TLS_CIPHER_AES_CCM_128_KEY_SIZE]byte + salt [TLS_CIPHER_AES_CCM_128_SALT_SIZE]byte + rec_seq [TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE]byte +} + +func (crypto *kernelCryptoAES128CCM) String() string { + crypto.cipher_type = TLS_CIPHER_AES_CCM_128 + return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +} + +var _ kernelCrypto = &kernelCryptoChacha20Poly1035{} + +type kernelCryptoChacha20Poly1035 struct { + kernelCryptoInfo + iv [TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE]byte + key [TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE]byte + salt [TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE]byte + rec_seq [TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE]byte +} + +func (crypto *kernelCryptoChacha20Poly1035) String() string { + crypto.cipher_type = TLS_CIPHER_CHACHA20_POLY1305 + return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +} + +// var _ kernelCrypto = &kernelCryptoSM4GCM{} + +// type kernelCryptoSM4GCM struct { +// kernelCryptoInfo +// iv [TLS_CIPHER_SM4_GCM_IV_SIZE]byte +// key [TLS_CIPHER_SM4_GCM_KEY_SIZE]byte +// salt [TLS_CIPHER_SM4_GCM_SALT_SIZE]byte +// rec_seq [TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE]byte +// } + +// func (crypto *kernelCryptoSM4GCM) String() string { +// crypto.cipher_type = TLS_CIPHER_SM4_GCM +// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +// } + +// var _ kernelCrypto = &kernelCryptoSM4CCM{} + +// type kernelCryptoSM4CCM struct { +// kernelCryptoInfo +// iv [TLS_CIPHER_SM4_CCM_IV_SIZE]byte +// key [TLS_CIPHER_SM4_CCM_KEY_SIZE]byte +// salt [TLS_CIPHER_SM4_CCM_SALT_SIZE]byte +// rec_seq [TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE]byte +// } + +// func (crypto *kernelCryptoSM4CCM) String() string { +// crypto.cipher_type = TLS_CIPHER_SM4_CCM +// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +// } + +var _ kernelCrypto = &kernelCryptoARIA128GCM{} + +type kernelCryptoARIA128GCM struct { + kernelCryptoInfo + iv [TLS_CIPHER_ARIA_GCM_128_IV_SIZE]byte + key [TLS_CIPHER_ARIA_GCM_128_KEY_SIZE]byte + salt [TLS_CIPHER_ARIA_GCM_128_SALT_SIZE]byte + rec_seq [TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE]byte +} + +func (crypto *kernelCryptoARIA128GCM) String() string { + crypto.cipher_type = TLS_CIPHER_ARIA_GCM_128 + return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +} + +var _ kernelCrypto = &kernelCryptoARIA256GCM{} + +type kernelCryptoARIA256GCM struct { + kernelCryptoInfo + iv [TLS_CIPHER_ARIA_GCM_256_IV_SIZE]byte + key [TLS_CIPHER_ARIA_GCM_256_KEY_SIZE]byte + salt [TLS_CIPHER_ARIA_GCM_256_SALT_SIZE]byte + rec_seq [TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE]byte +} + +func (crypto *kernelCryptoARIA256GCM) String() string { + crypto.cipher_type = TLS_CIPHER_ARIA_GCM_256 + return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) +} + +func kernelCipher(kernel *Support, hc *badtls.RawHalfConn, cipherSuite uint16, isRX bool) kernelCrypto { + if !kernel.TLS { + return nil + } + + switch *hc.Version { + case tls.VersionTLS12: + if isRX && !kernel.TLS_Version13_RX { + return nil + } + + case tls.VersionTLS13: + if !kernel.TLS_Version13 { + return nil + } + + if isRX && !kernel.TLS_Version13_RX { + return nil + } + + default: + return nil + } + + var key, iv []byte + if *hc.Version == tls.VersionTLS13 { + key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), *hc.TrafficSecret) + /*if isRX { + key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.RemoteTrafficSecret) + } else { + key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.TrafficSecret) + }*/ + } else { + // csPtr := cipherSuiteByID(cipherSuite) + // keysFromMasterSecret(*hc.Version, csPtr, keyLog.Secret, keyLog.Random) + return nil + } + + switch cipherSuite { + case tls.TLS_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: + crypto := new(kernelCryptoAES128GCM) + + crypto.version = *hc.Version + copy(crypto.key[:], key) + copy(crypto.iv[:], iv[4:]) + copy(crypto.salt[:], iv[:4]) + crypto.rec_seq = *hc.Seq + + return crypto + case tls.TLS_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: + if !kernel.TLS_AES_256_GCM { + return nil + } + + crypto := new(kernelCryptoAES256GCM) + + crypto.version = *hc.Version + copy(crypto.key[:], key) + copy(crypto.iv[:], iv[4:]) + copy(crypto.salt[:], iv[:4]) + crypto.rec_seq = *hc.Seq + + return crypto + //case tls.TLS_AES_128_CCM_SHA256, tls.TLS_RSA_WITH_AES_128_CCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_SHA256: + // if !kernel.TLS_AES_128_CCM { + // return nil + // } + // + // crypto := new(kernelCryptoAES128CCM) + // + // crypto.version = *hc.Version + // copy(crypto.key[:], key) + // copy(crypto.iv[:], iv[4:]) + // copy(crypto.salt[:], iv[:4]) + // crypto.rec_seq = *hc.Seq + // + // return crypto + case tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: + if !kernel.TLS_CHACHA20_POLY1305 { + return nil + } + + crypto := new(kernelCryptoChacha20Poly1035) + + crypto.version = *hc.Version + copy(crypto.key[:], key) + copy(crypto.iv[:], iv) + crypto.rec_seq = *hc.Seq + + return crypto + //case tls.TLS_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256: + // if !kernel.TLS_ARIA_GCM { + // return nil + // } + // + // crypto := new(kernelCryptoARIA128GCM) + // + // crypto.version = *hc.Version + // copy(crypto.key[:], key) + // copy(crypto.iv[:], iv[4:]) + // copy(crypto.salt[:], iv[:4]) + // crypto.rec_seq = *hc.Seq + // + // return crypto + //case tls.TLS_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384: + // if !kernel.TLS_ARIA_GCM { + // return nil + // } + // + // crypto := new(kernelCryptoARIA256GCM) + // + // crypto.version = *hc.Version + // copy(crypto.key[:], key) + // copy(crypto.iv[:], iv[4:]) + // copy(crypto.salt[:], iv[:4]) + // crypto.rec_seq = *hc.Seq + // + // return crypto + default: + return nil + } +} diff --git a/common/ktls/ktls_close.go b/common/ktls/ktls_close.go new file mode 100644 index 00000000..2052524d --- /dev/null +++ b/common/ktls/ktls_close.go @@ -0,0 +1,67 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "fmt" + "net" + "time" +) + +func (c *Conn) Close() error { + if !c.kernelTx { + return c.Conn.Close() + } + + // Interlock with Conn.Write above. + var x int32 + for { + x = c.rawConn.ActiveCall.Load() + if x&1 != 0 { + return net.ErrClosed + } + if c.rawConn.ActiveCall.CompareAndSwap(x, x|1) { + break + } + } + if x != 0 { + // io.Writer and io.Closer should not be used concurrently. + // If Close is called while a Write is currently in-flight, + // interpret that as a sign that this Close is really just + // being used to break the Write and/or clean up resources and + // avoid sending the alertCloseNotify, which may block + // waiting on handshakeMutex or the c.out mutex. + return c.conn.Close() + } + + var alertErr error + if c.rawConn.IsHandshakeComplete.Load() { + if err := c.closeNotify(); err != nil { + alertErr = fmt.Errorf("tls: failed to send closeNotify alert (but connection was closed anyway): %w", err) + } + } + + if err := c.conn.Close(); err != nil { + return err + } + return alertErr +} + +func (c *Conn) closeNotify() error { + c.rawConn.Out.Lock() + defer c.rawConn.Out.Unlock() + + if !*c.rawConn.CloseNotifySent { + // Set a Write Deadline to prevent possibly blocking forever. + c.SetWriteDeadline(time.Now().Add(time.Second * 5)) + *c.rawConn.CloseNotifyErr = c.sendAlertLocked(alertCloseNotify) + *c.rawConn.CloseNotifySent = true + // Any subsequent writes will fail. + c.SetWriteDeadline(time.Now()) + } + return *c.rawConn.CloseNotifyErr +} diff --git a/common/ktls/ktls_const.go b/common/ktls/ktls_const.go new file mode 100644 index 00000000..40cff760 --- /dev/null +++ b/common/ktls/ktls_const.go @@ -0,0 +1,24 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +const ( + maxPlaintext = 16384 // maximum plaintext payload length + maxCiphertext = 16384 + 2048 // maximum ciphertext payload length + maxCiphertextTLS13 = 16384 + 256 // maximum ciphertext length in TLS 1.3 + recordHeaderLen = 5 // record header length + maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB) + maxHandshakeCertificateMsg = 262144 // maximum certificate message size (256 KiB) + maxUselessRecords = 16 // maximum number of consecutive non-advancing records +) + +const ( + recordTypeChangeCipherSpec = 20 + recordTypeAlert = 21 + recordTypeHandshake = 22 + recordTypeApplicationData = 23 +) diff --git a/common/ktls/ktls_handshake_messages.go b/common/ktls/ktls_handshake_messages.go new file mode 100644 index 00000000..f44958c0 --- /dev/null +++ b/common/ktls/ktls_handshake_messages.go @@ -0,0 +1,238 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "fmt" + + "golang.org/x/crypto/cryptobyte" +) + +// The marshalingFunction type is an adapter to allow the use of ordinary +// functions as cryptobyte.MarshalingValue. +type marshalingFunction func(b *cryptobyte.Builder) error + +func (f marshalingFunction) Marshal(b *cryptobyte.Builder) error { + return f(b) +} + +// addBytesWithLength appends a sequence of bytes to the cryptobyte.Builder. If +// the length of the sequence is not the value specified, it produces an error. +func addBytesWithLength(b *cryptobyte.Builder, v []byte, n int) { + b.AddValue(marshalingFunction(func(b *cryptobyte.Builder) error { + if len(v) != n { + return fmt.Errorf("invalid value length: expected %d, got %d", n, len(v)) + } + b.AddBytes(v) + return nil + })) +} + +// addUint64 appends a big-endian, 64-bit value to the cryptobyte.Builder. +func addUint64(b *cryptobyte.Builder, v uint64) { + b.AddUint32(uint32(v >> 32)) + b.AddUint32(uint32(v)) +} + +// readUint64 decodes a big-endian, 64-bit value into out and advances over it. +// It reports whether the read was successful. +func readUint64(s *cryptobyte.String, out *uint64) bool { + var hi, lo uint32 + if !s.ReadUint32(&hi) || !s.ReadUint32(&lo) { + return false + } + *out = uint64(hi)<<32 | uint64(lo) + return true +} + +// readUint8LengthPrefixed acts like s.ReadUint8LengthPrefixed, but targets a +// []byte instead of a cryptobyte.String. +func readUint8LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { + return s.ReadUint8LengthPrefixed((*cryptobyte.String)(out)) +} + +// readUint16LengthPrefixed acts like s.ReadUint16LengthPrefixed, but targets a +// []byte instead of a cryptobyte.String. +func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { + return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out)) +} + +// readUint24LengthPrefixed acts like s.ReadUint24LengthPrefixed, but targets a +// []byte instead of a cryptobyte.String. +func readUint24LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { + return s.ReadUint24LengthPrefixed((*cryptobyte.String)(out)) +} + +type keyUpdateMsg struct { + updateRequested bool +} + +func (m *keyUpdateMsg) marshal() ([]byte, error) { + var b cryptobyte.Builder + b.AddUint8(typeKeyUpdate) + b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { + if m.updateRequested { + b.AddUint8(1) + } else { + b.AddUint8(0) + } + }) + + return b.Bytes() +} + +func (m *keyUpdateMsg) unmarshal(data []byte) bool { + s := cryptobyte.String(data) + + var updateRequested uint8 + if !s.Skip(4) || // message type and uint24 length field + !s.ReadUint8(&updateRequested) || !s.Empty() { + return false + } + switch updateRequested { + case 0: + m.updateRequested = false + case 1: + m.updateRequested = true + default: + return false + } + return true +} + +// TLS handshake message types. +const ( + typeHelloRequest uint8 = 0 + typeClientHello uint8 = 1 + typeServerHello uint8 = 2 + typeNewSessionTicket uint8 = 4 + typeEndOfEarlyData uint8 = 5 + typeEncryptedExtensions uint8 = 8 + typeCertificate uint8 = 11 + typeServerKeyExchange uint8 = 12 + typeCertificateRequest uint8 = 13 + typeServerHelloDone uint8 = 14 + typeCertificateVerify uint8 = 15 + typeClientKeyExchange uint8 = 16 + typeFinished uint8 = 20 + typeCertificateStatus uint8 = 22 + typeKeyUpdate uint8 = 24 + typeCompressedCertificate uint8 = 25 + typeMessageHash uint8 = 254 // synthetic message +) + +// TLS compression types. +const ( + compressionNone uint8 = 0 +) + +// TLS extension numbers +const ( + extensionServerName uint16 = 0 + extensionStatusRequest uint16 = 5 + extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7 + extensionSupportedPoints uint16 = 11 + extensionSignatureAlgorithms uint16 = 13 + extensionALPN uint16 = 16 + extensionSCT uint16 = 18 + extensionPadding uint16 = 21 + extensionExtendedMasterSecret uint16 = 23 + extensionCompressCertificate uint16 = 27 // compress_certificate in TLS 1.3 + extensionSessionTicket uint16 = 35 + extensionPreSharedKey uint16 = 41 + extensionEarlyData uint16 = 42 + extensionSupportedVersions uint16 = 43 + extensionCookie uint16 = 44 + extensionPSKModes uint16 = 45 + extensionCertificateAuthorities uint16 = 47 + extensionSignatureAlgorithmsCert uint16 = 50 + extensionKeyShare uint16 = 51 + extensionQUICTransportParameters uint16 = 57 + extensionALPS uint16 = 17513 + extensionRenegotiationInfo uint16 = 0xff01 + extensionECHOuterExtensions uint16 = 0xfd00 + extensionEncryptedClientHello uint16 = 0xfe0d +) + +type handshakeMessage interface { + marshal() ([]byte, error) + unmarshal([]byte) bool +} +type newSessionTicketMsgTLS13 struct { + lifetime uint32 + ageAdd uint32 + nonce []byte + label []byte + maxEarlyData uint32 +} + +func (m *newSessionTicketMsgTLS13) marshal() ([]byte, error) { + var b cryptobyte.Builder + b.AddUint8(typeNewSessionTicket) + b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddUint32(m.lifetime) + b.AddUint32(m.ageAdd) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(m.nonce) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(m.label) + }) + + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + if m.maxEarlyData > 0 { + b.AddUint16(extensionEarlyData) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddUint32(m.maxEarlyData) + }) + } + }) + }) + + return b.Bytes() +} + +func (m *newSessionTicketMsgTLS13) unmarshal(data []byte) bool { + *m = newSessionTicketMsgTLS13{} + s := cryptobyte.String(data) + + var extensions cryptobyte.String + if !s.Skip(4) || // message type and uint24 length field + !s.ReadUint32(&m.lifetime) || + !s.ReadUint32(&m.ageAdd) || + !readUint8LengthPrefixed(&s, &m.nonce) || + !readUint16LengthPrefixed(&s, &m.label) || + !s.ReadUint16LengthPrefixed(&extensions) || + !s.Empty() { + return false + } + + for !extensions.Empty() { + var extension uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&extension) || + !extensions.ReadUint16LengthPrefixed(&extData) { + return false + } + + switch extension { + case extensionEarlyData: + if !extData.ReadUint32(&m.maxEarlyData) { + return false + } + default: + // Ignore unknown extensions. + continue + } + + if !extData.Empty() { + return false + } + } + + return true +} diff --git a/common/ktls/ktls_key_update.go b/common/ktls/ktls_key_update.go new file mode 100644 index 00000000..35268e8f --- /dev/null +++ b/common/ktls/ktls_key_update.go @@ -0,0 +1,173 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "crypto/tls" + "errors" + "fmt" + "io" + "os" +) + +// handlePostHandshakeMessage processes a handshake message arrived after the +// handshake is complete. Up to TLS 1.2, it indicates the start of a renegotiation. +func (c *Conn) handlePostHandshakeMessage() error { + if *c.rawConn.Vers != tls.VersionTLS13 { + return errors.New("ktls: kernel does not support TLS 1.2 renegotiation") + } + + msg, err := c.readHandshake(nil) + if err != nil { + return err + } + //c.retryCount++ + //if c.retryCount > maxUselessRecords { + // c.sendAlert(alertUnexpectedMessage) + // return c.in.setErrorLocked(errors.New("tls: too many non-advancing records")) + //} + + switch msg := msg.(type) { + case *newSessionTicketMsgTLS13: + // return errors.New("ktls: received new session ticket") + return nil + case *keyUpdateMsg: + return c.handleKeyUpdate(msg) + } + // The QUIC layer is supposed to treat an unexpected post-handshake CertificateRequest + // as a QUIC-level PROTOCOL_VIOLATION error (RFC 9001, Section 4.4). Returning an + // unexpected_message alert here doesn't provide it with enough information to distinguish + // this condition from other unexpected messages. This is probably fine. + c.sendAlert(alertUnexpectedMessage) + return fmt.Errorf("tls: received unexpected handshake message of type %T", msg) +} + +func (c *Conn) handleKeyUpdate(keyUpdate *keyUpdateMsg) error { + //if c.quic != nil { + // c.sendAlert(alertUnexpectedMessage) + // return c.in.setErrorLocked(errors.New("tls: received unexpected key update message")) + //} + + cipherSuite := cipherSuiteTLS13ByID(*c.rawConn.CipherSuite) + if cipherSuite == nil { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertInternalError)) + } + + newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.In.TrafficSecret) + c.rawConn.In.SetTrafficSecret(cipherSuite, 0 /*tls.QUICEncryptionLevelInitial*/, newSecret) + + err := c.resetupRX() + if err != nil { + c.sendAlert(alertInternalError) + return c.rawConn.In.SetErrorLocked(fmt.Errorf("ktls: resetupRX failed: %w", err)) + } + + if keyUpdate.updateRequested { + c.rawConn.Out.Lock() + defer c.rawConn.Out.Unlock() + + resetup, err := c.resetupTX() + if err != nil { + c.sendAlertLocked(alertInternalError) + return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err)) + } + + msg := &keyUpdateMsg{} + msgBytes, err := msg.marshal() + if err != nil { + return err + } + _, err = c.writeRecordLocked(recordTypeHandshake, msgBytes) + if err != nil { + // Surface the error at the next write. + c.rawConn.Out.SetErrorLocked(err) + return nil + } + + newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.Out.TrafficSecret) + c.rawConn.Out.SetTrafficSecret(cipherSuite, 0 /*QUICEncryptionLevelInitial*/, newSecret) + + err = resetup() + if err != nil { + return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err)) + } + } + + return nil +} + +func (c *Conn) readHandshakeBytes(n int) error { + //if c.quic != nil { + // return c.quicReadHandshakeBytes(n) + //} + for c.rawConn.Hand.Len() < n { + if err := c.readRecord(); err != nil { + return err + } + } + return nil +} + +func (c *Conn) readHandshake(transcript io.Writer) (any, error) { + if err := c.readHandshakeBytes(4); err != nil { + return nil, err + } + data := c.rawConn.Hand.Bytes() + + maxHandshakeSize := maxHandshake + // hasVers indicates we're past the first message, forcing someone trying to + // make us just allocate a large buffer to at least do the initial part of + // the handshake first. + //if c.haveVers && data[0] == typeCertificate { + // Since certificate messages are likely to be the only messages that + // can be larger than maxHandshake, we use a special limit for just + // those messages. + //maxHandshakeSize = maxHandshakeCertificateMsg + //} + + n := int(data[1])<<16 | int(data[2])<<8 | int(data[3]) + if n > maxHandshakeSize { + c.sendAlertLocked(alertInternalError) + return nil, c.rawConn.In.SetErrorLocked(fmt.Errorf("tls: handshake message of length %d bytes exceeds maximum of %d bytes", n, maxHandshakeSize)) + } + if err := c.readHandshakeBytes(4 + n); err != nil { + return nil, err + } + data = c.rawConn.Hand.Next(4 + n) + return c.unmarshalHandshakeMessage(data, transcript) +} + +func (c *Conn) unmarshalHandshakeMessage(data []byte, transcript io.Writer) (any, error) { + var m handshakeMessage + switch data[0] { + case typeNewSessionTicket: + if *c.rawConn.Vers == tls.VersionTLS13 { + m = new(newSessionTicketMsgTLS13) + } else { + return nil, os.ErrInvalid + } + case typeKeyUpdate: + m = new(keyUpdateMsg) + default: + return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + + // The handshake message unmarshalers + // expect to be able to keep references to data, + // so pass in a fresh copy that won't be overwritten. + data = append([]byte(nil), data...) + + if !m.unmarshal(data) { + return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError)) + } + + if transcript != nil { + transcript.Write(data) + } + + return m, nil +} diff --git a/common/ktls/ktls_linux.go b/common/ktls/ktls_linux.go new file mode 100644 index 00000000..1e327751 --- /dev/null +++ b/common/ktls/ktls_linux.go @@ -0,0 +1,329 @@ +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "crypto/tls" + "errors" + "io" + "os" + "strings" + "sync" + "syscall" + "unsafe" + + "github.com/sagernet/sing-box/common/badversion" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/shell" + + "golang.org/x/sys/unix" +) + +// mod from https://gitlab.com/go-extension/tls + +const ( + TLS_TX = 1 + TLS_RX = 2 + TLS_TX_ZEROCOPY_RO = 3 // TX zerocopy (only sendfile now) + TLS_RX_EXPECT_NO_PAD = 4 // Attempt opportunistic zero-copy, TLS 1.3 only + + TLS_SET_RECORD_TYPE = 1 + TLS_GET_RECORD_TYPE = 2 +) + +type Support struct { + TLS, TLS_RX bool + TLS_Version13, TLS_Version13_RX bool + + TLS_TX_ZEROCOPY bool + TLS_RX_NOPADDING bool + + TLS_AES_256_GCM bool + TLS_AES_128_CCM bool + TLS_CHACHA20_POLY1305 bool + TLS_SM4 bool + TLS_ARIA_GCM bool + + TLS_Version13_KeyUpdate bool +} + +var KernelSupport = sync.OnceValues(func() (*Support, error) { + var uname unix.Utsname + err := unix.Uname(&uname) + if err != nil { + return nil, err + } + + kernelVersion := badversion.Parse(strings.Trim(string(uname.Release[:]), "\x00")) + if err != nil { + return nil, err + } + var support Support + switch { + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 14}): + support.TLS_Version13_KeyUpdate = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 1}): + support.TLS_ARIA_GCM = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6}): + support.TLS_Version13_RX = true + support.TLS_RX_NOPADDING = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 19}): + support.TLS_TX_ZEROCOPY = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 16}): + support.TLS_SM4 = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 11}): + support.TLS_CHACHA20_POLY1305 = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 2}): + support.TLS_AES_128_CCM = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 1}): + support.TLS_AES_256_GCM = true + support.TLS_Version13 = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 17}): + support.TLS_RX = true + fallthrough + case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 13}): + support.TLS = true + } + + if support.TLS && support.TLS_Version13 { + _, err := os.Stat("/sys/module/tls") + if err != nil { + if os.Getuid() == 0 { + output, err := shell.Exec("modprobe", "tls").Read() + if err != nil { + return nil, E.Extend(E.Cause(err, "modprobe tls"), output) + } + } else { + return nil, E.New("ktls: kernel TLS module not loaded") + } + } + } + + return &support, nil +}) + +func Load() error { + support, err := KernelSupport() + if err != nil { + return E.Cause(err, "ktls: check availability") + } + if !support.TLS || !support.TLS_Version13 { + return E.New("ktls: kernel does not support TLS 1.3") + } + return nil +} + +func (c *Conn) setupKernel(txOffload, rxOffload bool) error { + if !txOffload && !rxOffload { + return os.ErrInvalid + } + support, err := KernelSupport() + if err != nil { + return E.Cause(err, "check availability") + } + if !support.TLS || !support.TLS_Version13 { + return E.New("kernel does not support TLS 1.3") + } + c.rawConn.Out.Lock() + defer c.rawConn.Out.Unlock() + err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptString(int(fd), unix.SOL_TCP, unix.TCP_ULP, "tls") + }) + if err != nil { + return os.NewSyscallError("setsockopt", err) + } + + if txOffload { + txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false) + if txCrypto == nil { + return E.New("unsupported cipher suite") + } + err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String()) + }) + if err != nil { + return err + } + if support.TLS_TX_ZEROCOPY { + err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_TX_ZEROCOPY_RO, 1) + }) + if err != nil { + return err + } + } + c.kernelTx = true + c.logger.DebugContext(c.ctx, "ktls: kernel TLS TX enabled") + } + + if rxOffload { + rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true) + if rxCrypto == nil { + return E.New("unsupported cipher suite") + } + err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String()) + }) + if err != nil { + return err + } + if *c.rawConn.Vers >= tls.VersionTLS13 && support.TLS_RX_NOPADDING { + err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_RX_EXPECT_NO_PAD, 1) + }) + if err != nil { + return err + } + } + c.kernelRx = true + c.logger.DebugContext(c.ctx, "ktls: kernel TLS RX enabled") + } + return nil +} + +func (c *Conn) resetupTX() (func() error, error) { + if !c.kernelTx { + return nil, nil + } + support, err := KernelSupport() + if err != nil { + return nil, err + } + if !support.TLS_Version13_KeyUpdate { + return nil, errors.New("ktls: kernel does not support rekey") + } + txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false) + if txCrypto == nil { + return nil, errors.New("ktls: set kernelCipher on unsupported tls session") + } + return func() error { + return control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String()) + }) + }, nil +} + +func (c *Conn) resetupRX() error { + if !c.kernelRx { + return nil + } + support, err := KernelSupport() + if err != nil { + return err + } + if !support.TLS_Version13_KeyUpdate { + return errors.New("ktls: kernel does not support rekey") + } + rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true) + if rxCrypto == nil { + return errors.New("ktls: set kernelCipher on unsupported tls session") + } + return control.Raw(c.rawSyscallConn, func(fd uintptr) error { + return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String()) + }) +} + +func (c *Conn) readKernelRecord() (uint8, []byte, error) { + if c.rawConn.RawInput.Len() < maxPlaintext { + c.rawConn.RawInput.Grow(maxPlaintext - c.rawConn.RawInput.Len()) + } + + data := c.rawConn.RawInput.Bytes()[:maxPlaintext] + + // cmsg for record type + buffer := make([]byte, unix.CmsgSpace(1)) + cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0])) + cmsg.SetLen(unix.CmsgLen(1)) + + var iov unix.Iovec + iov.Base = &data[0] + iov.SetLen(len(data)) + + var msg unix.Msghdr + msg.Control = &buffer[0] + msg.Controllen = cmsg.Len + msg.Iov = &iov + msg.Iovlen = 1 + + var n int + var err error + er := c.rawSyscallConn.Read(func(fd uintptr) bool { + n, err = recvmsg(int(fd), &msg, 0) + return err != unix.EAGAIN || c.pendingRxSplice + }) + if er != nil { + return 0, nil, er + } + switch err { + case nil: + case syscall.EINVAL, syscall.EAGAIN: + return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertProtocolVersion)) + case syscall.EMSGSIZE: + return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow)) + case syscall.EBADMSG: + return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecryptError)) + default: + return 0, nil, err + } + + if n <= 0 { + return 0, nil, c.rawConn.In.SetErrorLocked(io.EOF) + } + + if cmsg.Level == unix.SOL_TLS && cmsg.Type == TLS_GET_RECORD_TYPE { + typ := buffer[unix.CmsgLen(0)] + return typ, data[:n], nil + } + + return recordTypeApplicationData, data[:n], nil +} + +func (c *Conn) writeKernelRecord(typ uint16, data []byte) (int, error) { + if typ == recordTypeApplicationData { + return c.conn.Write(data) + } + + // cmsg for record type + buffer := make([]byte, unix.CmsgSpace(1)) + cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0])) + cmsg.SetLen(unix.CmsgLen(1)) + buffer[unix.CmsgLen(0)] = byte(typ) + cmsg.Level = unix.SOL_TLS + cmsg.Type = TLS_SET_RECORD_TYPE + + var iov unix.Iovec + iov.Base = &data[0] + iov.SetLen(len(data)) + + var msg unix.Msghdr + msg.Control = &buffer[0] + msg.Controllen = cmsg.Len + msg.Iov = &iov + msg.Iovlen = 1 + + var n int + var err error + ew := c.rawSyscallConn.Write(func(fd uintptr) bool { + n, err = sendmsg(int(fd), &msg, 0) + return err != unix.EAGAIN + }) + if ew != nil { + return 0, ew + } + return n, err +} + +//go:linkname recvmsg golang.org/x/sys/unix.recvmsg +func recvmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error) + +//go:linkname sendmsg golang.org/x/sys/unix.sendmsg +func sendmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error) diff --git a/common/ktls/ktls_prf.go b/common/ktls/ktls_prf.go new file mode 100644 index 00000000..ecf0b735 --- /dev/null +++ b/common/ktls/ktls_prf.go @@ -0,0 +1,24 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import "unsafe" + +//go:linkname cipherSuiteByID github.com/metacubex/utls.cipherSuiteByID +func cipherSuiteByID(id uint16) unsafe.Pointer + +//go:linkname keysFromMasterSecret github.com/metacubex/utls.keysFromMasterSecret +func keysFromMasterSecret(version uint16, suite unsafe.Pointer, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte) + +//go:linkname cipherSuiteTLS13ByID github.com/metacubex/utls.cipherSuiteTLS13ByID +func cipherSuiteTLS13ByID(id uint16) unsafe.Pointer + +//go:linkname nextTrafficSecret github.com/metacubex/utls.(*cipherSuiteTLS13).nextTrafficSecret +func nextTrafficSecret(cs unsafe.Pointer, trafficSecret []byte) []byte + +//go:linkname trafficKey github.com/metacubex/utls.(*cipherSuiteTLS13).trafficKey +func trafficKey(cs unsafe.Pointer, trafficSecret []byte) (key, iv []byte) diff --git a/common/ktls/ktls_read.go b/common/ktls/ktls_read.go new file mode 100644 index 00000000..5609bfb5 --- /dev/null +++ b/common/ktls/ktls_read.go @@ -0,0 +1,293 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "unsafe" +) + +func (c *Conn) Read(b []byte) (int, error) { + if !c.kernelRx { + return c.Conn.Read(b) + } + + if len(b) == 0 { + // Put this after Handshake, in case people were calling + // Read(nil) for the side effect of the Handshake. + return 0, nil + } + + c.rawConn.In.Lock() + defer c.rawConn.In.Unlock() + + for c.rawConn.Input.Len() == 0 { + if err := c.readRecord(); err != nil { + return 0, err + } + for c.rawConn.Hand.Len() > 0 { + if err := c.handlePostHandshakeMessage(); err != nil { + return 0, err + } + } + } + + n, _ := c.rawConn.Input.Read(b) + + // If a close-notify alert is waiting, read it so that we can return (n, + // EOF) instead of (n, nil), to signal to the HTTP response reading + // goroutine that the connection is now closed. This eliminates a race + // where the HTTP response reading goroutine would otherwise not observe + // the EOF until its next read, by which time a client goroutine might + // have already tried to reuse the HTTP connection for a new request. + // See https://golang.org/cl/76400046 and https://golang.org/issue/3514 + if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.RawInput.Len() > 0 && + c.rawConn.RawInput.Bytes()[0] == recordTypeAlert { + if err := c.readRecord(); err != nil { + return n, err // will be io.EOF on closeNotify + } + } + + return n, nil +} + +func (c *Conn) readRecord() error { + if *c.rawConn.In.Err != nil { + return *c.rawConn.In.Err + } + + typ, data, err := c.readRawRecord() + if err != nil { + return err + } + + if len(data) > maxPlaintext { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow)) + } + + // Application Data messages are always protected. + if c.rawConn.In.Cipher == nil && typ == recordTypeApplicationData { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + + //if typ != recordTypeAlert && typ != recordTypeChangeCipherSpec && len(data) > 0 { + // This is a state-advancing message: reset the retry count. + // c.retryCount = 0 + //} + + // Handshake messages MUST NOT be interleaved with other record types in TLS 1.3. + if *c.rawConn.Vers == tls.VersionTLS13 && typ != recordTypeHandshake && c.rawConn.Hand.Len() > 0 { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + + switch typ { + default: + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + case recordTypeAlert: + //if c.quic != nil { + // return c.rawConn.In.setErrorLocked(c.sendAlert(alertUnexpectedMessage)) + //} + if len(data) != 2 { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + if data[1] == alertCloseNotify { + return c.rawConn.In.SetErrorLocked(io.EOF) + } + if *c.rawConn.Vers == tls.VersionTLS13 { + // TLS 1.3 removed warning-level alerts except for alertUserCanceled + // (RFC 8446, § 6.1). Since at least one major implementation + // (https://bugs.openjdk.org/browse/JDK-8323517) misuses this alert, + // many TLS stacks now ignore it outright when seen in a TLS 1.3 + // handshake (e.g. BoringSSL, NSS, Rustls). + if data[1] == alertUserCanceled { + // Like TLS 1.2 alertLevelWarning alerts, we drop the record and retry. + return c.retryReadRecord( /*expectChangeCipherSpec*/ ) + } + return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])}) + } + switch data[0] { + case alertLevelWarning: + // Drop the record on the floor and retry. + return c.retryReadRecord( /*expectChangeCipherSpec*/ ) + case alertLevelError: + return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])}) + default: + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + + case recordTypeChangeCipherSpec: + if len(data) != 1 || data[0] != 1 { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError)) + } + // Handshake messages are not allowed to fragment across the CCS. + if c.rawConn.Hand.Len() > 0 { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + // In TLS 1.3, change_cipher_spec records are ignored until the + // Finished. See RFC 8446, Appendix D.4. Note that according to Section + // 5, a server can send a ChangeCipherSpec before its ServerHello, when + // c.vers is still unset. That's not useful though and suspicious if the + // server then selects a lower protocol version, so don't allow that. + if *c.rawConn.Vers == tls.VersionTLS13 { + return c.retryReadRecord( /*expectChangeCipherSpec*/ ) + } + // if !expectChangeCipherSpec { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + //} + //if err := c.rawConn.In.changeCipherSpec(); err != nil { + // return c.rawConn.In.setErrorLocked(c.sendAlert(err.(alert))) + //} + + case recordTypeApplicationData: + // Some OpenSSL servers send empty records in order to randomize the + // CBC RawIV. Ignore a limited number of empty records. + if len(data) == 0 { + return c.retryReadRecord( /*expectChangeCipherSpec*/ ) + } + // Note that data is owned by c.rawInput, following the Next call above, + // to avoid copying the plaintext. This is safe because c.rawInput is + // not read from or written to until c.input is drained. + c.rawConn.Input.Reset(data) + case recordTypeHandshake: + if len(data) == 0 { + return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) + } + c.rawConn.Hand.Write(data) + } + + return nil +} + +//nolint:staticcheck +func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) { + // Read from kernel. + if c.kernelRx { + return c.readKernelRecord() + } + + // Read header, payload. + if err = c.readFromUntil(c.conn, recordHeaderLen); err != nil { + // RFC 8446, Section 6.1 suggests that EOF without an alertCloseNotify + // is an error, but popular web sites seem to do this, so we accept it + // if and only if at the record boundary. + if err == io.ErrUnexpectedEOF && c.rawConn.RawInput.Len() == 0 { + err = io.EOF + } + if e, ok := err.(net.Error); !ok || !e.Temporary() { + c.rawConn.In.SetErrorLocked(err) + } + return + } + hdr := c.rawConn.RawInput.Bytes()[:recordHeaderLen] + typ = hdr[0] + + vers := uint16(hdr[1])<<8 | uint16(hdr[2]) + expectedVers := *c.rawConn.Vers + if expectedVers == tls.VersionTLS13 { + // All TLS 1.3 records are expected to have 0x0303 (1.2) after + // the initial hello (RFC 8446 Section 5.1). + expectedVers = tls.VersionTLS12 + } + n := int(hdr[3])<<8 | int(hdr[4]) + if /*c.haveVers && */ vers != expectedVers { + c.sendAlert(alertProtocolVersion) + msg := fmt.Sprintf("received record with version %x when expecting version %x", vers, expectedVers) + err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg)) + return + } + //if !c.haveVers { + // // First message, be extra suspicious: this might not be a TLS + // // client. Bail out before reading a full 'body', if possible. + // // The current max version is 3.3 so if the version is >= 16.0, + // // it's probably not real. + // if (typ != recordTypeAlert && typ != recordTypeHandshake) || vers >= 0x1000 { + // err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(c.conn, "first record does not look like a TLS handshake")) + // return + // } + //} + if *c.rawConn.Vers == tls.VersionTLS13 && n > maxCiphertextTLS13 || n > maxCiphertext { + c.sendAlert(alertRecordOverflow) + msg := fmt.Sprintf("oversized record received with length %d", n) + err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg)) + return + } + if err = c.readFromUntil(c.conn, recordHeaderLen+n); err != nil { + if e, ok := err.(net.Error); !ok || !e.Temporary() { + c.rawConn.In.SetErrorLocked(err) + } + return + } + + // Process message. + record := c.rawConn.RawInput.Next(recordHeaderLen + n) + data, typ, err = c.rawConn.In.Decrypt(record) + if err != nil { + err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1]))) + return + } + return +} + +// retryReadRecord recurs into readRecordOrCCS to drop a non-advancing record, like +// a warning alert, empty application_data, or a change_cipher_spec in TLS 1.3. +func (c *Conn) retryReadRecord( /*expectChangeCipherSpec bool*/ ) error { + //c.retryCount++ + //if c.retryCount > maxUselessRecords { + // c.sendAlert(alertUnexpectedMessage) + // return c.in.setErrorLocked(errors.New("tls: too many ignored records")) + //} + return c.readRecord( /*expectChangeCipherSpec*/ ) +} + +// atLeastReader reads from R, stopping with EOF once at least N bytes have been +// read. It is different from an io.LimitedReader in that it doesn't cut short +// the last Read call, and in that it considers an early EOF an error. +type atLeastReader struct { + R io.Reader + N int64 +} + +func (r *atLeastReader) Read(p []byte) (int, error) { + if r.N <= 0 { + return 0, io.EOF + } + n, err := r.R.Read(p) + r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809 + if r.N > 0 && err == io.EOF { + return n, io.ErrUnexpectedEOF + } + if r.N <= 0 && err == nil { + return n, io.EOF + } + return n, err +} + +// readFromUntil reads from r into c.rawConn.RawInput until c.rawConn.RawInput contains +// at least n bytes or else returns an error. +func (c *Conn) readFromUntil(r io.Reader, n int) error { + if c.rawConn.RawInput.Len() >= n { + return nil + } + needs := n - c.rawConn.RawInput.Len() + // There might be extra input waiting on the wire. Make a best effort + // attempt to fetch it so that it can be used in (*Conn).Read to + // "predict" closeNotify alerts. + c.rawConn.RawInput.Grow(needs + bytes.MinRead) + _, err := c.rawConn.RawInput.ReadFrom(&atLeastReader{r, int64(needs)}) + return err +} + +func (c *Conn) newRecordHeaderError(conn net.Conn, msg string) (err tls.RecordHeaderError) { + err.Msg = msg + err.Conn = conn + copy(err.RecordHeader[:], c.rawConn.RawInput.Bytes()) + return err +} diff --git a/common/ktls/ktls_read_wait.go b/common/ktls/ktls_read_wait.go new file mode 100644 index 00000000..4b4edc1e --- /dev/null +++ b/common/ktls/ktls_read_wait.go @@ -0,0 +1,41 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "github.com/sagernet/sing/common/buf" + N "github.com/sagernet/sing/common/network" +) + +func (c *Conn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *Conn) WaitReadBuffer() (buffer *buf.Buffer, err error) { + c.rawConn.In.Lock() + defer c.rawConn.In.Unlock() + for c.rawConn.Input.Len() == 0 { + err = c.readRecord() + if err != nil { + return + } + } + buffer = c.readWaitOptions.NewBuffer() + n, err := c.rawConn.Input.Read(buffer.FreeBytes()) + if err != nil { + buffer.Release() + return + } + buffer.Truncate(n) + if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 && + c.rawConn.RawInput.Bytes()[0] == recordTypeAlert { + _ = c.rawConn.ReadRecord() + } + c.readWaitOptions.PostReturn(buffer) + return +} diff --git a/common/ktls/ktls_stub_nolinkname.go b/common/ktls/ktls_stub_nolinkname.go new file mode 100644 index 00000000..44a0b30c --- /dev/null +++ b/common/ktls/ktls_stub_nolinkname.go @@ -0,0 +1,15 @@ +//go:build linux && go1.25 && !badlinkname + +package ktls + +import ( + "context" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + aTLS "github.com/sagernet/sing/common/tls" +) + +func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { + return nil, E.New("kTLS requires build flags `badlinkname` and `-ldflags=-checklinkname=0`, please recompile your binary") +} diff --git a/common/ktls/ktls_stub_nonlinux.go b/common/ktls/ktls_stub_nonlinux.go new file mode 100644 index 00000000..e754b775 --- /dev/null +++ b/common/ktls/ktls_stub_nonlinux.go @@ -0,0 +1,15 @@ +//go:build !linux + +package ktls + +import ( + "context" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + aTLS "github.com/sagernet/sing/common/tls" +) + +func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { + return nil, E.New("kTLS is only supported on Linux") +} diff --git a/common/ktls/ktls_stub_oldgo.go b/common/ktls/ktls_stub_oldgo.go new file mode 100644 index 00000000..613bf7f1 --- /dev/null +++ b/common/ktls/ktls_stub_oldgo.go @@ -0,0 +1,15 @@ +//go:build linux && !go1.25 + +package ktls + +import ( + "context" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + aTLS "github.com/sagernet/sing/common/tls" +) + +func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { + return nil, E.New("kTLS requires Go 1.25 or later, please recompile your binary") +} diff --git a/common/ktls/ktls_write.go b/common/ktls/ktls_write.go new file mode 100644 index 00000000..76533b4a --- /dev/null +++ b/common/ktls/ktls_write.go @@ -0,0 +1,154 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && go1.25 && badlinkname + +package ktls + +import ( + "crypto/cipher" + "crypto/tls" + "errors" + "net" +) + +func (c *Conn) Write(b []byte) (int, error) { + if !c.kernelTx { + return c.Conn.Write(b) + } + // interlock with Close below + for { + x := c.rawConn.ActiveCall.Load() + if x&1 != 0 { + return 0, net.ErrClosed + } + if c.rawConn.ActiveCall.CompareAndSwap(x, x+2) { + break + } + } + defer c.rawConn.ActiveCall.Add(-2) + + //if err := c.Conn.HandshakeContext(context.Background()); err != nil { + // return 0, err + //} + + c.rawConn.Out.Lock() + defer c.rawConn.Out.Unlock() + + if err := *c.rawConn.Out.Err; err != nil { + return 0, err + } + + if !c.rawConn.IsHandshakeComplete.Load() { + return 0, tls.AlertError(alertInternalError) + } + + if *c.rawConn.CloseNotifySent { + // return 0, errShutdown + return 0, errors.New("tls: protocol is shutdown") + } + + // TLS 1.0 is susceptible to a chosen-plaintext + // attack when using block mode ciphers due to predictable IVs. + // This can be prevented by splitting each Application Data + // record into two records, effectively randomizing the RawIV. + // + // https://www.openssl.org/~bodo/tls-cbc.txt + // https://bugzilla.mozilla.org/show_bug.cgi?id=665814 + // https://www.imperialviolet.org/2012/01/15/beastfollowup.html + + var m int + if len(b) > 1 && *c.rawConn.Vers == tls.VersionTLS10 { + if _, ok := (*c.rawConn.Out.Cipher).(cipher.BlockMode); ok { + n, err := c.writeRecordLocked(recordTypeApplicationData, b[:1]) + if err != nil { + return n, c.rawConn.Out.SetErrorLocked(err) + } + m, b = 1, b[1:] + } + } + + n, err := c.writeRecordLocked(recordTypeApplicationData, b) + return n + m, c.rawConn.Out.SetErrorLocked(err) +} + +func (c *Conn) writeRecordLocked(typ uint16, data []byte) (n int, err error) { + if !c.kernelTx { + return c.rawConn.WriteRecordLocked(typ, data) + } + /*for len(data) > 0 { + m := len(data) + if maxPayload := c.maxPayloadSizeForWrite(typ); m > maxPayload { + m = maxPayload + } + _, err = c.writeKernelRecord(typ, data[:m]) + if err != nil { + return + } + n += m + data = data[m:] + }*/ + return c.writeKernelRecord(typ, data) +} + +const ( + // tcpMSSEstimate is a conservative estimate of the TCP maximum segment + // size (MSS). A constant is used, rather than querying the kernel for + // the actual MSS, to avoid complexity. The value here is the IPv6 + // minimum MTU (1280 bytes) minus the overhead of an IPv6 header (40 + // bytes) and a TCP header with timestamps (32 bytes). + tcpMSSEstimate = 1208 + + // recordSizeBoostThreshold is the number of bytes of application data + // sent after which the TLS record size will be increased to the + // maximum. + recordSizeBoostThreshold = 128 * 1024 +) + +func (c *Conn) maxPayloadSizeForWrite(typ uint16) int { + if /*c.config.DynamicRecordSizingDisabled ||*/ typ != recordTypeApplicationData { + return maxPlaintext + } + + if *c.rawConn.PacketsSent >= recordSizeBoostThreshold { + return maxPlaintext + } + + // Subtract TLS overheads to get the maximum payload size. + payloadBytes := tcpMSSEstimate - recordHeaderLen - c.rawConn.Out.ExplicitNonceLen() + if rawCipher := *c.rawConn.Out.Cipher; rawCipher != nil { + switch ciph := rawCipher.(type) { + case cipher.Stream: + payloadBytes -= (*c.rawConn.Out.Mac).Size() + case cipher.AEAD: + payloadBytes -= ciph.Overhead() + /*case cbcMode: + blockSize := ciph.BlockSize() + // The payload must fit in a multiple of blockSize, with + // room for at least one padding byte. + payloadBytes = (payloadBytes & ^(blockSize - 1)) - 1 + // The RawMac is appended before padding so affects the + // payload size directly. + payloadBytes -= c.out.mac.Size()*/ + default: + panic("unknown cipher type") + } + } + if *c.rawConn.Vers == tls.VersionTLS13 { + payloadBytes-- // encrypted ContentType + } + + // Allow packet growth in arithmetic progression up to max. + pkt := *c.rawConn.PacketsSent + *c.rawConn.PacketsSent++ + if pkt > 1000 { + return maxPlaintext // avoid overflow in multiply below + } + + n := payloadBytes * int(pkt+1) + if n > maxPlaintext { + n = maxPlaintext + } + return n +} diff --git a/common/listener/listener.go b/common/listener/listener.go new file mode 100644 index 00000000..cc27a62e --- /dev/null +++ b/common/listener/listener.go @@ -0,0 +1,172 @@ +package listener + +import ( + "context" + "net" + "net/netip" + "runtime" + "strings" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/settings" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/vishvananda/netns" +) + +type Listener struct { + ctx context.Context + logger logger.ContextLogger + network []string + listenOptions option.ListenOptions + connHandler adapter.ConnectionHandlerEx + packetHandler adapter.PacketHandlerEx + oobPacketHandler adapter.OOBPacketHandlerEx + threadUnsafePacketWriter bool + disablePacketOutput bool + setSystemProxy bool + systemProxySOCKS bool + tproxy bool + + tcpListener net.Listener + systemProxy settings.SystemProxy + udpConn *net.UDPConn + udpAddr M.Socksaddr + packetOutbound chan *N.PacketBuffer + packetOutboundClosed chan struct{} + shutdown atomic.Bool +} + +type Options struct { + Context context.Context + Logger logger.ContextLogger + Network []string + Listen option.ListenOptions + ConnectionHandler adapter.ConnectionHandlerEx + PacketHandler adapter.PacketHandlerEx + OOBPacketHandler adapter.OOBPacketHandlerEx + ThreadUnsafePacketWriter bool + DisablePacketOutput bool + SetSystemProxy bool + SystemProxySOCKS bool + TProxy bool +} + +func New( + options Options, +) *Listener { + return &Listener{ + ctx: options.Context, + logger: options.Logger, + network: options.Network, + listenOptions: options.Listen, + connHandler: options.ConnectionHandler, + packetHandler: options.PacketHandler, + oobPacketHandler: options.OOBPacketHandler, + threadUnsafePacketWriter: options.ThreadUnsafePacketWriter, + disablePacketOutput: options.DisablePacketOutput, + setSystemProxy: options.SetSystemProxy, + systemProxySOCKS: options.SystemProxySOCKS, + tproxy: options.TProxy, + } +} + +func (l *Listener) Start() error { + if common.Contains(l.network, N.NetworkTCP) { + _, err := l.ListenTCP() + if err != nil { + return err + } + go l.loopTCPIn() + } + if common.Contains(l.network, N.NetworkUDP) { + _, err := l.ListenUDP() + if err != nil { + return err + } + l.packetOutboundClosed = make(chan struct{}) + l.packetOutbound = make(chan *N.PacketBuffer, 64) + go l.loopUDPIn() + if !l.disablePacketOutput { + go l.loopUDPOut() + } + } + if l.setSystemProxy { + listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port + var listenAddrString string + listenAddr := l.listenOptions.Listen.Build(netip.IPv4Unspecified()) + if listenAddr.IsUnspecified() { + listenAddrString = "127.0.0.1" + } else { + listenAddrString = listenAddr.String() + } + systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS) + if err != nil { + return E.Cause(err, "initialize system proxy") + } + err = systemProxy.Enable() + if err != nil { + return E.Cause(err, "set system proxy") + } + l.systemProxy = systemProxy + } + return nil +} + +func (l *Listener) Close() error { + l.shutdown.Store(true) + var err error + if l.systemProxy != nil && l.systemProxy.IsEnabled() { + err = l.systemProxy.Disable() + } + return E.Errors(err, common.Close( + l.tcpListener, + common.PtrOrNil(l.udpConn), + )) +} + +func (l *Listener) TCPListener() net.Listener { + return l.tcpListener +} + +func (l *Listener) UDPConn() *net.UDPConn { + return l.udpConn +} + +func (l *Listener) ListenOptions() option.ListenOptions { + return l.listenOptions +} + +func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) { + if nameOrPath != "" { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + currentNs, err := netns.Get() + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "get current netns") + } + defer currentNs.Close() + defer netns.Set(currentNs) + var targetNs netns.NsHandle + if strings.HasPrefix(nameOrPath, "/") { + targetNs, err = netns.GetFromPath(nameOrPath) + } else { + targetNs, err = netns.GetFromName(nameOrPath) + } + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath) + } + defer targetNs.Close() + err = netns.Set(targetNs) + if err != nil { + return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath) + } + } + return block() +} diff --git a/common/listener/listener_tcp.go b/common/listener/listener_tcp.go new file mode 100644 index 00000000..54d84a6b --- /dev/null +++ b/common/listener/listener_tcp.go @@ -0,0 +1,111 @@ +package listener + +import ( + "net" + "net/netip" + "strings" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/redir" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/control" + 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/service" + + "github.com/database64128/tfo-go/v2" +) + +func (l *Listener) ListenTCP() (net.Listener, error) { + //nolint:staticcheck + if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader { + return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") + } + var err error + bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) + var listenConfig net.ListenConfig + if l.listenOptions.BindInterface != "" { + listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) + } + if l.listenOptions.RoutingMark != 0 { + listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) + } + if l.listenOptions.ReuseAddr { + listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) + } + if l.listenOptions.DisableTCPKeepAlive { + listenConfig.KeepAlive = -1 + listenConfig.KeepAliveConfig.Enable = false + } else { + keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) + if keepIdle == 0 { + keepIdle = C.TCPKeepAliveInitial + } + keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval) + if keepInterval == 0 { + keepInterval = C.TCPKeepAliveInterval + } + listenConfig.KeepAliveConfig = net.KeepAliveConfig{ + Enable: true, + Idle: keepIdle, + Interval: keepInterval, + } + } + if l.listenOptions.TCPMultiPath { + listenConfig.SetMultipathTCP(true) + } + if l.tproxy { + listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false) + }) + }) + } + tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) { + if l.listenOptions.TCPFastOpen { + var tfoConfig tfo.ListenConfig + tfoConfig.ListenConfig = listenConfig + return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) + } else { + return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) + } + }) + if err != nil { + return nil, err + } + l.logger.Info("tcp server started at ", tcpListener.Addr()) + l.tcpListener = tcpListener + return tcpListener, err +} + +func (l *Listener) loopTCPIn() { + tcpListener := l.tcpListener + var metadata adapter.InboundContext + for { + conn, err := tcpListener.Accept() + if err != nil { + //nolint:staticcheck + if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() { + l.logger.Error(err) + continue + } + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.tcpListener.Close() + l.logger.Error("tcp listener closed: ", err) + continue + } + //nolint:staticcheck + metadata.InboundDetour = l.listenOptions.Detour + metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() + metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + ctx := log.ContextWithNewID(l.ctx) + l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil) + } +} diff --git a/common/listener/listener_udp.go b/common/listener/listener_udp.go new file mode 100644 index 00000000..e689c8bb --- /dev/null +++ b/common/listener/listener_udp.go @@ -0,0 +1,207 @@ +package listener + +import ( + "context" + "net" + "net/netip" + "os" + "strings" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/redir" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + 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/service" +) + +func (l *Listener) ListenUDP() (net.PacketConn, error) { + bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) + var listenConfig net.ListenConfig + if l.listenOptions.BindInterface != "" { + listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) + } + if l.listenOptions.RoutingMark != 0 { + listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) + } + if l.listenOptions.ReuseAddr { + listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) + } + var udpFragment bool + if l.listenOptions.UDPFragment != nil { + udpFragment = *l.listenOptions.UDPFragment + } else { + udpFragment = l.listenOptions.UDPFragmentDefault + } + if !udpFragment { + listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment()) + } + if l.tproxy { + listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true) + }) + }) + } + udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { + return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) + }) + if err != nil { + return nil, err + } + l.udpConn = udpConn.(*net.UDPConn) + l.udpAddr = bindAddr + l.logger.Info("udp server started at ", udpConn.LocalAddr()) + return udpConn, err +} + +func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) { + return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) { + if l.listenOptions.BindInterface != "" { + dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) + } + if l.listenOptions.RoutingMark != 0 { + dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) + } + if l.listenOptions.ReuseAddr { + dialer.Control = control.Append(dialer.Control, control.ReuseAddr()) + } + return dialer.DialContext(ctx, network, address) + }) +} + +func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) { + return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { + if l.listenOptions.BindInterface != "" { + listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) + } + if l.listenOptions.RoutingMark != 0 { + listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) + } + if l.listenOptions.ReuseAddr { + listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) + } + return listenConfig.ListenPacket(ctx, network, address) + }) +} + +func (l *Listener) UDPAddr() M.Socksaddr { + return l.udpAddr +} + +func (l *Listener) PacketWriter() N.PacketWriter { + return (*packetWriter)(l) +} + +func (l *Listener) loopUDPIn() { + defer close(l.packetOutboundClosed) + var buffer *buf.Buffer + if !l.threadUnsafePacketWriter { + buffer = buf.NewPacket() + defer buffer.Release() + buffer.IncRef() + defer buffer.DecRef() + } + if l.oobPacketHandler != nil { + oob := make([]byte, 1024) + for { + if l.threadUnsafePacketWriter { + buffer = buf.NewPacket() + } else { + buffer.Reset() + } + n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob) + if err != nil { + if l.threadUnsafePacketWriter { + buffer.Release() + } + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.udpConn.Close() + l.logger.Error("udp listener closed: ", err) + return + } + buffer.Truncate(n) + l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) + } + } else { + for { + if l.threadUnsafePacketWriter { + buffer = buf.NewPacket() + } else { + buffer.Reset() + } + n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) + if err != nil { + if l.threadUnsafePacketWriter { + buffer.Release() + } + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.udpConn.Close() + l.logger.Error("udp listener closed: ", err) + return + } + buffer.Truncate(n) + l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) + } + } +} + +func (l *Listener) loopUDPOut() { + for { + select { + case packet := <-l.packetOutbound: + destination := packet.Destination.AddrPort() + _, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination) + packet.Buffer.Release() + N.PutPacketBuffer(packet) + if err != nil { + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.logger.Error("udp listener write back: ", destination, ": ", err) + continue + } + continue + case <-l.packetOutboundClosed: + } + for { + select { + case packet := <-l.packetOutbound: + packet.Buffer.Release() + N.PutPacketBuffer(packet) + default: + return + } + } + } +} + +type packetWriter Listener + +func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + packet := N.NewPacketBuffer() + packet.Buffer = buffer + packet.Destination = destination + select { + case w.packetOutbound <- packet: + return nil + default: + buffer.Release() + N.PutPacketBuffer(packet) + if w.shutdown.Load() { + return os.ErrClosed + } + w.logger.Trace("dropped packet to ", destination) + return nil + } +} + +func (w *packetWriter) WriteIsThreadUnsafe() { +} diff --git a/common/mux/client.go b/common/mux/client.go new file mode 100644 index 00000000..bf103355 --- /dev/null +++ b/common/mux/client.go @@ -0,0 +1,59 @@ +package mux + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-mux" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Client = mux.Client + +func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) { + if !options.Enabled { + return nil, nil + } + var brutalOptions mux.BrutalOptions + if options.Brutal != nil && options.Brutal.Enabled { + brutalOptions = mux.BrutalOptions{ + Enabled: true, + SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps), + ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps), + } + if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid upload speed") + } + if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid download speed") + } + } + return mux.NewClient(mux.Options{ + Dialer: &clientDialer{dialer}, + Logger: logger, + Protocol: options.Protocol, + MaxConnections: options.MaxConnections, + MinStreams: options.MinStreams, + MaxStreams: options.MaxStreams, + Padding: options.Padding, + Brutal: brutalOptions, + }) +} + +type clientDialer struct { + N.Dialer +} + +func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination) +} + +func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination) +} diff --git a/common/mux/router.go b/common/mux/router.go new file mode 100644 index 00000000..ec788086 --- /dev/null +++ b/common/mux/router.go @@ -0,0 +1,80 @@ +package mux + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-mux" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +type Router struct { + router adapter.ConnectionRouterEx + service *mux.Service +} + +func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) { + if !options.Enabled { + return router, nil + } + var brutalOptions mux.BrutalOptions + if options.Brutal != nil && options.Brutal.Enabled { + brutalOptions = mux.BrutalOptions{ + Enabled: true, + SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps), + ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps), + } + if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid upload speed") + } + if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid download speed") + } + } + service, err := mux.NewService(mux.ServiceOptions{ + NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + Logger: logger, + HandlerEx: adapter.NewRouteContextHandlerEx(router), + Padding: options.Padding, + Brutal: brutalOptions, + }) + if err != nil { + return nil, err + } + return &Router{router, service}, nil +} + +// Deprecated: Use RouteConnectionEx instead. +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + if metadata.Destination == mux.Destination { + // TODO: check if WithContext is necessary + return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) + } else { + return r.router.RouteConnection(ctx, conn, metadata) + } +} + +// Deprecated: Use RoutePacketConnectionEx instead. +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return r.router.RoutePacketConnection(ctx, conn, metadata) +} + +func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if metadata.Destination == mux.Destination { + r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose) + return + } + r.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/common/networkquality/http.go b/common/networkquality/http.go new file mode 100644 index 00000000..f9ff2a4a --- /dev/null +++ b/common/networkquality/http.go @@ -0,0 +1,142 @@ +package networkquality + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + + C "github.com/sagernet/sing-box/constant" + sBufio "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" +) + +func FormatBitrate(bps int64) string { + switch { + case bps >= 1_000_000_000: + return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000) + case bps >= 1_000_000: + return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000) + case bps >= 1_000: + return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000) + default: + return fmt.Sprintf("%d bps", bps) + } +} + +func NewHTTPClient(dialer N.Dialer) *http.Client { + transport := &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + } + if dialer != nil { + transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + } + return &http.Client{Transport: transport} +} + +func baseTransportFromClient(client *http.Client) (*http.Transport, error) { + if client == nil { + return nil, E.New("http client is nil") + } + if client.Transport == nil { + return http.DefaultTransport.(*http.Transport).Clone(), nil + } + transport, ok := client.Transport.(*http.Transport) + if !ok { + return nil, E.New("http client transport must be *http.Transport") + } + return transport.Clone(), nil +} + +func newMeasurementClient( + baseClient *http.Client, + connectEndpoint string, + singleConnection bool, + disableKeepAlives bool, + readCounters []N.CountFunc, + writeCounters []N.CountFunc, +) (*http.Client, error) { + transport, err := baseTransportFromClient(baseClient) + if err != nil { + return nil, err + } + transport.DisableCompression = true + transport.DisableKeepAlives = disableKeepAlives + if singleConnection { + transport.MaxConnsPerHost = 1 + transport.MaxIdleConnsPerHost = 1 + transport.MaxIdleConns = 1 + } + + baseDialContext := transport.DialContext + if baseDialContext == nil { + dialer := &net.Dialer{} + baseDialContext = dialer.DialContext + } + transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) { + dialAddr := addr + if connectEndpoint != "" { + dialAddr = rewriteDialAddress(addr, connectEndpoint) + } + conn, dialErr := baseDialContext(ctx, network, dialAddr) + if dialErr != nil { + return nil, dialErr + } + if len(readCounters) > 0 || len(writeCounters) > 0 { + return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil + } + return conn, nil + } + + return &http.Client{ + Transport: transport, + CheckRedirect: baseClient.CheckRedirect, + Jar: baseClient.Jar, + Timeout: baseClient.Timeout, + }, nil +} + +type MeasurementClientFactory func( + connectEndpoint string, + singleConnection bool, + disableKeepAlives bool, + readCounters []N.CountFunc, + writeCounters []N.CountFunc, +) (*http.Client, error) + +func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory { + return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) { + return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters) + } +} + +func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) { + if !useHTTP3 { + return nil, nil + } + return NewHTTP3MeasurementClientFactory(dialer) +} + +func rewriteDialAddress(addr string, connectEndpoint string) string { + connectEndpoint = strings.TrimSpace(connectEndpoint) + host, port, err := net.SplitHostPort(addr) + if err != nil { + return addr + } + endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint) + if err == nil { + host = endpointHost + if endpointPort != "" { + port = endpointPort + } + } else if connectEndpoint != "" { + host = connectEndpoint + } + return net.JoinHostPort(host, port) +} diff --git a/common/networkquality/http3.go b/common/networkquality/http3.go new file mode 100644 index 00000000..0e907821 --- /dev/null +++ b/common/networkquality/http3.go @@ -0,0 +1,55 @@ +//go:build with_quic + +package networkquality + +import ( + "context" + "crypto/tls" + "net" + "net/http" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + sBufio "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) { + // singleConnection and disableKeepAlives are not applied: + // HTTP/3 multiplexes streams over a single QUIC connection by default. + return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) { + transport := &http3.Transport{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { + dialAddr := addr + if connectEndpoint != "" { + dialAddr = rewriteDialAddress(addr, connectEndpoint) + } + destination := M.ParseSocksaddr(dialAddr) + var udpConn net.Conn + var dialErr error + if dialer != nil { + udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination) + } else { + var netDialer net.Dialer + udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String()) + } + if dialErr != nil { + return nil, dialErr + } + wrappedConn := udpConn + if len(readCounters) > 0 || len(writeCounters) > 0 { + wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters) + } + packetConn := sBufio.NewUnbindPacketConn(wrappedConn) + quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg) + if dialErr != nil { + udpConn.Close() + return nil, dialErr + } + return quicConn, nil + }, + } + return &http.Client{Transport: transport}, nil + }, nil +} diff --git a/common/networkquality/http3_stub.go b/common/networkquality/http3_stub.go new file mode 100644 index 00000000..632837e6 --- /dev/null +++ b/common/networkquality/http3_stub.go @@ -0,0 +1,12 @@ +//go:build !with_quic + +package networkquality + +import ( + C "github.com/sagernet/sing-box/constant" + N "github.com/sagernet/sing/common/network" +) + +func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) { + return nil, C.ErrQUICNotIncluded +} diff --git a/common/networkquality/networkquality.go b/common/networkquality/networkquality.go new file mode 100644 index 00000000..a4c73472 --- /dev/null +++ b/common/networkquality/networkquality.go @@ -0,0 +1,1413 @@ +package networkquality + +import ( + "context" + "crypto/tls" + "encoding/json" + "io" + "math" + "math/rand" + "net/http" + "net/http/httptrace" + "net/url" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + sBufio "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +const DefaultConfigURL = "https://mensura.cdn-apple.com/api/v1/gm/config" + +type Config struct { + Version int `json:"version"` + TestEndpoint string `json:"test_endpoint"` + URLs URLs `json:"urls"` +} + +type URLs struct { + SmallHTTPSDownloadURL string `json:"small_https_download_url"` + LargeHTTPSDownloadURL string `json:"large_https_download_url"` + HTTPSUploadURL string `json:"https_upload_url"` + SmallDownloadURL string `json:"small_download_url"` + LargeDownloadURL string `json:"large_download_url"` + UploadURL string `json:"upload_url"` +} + +func (u *URLs) smallDownloadURL() string { + if u.SmallHTTPSDownloadURL != "" { + return u.SmallHTTPSDownloadURL + } + return u.SmallDownloadURL +} + +func (u *URLs) largeDownloadURL() string { + if u.LargeHTTPSDownloadURL != "" { + return u.LargeHTTPSDownloadURL + } + return u.LargeDownloadURL +} + +func (u *URLs) uploadURL() string { + if u.HTTPSUploadURL != "" { + return u.HTTPSUploadURL + } + return u.UploadURL +} + +type Accuracy int32 + +const ( + AccuracyLow Accuracy = 0 + AccuracyMedium Accuracy = 1 + AccuracyHigh Accuracy = 2 +) + +func (a Accuracy) String() string { + switch a { + case AccuracyHigh: + return "High" + case AccuracyMedium: + return "Medium" + default: + return "Low" + } +} + +type Result struct { + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + DownloadCapacityAccuracy Accuracy + UploadCapacityAccuracy Accuracy + DownloadRPMAccuracy Accuracy + UploadRPMAccuracy Accuracy +} + +type Progress struct { + Phase Phase + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + ElapsedMs int64 + DownloadCapacityAccuracy Accuracy + UploadCapacityAccuracy Accuracy + DownloadRPMAccuracy Accuracy + UploadRPMAccuracy Accuracy +} + +type Phase int32 + +const ( + PhaseIdle Phase = 0 + PhaseDownload Phase = 1 + PhaseUpload Phase = 2 + PhaseDone Phase = 3 +) + +type Options struct { + ConfigURL string + HTTPClient *http.Client + NewMeasurementClient MeasurementClientFactory + Serial bool + MaxRuntime time.Duration + OnProgress func(Progress) + Context context.Context +} + +const DefaultMaxRuntime = 20 * time.Second + +type measurementSettings struct { + idleProbeCount int + testTimeout time.Duration + stabilityInterval time.Duration + sampleInterval time.Duration + progressInterval time.Duration + maxProbesPerSecond int + initialConnections int + maxConnections int + movingAvgDistance int + trimPercent int + stdDevTolerancePct float64 + maxProbeCapacityPct float64 +} + +var settings = measurementSettings{ + idleProbeCount: 5, + testTimeout: DefaultMaxRuntime, + stabilityInterval: time.Second, + sampleInterval: 250 * time.Millisecond, + progressInterval: 500 * time.Millisecond, + maxProbesPerSecond: 100, + initialConnections: 1, + maxConnections: 16, + movingAvgDistance: 4, + trimPercent: 5, + stdDevTolerancePct: 5, + maxProbeCapacityPct: 0.05, +} + +type resolvedConfig struct { + smallURL *url.URL + largeURL *url.URL + uploadURL *url.URL + connectEndpoint string +} + +type directionPlan struct { + dataURL *url.URL + probeURL *url.URL + connectEndpoint string + isUpload bool +} + +type probeTrace struct { + reused bool + connectStart time.Time + connectDone time.Time + tlsStart time.Time + tlsDone time.Time + tlsVersion uint16 + gotConn time.Time + wroteRequest time.Time + firstResponseByte time.Time +} + +type probeMeasurement struct { + total time.Duration + tcp time.Duration + tls time.Duration + httpFirst time.Duration + httpLoaded time.Duration + bytes int64 + reused bool +} + +type probeRound struct { + interval int + tcp time.Duration + tls time.Duration + httpFirst time.Duration + httpLoaded time.Duration +} + +func (p probeRound) responsivenessLatency() float64 { + var foreignSamples []float64 + if p.tcp > 0 { + foreignSamples = append(foreignSamples, durationMillis(p.tcp)) + } + if p.tls > 0 { + foreignSamples = append(foreignSamples, durationMillis(p.tls)) + } + if p.httpFirst > 0 { + foreignSamples = append(foreignSamples, durationMillis(p.httpFirst)) + } + if len(foreignSamples) == 0 || p.httpLoaded <= 0 { + return 0 + } + return (meanFloat64s(foreignSamples) + durationMillis(p.httpLoaded)) / 2 +} + +const maxConsecutiveErrors = 3 + +type loadConnection struct { + client *http.Client + dataURL *url.URL + isUpload bool + active atomic.Bool + ready atomic.Bool +} + +func (c *loadConnection) run(ctx context.Context, onError func(error)) { + defer c.client.CloseIdleConnections() + markActive := func() { + c.ready.Store(true) + c.active.Store(true) + } + var consecutiveErrors int + for { + select { + case <-ctx.Done(): + return + default: + } + var err error + if c.isUpload { + err = runUploadRequest(ctx, c.client, c.dataURL.String(), markActive) + } else { + err = runDownloadRequest(ctx, c.client, c.dataURL.String(), markActive) + } + c.active.Store(false) + if err != nil { + if ctx.Err() != nil { + return + } + consecutiveErrors++ + if consecutiveErrors > maxConsecutiveErrors { + onError(err) + return + } + c.client.CloseIdleConnections() + continue + } + consecutiveErrors = 0 + } +} + +type intervalThroughput struct { + interval int + bps float64 +} + +type intervalWindow struct { + lower int + upper int +} + +type stabilityTracker struct { + window int + stdDevTolerancePct float64 + instantaneous []float64 + movingAverages []float64 +} + +func (s *stabilityTracker) add(value float64) bool { + if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { + return false + } + s.instantaneous = append(s.instantaneous, value) + if len(s.instantaneous) > s.window { + s.instantaneous = s.instantaneous[len(s.instantaneous)-s.window:] + } + s.movingAverages = append(s.movingAverages, meanFloat64s(s.instantaneous)) + if len(s.movingAverages) > s.window { + s.movingAverages = s.movingAverages[len(s.movingAverages)-s.window:] + } + return s.stable() +} + +func (s *stabilityTracker) ready() bool { + return len(s.movingAverages) >= s.window +} + +func (s *stabilityTracker) accuracy() Accuracy { + if s.stable() { + return AccuracyHigh + } + if s.ready() { + return AccuracyMedium + } + return AccuracyLow +} + +func (s *stabilityTracker) stable() bool { + if len(s.movingAverages) < s.window { + return false + } + currentAverage := s.movingAverages[len(s.movingAverages)-1] + if currentAverage <= 0 { + return false + } + return stdDevFloat64s(s.movingAverages) <= currentAverage*(s.stdDevTolerancePct/100) +} + +type directionMeasurement struct { + capacity int64 + rpm int32 + capacityAccuracy Accuracy + rpmAccuracy Accuracy +} + +type directionRunner struct { + factory MeasurementClientFactory + plan directionPlan + probeBytes int64 + + errCh chan error + errOnce sync.Once + wg sync.WaitGroup + + totalBytes atomic.Int64 + currentCapacity atomic.Int64 + currentRPM atomic.Int32 + currentInterval atomic.Int64 + + connMu sync.Mutex + connections []*loadConnection + + probeMu sync.Mutex + probeRounds []probeRound + intervalProbeValues []float64 + responsivenessWindow *intervalWindow + throughputs []intervalThroughput + throughputWindow *intervalWindow +} + +func newDirectionRunner(factory MeasurementClientFactory, plan directionPlan, probeBytes int64) *directionRunner { + return &directionRunner{ + factory: factory, + plan: plan, + probeBytes: probeBytes, + errCh: make(chan error, 1), + } +} + +func (r *directionRunner) fail(err error) { + if err == nil { + return + } + r.errOnce.Do(func() { + select { + case r.errCh <- err: + default: + } + }) +} + +func (r *directionRunner) onConnectionFailed(err error) { + r.connMu.Lock() + activeCount := 0 + for _, conn := range r.connections { + if conn.active.Load() { + activeCount++ + } + } + r.connMu.Unlock() + if activeCount == 0 { + r.fail(err) + } +} + +func (r *directionRunner) addConnection(ctx context.Context) error { + counter := N.CountFunc(func(n int64) { r.totalBytes.Add(n) }) + var readCounters, writeCounters []N.CountFunc + if r.plan.isUpload { + writeCounters = []N.CountFunc{counter} + } else { + readCounters = []N.CountFunc{counter} + } + client, err := r.factory(r.plan.connectEndpoint, true, false, readCounters, writeCounters) + if err != nil { + return err + } + conn := &loadConnection{ + client: client, + dataURL: r.plan.dataURL, + isUpload: r.plan.isUpload, + } + r.connMu.Lock() + r.connections = append(r.connections, conn) + r.connMu.Unlock() + r.wg.Add(1) + go func() { + defer r.wg.Done() + conn.run(ctx, r.onConnectionFailed) + }() + return nil +} + +func (r *directionRunner) connectionCount() int { + r.connMu.Lock() + defer r.connMu.Unlock() + return len(r.connections) +} + +func (r *directionRunner) pickReadyConnection() *loadConnection { + r.connMu.Lock() + defer r.connMu.Unlock() + var ready []*loadConnection + for _, conn := range r.connections { + if conn.ready.Load() && conn.active.Load() { + ready = append(ready, conn) + } + } + if len(ready) == 0 { + return nil + } + return ready[rand.Intn(len(ready))] +} + +func (r *directionRunner) startProber(ctx context.Context) { + r.wg.Add(1) + go func() { + defer r.wg.Done() + ticker := time.NewTicker(r.probeInterval()) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + conn := r.pickReadyConnection() + if conn == nil { + continue + } + go func(selfClient *http.Client) { + foreignClient, err := r.factory(r.plan.connectEndpoint, true, true, nil, nil) + if err != nil { + return + } + round, err := collectProbeRound(ctx, foreignClient, selfClient, r.plan.probeURL.String()) + foreignClient.CloseIdleConnections() + if err != nil { + return + } + r.recordProbeRound(probeRound{ + interval: int(r.currentInterval.Load()), + tcp: round.tcp, + tls: round.tls, + httpFirst: round.httpFirst, + httpLoaded: round.httpLoaded, + }) + }(conn.client) + ticker.Reset(r.probeInterval()) + } + }() +} + +func (r *directionRunner) probeInterval() time.Duration { + interval := time.Second / time.Duration(settings.maxProbesPerSecond) + capacity := r.currentCapacity.Load() + if capacity <= 0 || r.probeBytes <= 0 || settings.maxProbeCapacityPct <= 0 { + return interval + } + bitsPerRound := float64(r.probeBytes*2) * 8 + minSeconds := bitsPerRound / (float64(capacity) * settings.maxProbeCapacityPct) + if minSeconds <= 0 { + return interval + } + capacityInterval := time.Duration(minSeconds * float64(time.Second)) + if capacityInterval > interval { + interval = capacityInterval + } + return interval +} + +func (r *directionRunner) recordProbeRound(round probeRound) { + r.probeMu.Lock() + r.probeRounds = append(r.probeRounds, round) + if latency := round.responsivenessLatency(); latency > 0 { + r.intervalProbeValues = append(r.intervalProbeValues, latency) + } + r.currentRPM.Store(calculateRPM(r.probeRounds)) + r.probeMu.Unlock() +} + +func (r *directionRunner) swapIntervalProbeValues() []float64 { + r.probeMu.Lock() + defer r.probeMu.Unlock() + values := append([]float64(nil), r.intervalProbeValues...) + r.intervalProbeValues = nil + return values +} + +func (r *directionRunner) setResponsivenessWindow(currentInterval int) { + lower := currentInterval - settings.movingAvgDistance + 1 + if lower < 0 { + lower = 0 + } + r.probeMu.Lock() + r.responsivenessWindow = &intervalWindow{lower: lower, upper: currentInterval} + r.probeMu.Unlock() +} + +func (r *directionRunner) recordThroughput(interval int, bps float64) { + r.probeMu.Lock() + r.throughputs = append(r.throughputs, intervalThroughput{interval: interval, bps: bps}) + r.probeMu.Unlock() +} + +func (r *directionRunner) setThroughputWindow(currentInterval int) { + lower := currentInterval - settings.movingAvgDistance + 1 + if lower < 0 { + lower = 0 + } + r.probeMu.Lock() + r.throughputWindow = &intervalWindow{lower: lower, upper: currentInterval} + r.probeMu.Unlock() +} + +func (r *directionRunner) finalRPM() int32 { + r.probeMu.Lock() + defer r.probeMu.Unlock() + if r.responsivenessWindow == nil { + return calculateRPM(r.probeRounds) + } + var rounds []probeRound + for _, round := range r.probeRounds { + if round.interval >= r.responsivenessWindow.lower && round.interval <= r.responsivenessWindow.upper { + rounds = append(rounds, round) + } + } + if len(rounds) == 0 { + rounds = r.probeRounds + } + return calculateRPM(rounds) +} + +func (r *directionRunner) finalCapacity(totalDuration time.Duration) int64 { + r.probeMu.Lock() + defer r.probeMu.Unlock() + var samples []float64 + if r.throughputWindow != nil { + for _, sample := range r.throughputs { + if sample.interval >= r.throughputWindow.lower && sample.interval <= r.throughputWindow.upper { + samples = append(samples, sample.bps) + } + } + } + if len(samples) == 0 { + for _, sample := range r.throughputs { + samples = append(samples, sample.bps) + } + } + if len(samples) > 0 { + return int64(math.Round(upperTrimmedMean(samples, settings.trimPercent))) + } + if totalDuration > 0 { + return int64(float64(r.totalBytes.Load()) * 8 / totalDuration.Seconds()) + } + return 0 +} + +func (r *directionRunner) wait() { + r.wg.Wait() +} + +func Run(options Options) (*Result, error) { + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + if options.HTTPClient == nil { + return nil, E.New("http client is required") + } + maxRuntime, err := normalizeMaxRuntime(options.MaxRuntime) + if err != nil { + return nil, err + } + configURL := resolveConfigURL(options.ConfigURL) + config, err := fetchConfig(ctx, options.HTTPClient, configURL) + if err != nil { + return nil, E.Cause(err, "fetch config") + } + resolved, err := validateConfig(config) + if err != nil { + return nil, E.Cause(err, "validate config") + } + + start := time.Now() + report := func(progress Progress) { + if options.OnProgress == nil { + return + } + progress.ElapsedMs = time.Since(start).Milliseconds() + options.OnProgress(progress) + } + + factory := options.NewMeasurementClient + if factory == nil { + factory = defaultMeasurementClientFactory(options.HTTPClient) + } + + report(Progress{Phase: PhaseIdle}) + idleLatency, probeBytes, err := measureIdleLatency(ctx, factory, resolved) + if err != nil { + return nil, E.Cause(err, "measure idle latency") + } + report(Progress{Phase: PhaseIdle, IdleLatencyMs: idleLatency}) + + start = time.Now() + + var download, upload *directionMeasurement + if options.Serial { + download, upload, err = measureSerial( + ctx, + factory, + resolved, + idleLatency, + probeBytes, + maxRuntime, + report, + ) + } else { + download, upload, err = measureParallel( + ctx, + factory, + resolved, + idleLatency, + probeBytes, + maxRuntime, + report, + ) + } + if err != nil { + return nil, err + } + + result := &Result{ + DownloadCapacity: download.capacity, + UploadCapacity: upload.capacity, + DownloadRPM: download.rpm, + UploadRPM: upload.rpm, + IdleLatencyMs: idleLatency, + DownloadCapacityAccuracy: download.capacityAccuracy, + UploadCapacityAccuracy: upload.capacityAccuracy, + DownloadRPMAccuracy: download.rpmAccuracy, + UploadRPMAccuracy: upload.rpmAccuracy, + } + report(Progress{ + Phase: PhaseDone, + DownloadCapacity: result.DownloadCapacity, + UploadCapacity: result.UploadCapacity, + DownloadRPM: result.DownloadRPM, + UploadRPM: result.UploadRPM, + IdleLatencyMs: result.IdleLatencyMs, + DownloadCapacityAccuracy: result.DownloadCapacityAccuracy, + UploadCapacityAccuracy: result.UploadCapacityAccuracy, + DownloadRPMAccuracy: result.DownloadRPMAccuracy, + UploadRPMAccuracy: result.UploadRPMAccuracy, + }) + return result, nil +} + +func normalizeMaxRuntime(maxRuntime time.Duration) (time.Duration, error) { + if maxRuntime == 0 { + return settings.testTimeout, nil + } + if maxRuntime < 0 { + return 0, E.New("max runtime must be positive") + } + return maxRuntime, nil +} + +func measureSerial( + ctx context.Context, + factory MeasurementClientFactory, + resolved *resolvedConfig, + idleLatency int32, + probeBytes int64, + maxRuntime time.Duration, + report func(Progress), +) (*directionMeasurement, *directionMeasurement, error) { + downloadRuntime, uploadRuntime := splitRuntimeBudget(maxRuntime, 2) + report(Progress{Phase: PhaseDownload, IdleLatencyMs: idleLatency}) + download, err := measureDirection(ctx, factory, directionPlan{ + dataURL: resolved.largeURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + }, probeBytes, downloadRuntime, func(capacity int64, rpm int32) { + report(Progress{ + Phase: PhaseDownload, + DownloadCapacity: capacity, + DownloadRPM: rpm, + IdleLatencyMs: idleLatency, + }) + }) + if err != nil { + return nil, nil, E.Cause(err, "measure download") + } + + report(Progress{ + Phase: PhaseUpload, + DownloadCapacity: download.capacity, + DownloadRPM: download.rpm, + IdleLatencyMs: idleLatency, + }) + upload, err := measureDirection(ctx, factory, directionPlan{ + dataURL: resolved.uploadURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + isUpload: true, + }, probeBytes, uploadRuntime, func(capacity int64, rpm int32) { + report(Progress{ + Phase: PhaseUpload, + DownloadCapacity: download.capacity, + UploadCapacity: capacity, + DownloadRPM: download.rpm, + UploadRPM: rpm, + IdleLatencyMs: idleLatency, + }) + }) + if err != nil { + return nil, nil, E.Cause(err, "measure upload") + } + return download, upload, nil +} + +func measureParallel( + ctx context.Context, + factory MeasurementClientFactory, + resolved *resolvedConfig, + idleLatency int32, + probeBytes int64, + maxRuntime time.Duration, + report func(Progress), +) (*directionMeasurement, *directionMeasurement, error) { + type parallelResult struct { + measurement *directionMeasurement + err error + } + type progressState struct { + sync.Mutex + downloadCapacity int64 + uploadCapacity int64 + downloadRPM int32 + uploadRPM int32 + } + + parallelCtx, cancel := context.WithCancel(ctx) + defer cancel() + + report(Progress{Phase: PhaseDownload, IdleLatencyMs: idleLatency}) + report(Progress{Phase: PhaseUpload, IdleLatencyMs: idleLatency}) + + var state progressState + sendProgress := func(phase Phase, capacity int64, rpm int32) { + state.Lock() + if phase == PhaseDownload { + state.downloadCapacity = capacity + state.downloadRPM = rpm + } else { + state.uploadCapacity = capacity + state.uploadRPM = rpm + } + snapshot := Progress{ + Phase: phase, + DownloadCapacity: state.downloadCapacity, + UploadCapacity: state.uploadCapacity, + DownloadRPM: state.downloadRPM, + UploadRPM: state.uploadRPM, + IdleLatencyMs: idleLatency, + } + state.Unlock() + report(snapshot) + } + + var wg sync.WaitGroup + downloadCh := make(chan parallelResult, 1) + uploadCh := make(chan parallelResult, 1) + wg.Add(2) + go func() { + defer wg.Done() + measurement, err := measureDirection(parallelCtx, factory, directionPlan{ + dataURL: resolved.largeURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + }, probeBytes, maxRuntime, func(capacity int64, rpm int32) { + sendProgress(PhaseDownload, capacity, rpm) + }) + if err != nil { + cancel() + downloadCh <- parallelResult{err: E.Cause(err, "measure download")} + return + } + downloadCh <- parallelResult{measurement: measurement} + }() + go func() { + defer wg.Done() + measurement, err := measureDirection(parallelCtx, factory, directionPlan{ + dataURL: resolved.uploadURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + isUpload: true, + }, probeBytes, maxRuntime, func(capacity int64, rpm int32) { + sendProgress(PhaseUpload, capacity, rpm) + }) + if err != nil { + cancel() + uploadCh <- parallelResult{err: E.Cause(err, "measure upload")} + return + } + uploadCh <- parallelResult{measurement: measurement} + }() + + download := <-downloadCh + upload := <-uploadCh + wg.Wait() + if download.err != nil { + return nil, nil, download.err + } + if upload.err != nil { + return nil, nil, upload.err + } + return download.measurement, upload.measurement, nil +} + +func resolveConfigURL(rawURL string) string { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return DefaultConfigURL + } + if !strings.Contains(rawURL, "://") && !strings.Contains(rawURL, "/") { + return "https://" + rawURL + "/.well-known/nq" + } + parsedURL, err := url.Parse(rawURL) + if err != nil { + return rawURL + } + if parsedURL.Scheme != "" && parsedURL.Host != "" && (parsedURL.Path == "" || parsedURL.Path == "/") { + parsedURL.Path = "/.well-known/nq" + return parsedURL.String() + } + return rawURL +} + +func fetchConfig(ctx context.Context, client *http.Client, configURL string) (*Config, error) { + req, err := newRequest(ctx, http.MethodGet, configURL, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err = validateResponse(resp); err != nil { + return nil, err + } + var config Config + if err = json.NewDecoder(resp.Body).Decode(&config); err != nil { + return nil, E.Cause(err, "decode config") + } + return &config, nil +} + +func validateConfig(config *Config) (*resolvedConfig, error) { + if config == nil { + return nil, E.New("config is nil") + } + if config.Version != 1 { + return nil, E.New("unsupported config version: ", config.Version) + } + parseURL := func(name string, rawURL string) (*url.URL, error) { + if rawURL == "" { + return nil, E.New("config missing required URL: ", name) + } + parsedURL, err := url.Parse(rawURL) + if err != nil { + return nil, E.Cause(err, "parse "+name) + } + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return nil, E.New("unsupported URL scheme in ", name, ": ", parsedURL.Scheme) + } + if parsedURL.Host == "" { + return nil, E.New("config missing host in ", name) + } + return parsedURL, nil + } + + smallURL, err := parseURL("small_download_url", config.URLs.smallDownloadURL()) + if err != nil { + return nil, err + } + largeURL, err := parseURL("large_download_url", config.URLs.largeDownloadURL()) + if err != nil { + return nil, err + } + uploadURL, err := parseURL("upload_url", config.URLs.uploadURL()) + if err != nil { + return nil, err + } + + if smallURL.Host != largeURL.Host || smallURL.Host != uploadURL.Host { + return nil, E.New("config URLs must use the same host") + } + + return &resolvedConfig{ + smallURL: smallURL, + largeURL: largeURL, + uploadURL: uploadURL, + connectEndpoint: strings.TrimSpace(config.TestEndpoint), + }, nil +} + +func measureIdleLatency(ctx context.Context, factory MeasurementClientFactory, config *resolvedConfig) (int32, int64, error) { + var latencies []int64 + var maxProbeBytes int64 + for i := 0; i < settings.idleProbeCount; i++ { + select { + case <-ctx.Done(): + return 0, 0, ctx.Err() + default: + } + client, err := factory(config.connectEndpoint, true, true, nil, nil) + if err != nil { + return 0, 0, err + } + measurement, err := runProbe(ctx, client, config.smallURL.String(), false) + client.CloseIdleConnections() + if err != nil { + return 0, 0, err + } + latencies = append(latencies, measurement.total.Milliseconds()) + if measurement.bytes > maxProbeBytes { + maxProbeBytes = measurement.bytes + } + } + sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] }) + return int32(latencies[len(latencies)/2]), maxProbeBytes, nil +} + +func measureDirection( + ctx context.Context, + factory MeasurementClientFactory, + plan directionPlan, + probeBytes int64, + maxRuntime time.Duration, + onProgress func(capacity int64, rpm int32), +) (*directionMeasurement, error) { + phaseCtx, phaseCancel := context.WithTimeout(ctx, maxRuntime) + defer phaseCancel() + + runner := newDirectionRunner(factory, plan, probeBytes) + defer runner.wait() + + for i := 0; i < settings.initialConnections; i++ { + err := runner.addConnection(phaseCtx) + if err != nil { + return nil, err + } + } + + runner.startProber(phaseCtx) + + throughputTracker := stabilityTracker{ + window: settings.movingAvgDistance, + stdDevTolerancePct: settings.stdDevTolerancePct, + } + responsivenessTracker := stabilityTracker{ + window: settings.movingAvgDistance, + stdDevTolerancePct: settings.stdDevTolerancePct, + } + + start := time.Now() + sampleTicker := time.NewTicker(settings.sampleInterval) + defer sampleTicker.Stop() + intervalTicker := time.NewTicker(settings.stabilityInterval) + defer intervalTicker.Stop() + progressTicker := time.NewTicker(settings.progressInterval) + defer progressTicker.Stop() + + prevSampleBytes := int64(0) + prevSampleTime := start + prevIntervalBytes := int64(0) + prevIntervalTime := start + var ewmaCapacity float64 + var goodputSaturated bool + var intervalIndex int + + for { + select { + case err := <-runner.errCh: + return nil, err + case now := <-sampleTicker.C: + currentBytes := runner.totalBytes.Load() + elapsed := now.Sub(prevSampleTime).Seconds() + if elapsed > 0 { + instantaneousBps := float64(currentBytes-prevSampleBytes) * 8 / elapsed + if ewmaCapacity == 0 { + ewmaCapacity = instantaneousBps + } else { + ewmaCapacity = 0.3*instantaneousBps + 0.7*ewmaCapacity + } + runner.currentCapacity.Store(int64(ewmaCapacity)) + } + prevSampleBytes = currentBytes + prevSampleTime = now + case <-intervalTicker.C: + now := time.Now() + currentBytes := runner.totalBytes.Load() + elapsed := now.Sub(prevIntervalTime).Seconds() + if elapsed > 0 { + intervalBps := float64(currentBytes-prevIntervalBytes) * 8 / elapsed + runner.recordThroughput(intervalIndex, intervalBps) + throughputStable := throughputTracker.add(intervalBps) + if throughputStable && runner.throughputWindow == nil { + runner.setThroughputWindow(intervalIndex) + } + if !goodputSaturated && (throughputStable || (runner.connectionCount() >= settings.maxConnections && throughputTracker.ready())) { + goodputSaturated = true + } + if runner.connectionCount() < settings.maxConnections { + err := runner.addConnection(phaseCtx) + if err != nil { + return nil, err + } + } + } + if goodputSaturated { + if values := runner.swapIntervalProbeValues(); len(values) > 0 { + if responsivenessTracker.add(upperTrimmedMean(values, settings.trimPercent)) && runner.responsivenessWindow == nil { + runner.setResponsivenessWindow(intervalIndex) + phaseCancel() + } + } + } + prevIntervalBytes = currentBytes + prevIntervalTime = now + intervalIndex++ + runner.currentInterval.Store(int64(intervalIndex)) + case <-progressTicker.C: + if onProgress != nil { + onProgress(int64(ewmaCapacity), runner.currentRPM.Load()) + } + case <-phaseCtx.Done(): + if ctx.Err() != nil { + return nil, ctx.Err() + } + totalDuration := time.Since(start) + return &directionMeasurement{ + capacity: runner.finalCapacity(totalDuration), + rpm: runner.finalRPM(), + capacityAccuracy: throughputTracker.accuracy(), + rpmAccuracy: responsivenessTracker.accuracy(), + }, nil + } + } +} + +func splitRuntimeBudget(total time.Duration, directions int) (time.Duration, time.Duration) { + if directions <= 1 { + return total, total + } + first := total / time.Duration(directions) + second := total - first + return first, second +} + +func collectProbeRound(ctx context.Context, foreignClient *http.Client, selfClient *http.Client, rawURL string) (probeMeasurement, error) { + var foreignResult probeMeasurement + var selfResult probeMeasurement + var foreignErr error + var selfErr error + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + foreignResult, foreignErr = runProbe(ctx, foreignClient, rawURL, false) + }() + go func() { + defer wg.Done() + selfResult, selfErr = runProbe(ctx, selfClient, rawURL, true) + }() + wg.Wait() + + if foreignErr != nil { + return probeMeasurement{}, E.Cause(foreignErr, "foreign probe") + } + if selfErr != nil { + return probeMeasurement{}, E.Cause(selfErr, "self probe") + } + return probeMeasurement{ + tcp: foreignResult.tcp, + tls: foreignResult.tls, + httpFirst: foreignResult.httpFirst, + httpLoaded: selfResult.httpLoaded, + }, nil +} + +func runProbe(ctx context.Context, client *http.Client, rawURL string, expectReuse bool) (probeMeasurement, error) { + var trace probeTrace + start := time.Now() + req, err := newRequest(httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ + ConnectStart: func(string, string) { + if trace.connectStart.IsZero() { + trace.connectStart = time.Now() + } + }, + ConnectDone: func(string, string, error) { + if trace.connectDone.IsZero() { + trace.connectDone = time.Now() + } + }, + TLSHandshakeStart: func() { + if trace.tlsStart.IsZero() { + trace.tlsStart = time.Now() + } + }, + TLSHandshakeDone: func(state tls.ConnectionState, _ error) { + if trace.tlsDone.IsZero() { + trace.tlsDone = time.Now() + trace.tlsVersion = state.Version + } + }, + GotConn: func(info httptrace.GotConnInfo) { + trace.reused = info.Reused + trace.gotConn = time.Now() + }, + WroteRequest: func(httptrace.WroteRequestInfo) { + trace.wroteRequest = time.Now() + }, + GotFirstResponseByte: func() { + trace.firstResponseByte = time.Now() + }, + }), http.MethodGet, rawURL, nil) + if err != nil { + return probeMeasurement{}, err + } + if !expectReuse { + req.Close = true + } + resp, err := client.Do(req) + if err != nil { + return probeMeasurement{}, err + } + defer resp.Body.Close() + if err = validateResponse(resp); err != nil { + return probeMeasurement{}, err + } + n, err := io.Copy(io.Discard, resp.Body) + end := time.Now() + if err != nil { + return probeMeasurement{}, err + } + if expectReuse && !trace.reused { + return probeMeasurement{}, E.New("self probe did not reuse an existing connection") + } + + httpStart := trace.wroteRequest + if httpStart.IsZero() { + switch { + case !trace.tlsDone.IsZero(): + httpStart = trace.tlsDone + case !trace.connectDone.IsZero(): + httpStart = trace.connectDone + case !trace.gotConn.IsZero(): + httpStart = trace.gotConn + default: + httpStart = start + } + } + + measurement := probeMeasurement{ + total: end.Sub(start), + bytes: n, + reused: trace.reused, + } + if !trace.connectStart.IsZero() && !trace.connectDone.IsZero() && trace.connectDone.After(trace.connectStart) { + measurement.tcp = trace.connectDone.Sub(trace.connectStart) + } + if !trace.tlsStart.IsZero() && !trace.tlsDone.IsZero() && trace.tlsDone.After(trace.tlsStart) { + measurement.tls = trace.tlsDone.Sub(trace.tlsStart) + if roundTrips := tlsHandshakeRoundTrips(trace.tlsVersion); roundTrips > 1 { + measurement.tls /= time.Duration(roundTrips) + } + } + if !trace.firstResponseByte.IsZero() && trace.firstResponseByte.After(httpStart) { + measurement.httpFirst = trace.firstResponseByte.Sub(httpStart) + } + if end.After(httpStart) { + measurement.httpLoaded = end.Sub(httpStart) + } + return measurement, nil +} + +func runDownloadRequest(ctx context.Context, client *http.Client, rawURL string, onActive func()) error { + req, err := newRequest(ctx, http.MethodGet, rawURL, nil) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + err = validateResponse(resp) + if err != nil { + return err + } + if onActive != nil { + onActive() + } + _, err = sBufio.Copy(io.Discard, resp.Body) + if ctx.Err() != nil { + return nil + } + return err +} + +func runUploadRequest(ctx context.Context, client *http.Client, rawURL string, onActive func()) error { + body := &uploadBody{ + ctx: ctx, + onActive: onActive, + } + req, err := newRequest(ctx, http.MethodPost, rawURL, body) + if err != nil { + return err + } + req.ContentLength = -1 + req.Header.Set("Content-Type", "application/octet-stream") + resp, err := client.Do(req) + if err != nil { + if ctx.Err() != nil { + return nil + } + return err + } + defer resp.Body.Close() + err = validateResponse(resp) + if err != nil { + return err + } + _, _ = io.Copy(io.Discard, resp.Body) + <-ctx.Done() + return nil +} + +func newRequest(ctx context.Context, method string, rawURL string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, rawURL, body) + if err != nil { + return nil, err + } + req.Header.Set("Accept-Encoding", "identity") + return req, nil +} + +func validateResponse(resp *http.Response) error { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return E.New("unexpected status: ", resp.Status) + } + if encoding := resp.Header.Get("Content-Encoding"); encoding != "" { + return E.New("unexpected content encoding: ", encoding) + } + return nil +} + +func calculateRPM(rounds []probeRound) int32 { + if len(rounds) == 0 { + return 0 + } + var tcpSamples []float64 + var tlsSamples []float64 + var httpFirstSamples []float64 + var httpLoadedSamples []float64 + for _, round := range rounds { + if round.tcp > 0 { + tcpSamples = append(tcpSamples, durationMillis(round.tcp)) + } + if round.tls > 0 { + tlsSamples = append(tlsSamples, durationMillis(round.tls)) + } + if round.httpFirst > 0 { + httpFirstSamples = append(httpFirstSamples, durationMillis(round.httpFirst)) + } + if round.httpLoaded > 0 { + httpLoadedSamples = append(httpLoadedSamples, durationMillis(round.httpLoaded)) + } + } + httpLoaded := upperTrimmedMean(httpLoadedSamples, settings.trimPercent) + if httpLoaded <= 0 { + return 0 + } + var foreignComponents []float64 + if tcp := upperTrimmedMean(tcpSamples, settings.trimPercent); tcp > 0 { + foreignComponents = append(foreignComponents, tcp) + } + if tls := upperTrimmedMean(tlsSamples, settings.trimPercent); tls > 0 { + foreignComponents = append(foreignComponents, tls) + } + if httpFirst := upperTrimmedMean(httpFirstSamples, settings.trimPercent); httpFirst > 0 { + foreignComponents = append(foreignComponents, httpFirst) + } + if len(foreignComponents) == 0 { + return 0 + } + foreignLatency := meanFloat64s(foreignComponents) + foreignRPM := 60000.0 / foreignLatency + loadedRPM := 60000.0 / httpLoaded + return int32(math.Round((foreignRPM + loadedRPM) / 2)) +} + +func tlsHandshakeRoundTrips(version uint16) int { + switch version { + case tls.VersionTLS12, tls.VersionTLS11, tls.VersionTLS10: + return 2 + default: + return 1 + } +} + +func durationMillis(value time.Duration) float64 { + return float64(value) / float64(time.Millisecond) +} + +func upperTrimmedMean(values []float64, trimPercent int) float64 { + trimmed := upperTrimFloat64s(values, trimPercent) + if len(trimmed) == 0 { + return 0 + } + return meanFloat64s(trimmed) +} + +func upperTrimFloat64s(values []float64, trimPercent int) []float64 { + if len(values) == 0 { + return nil + } + trimmed := append([]float64(nil), values...) + sort.Float64s(trimmed) + if trimPercent <= 0 { + return trimmed + } + trimCount := int(math.Floor(float64(len(trimmed)) * float64(trimPercent) / 100)) + if trimCount <= 0 || trimCount >= len(trimmed) { + return trimmed + } + return trimmed[:len(trimmed)-trimCount] +} + +func meanFloat64s(values []float64) float64 { + if len(values) == 0 { + return 0 + } + var total float64 + for _, value := range values { + total += value + } + return total / float64(len(values)) +} + +func stdDevFloat64s(values []float64) float64 { + if len(values) == 0 { + return 0 + } + mean := meanFloat64s(values) + var total float64 + for _, value := range values { + delta := value - mean + total += delta * delta + } + return math.Sqrt(total / float64(len(values))) +} + +type uploadBody struct { + ctx context.Context + activated atomic.Bool + onActive func() +} + +func (u *uploadBody) Read(p []byte) (int, error) { + if err := u.ctx.Err(); err != nil { + return 0, err + } + clear(p) + n := len(p) + if n > 0 && u.onActive != nil && u.activated.CompareAndSwap(false, true) { + u.onActive() + } + return n, nil +} + +func (u *uploadBody) Close() error { + return nil +} diff --git a/common/pipelistener/listener.go b/common/pipelistener/listener.go new file mode 100644 index 00000000..68de4d90 --- /dev/null +++ b/common/pipelistener/listener.go @@ -0,0 +1,57 @@ +package pipelistener + +import ( + "io" + "net" +) + +var _ net.Listener = (*Listener)(nil) + +type Listener struct { + pipe chan net.Conn + done chan struct{} +} + +func New(channelSize int) *Listener { + return &Listener{ + pipe: make(chan net.Conn, channelSize), + done: make(chan struct{}), + } +} + +func (l *Listener) Serve(conn net.Conn) { + l.pipe <- conn +} + +func (l *Listener) Accept() (net.Conn, error) { + select { + case conn := <-l.pipe: + return conn, nil + case <-l.done: + return nil, io.ErrClosedPipe + } +} + +func (l *Listener) Close() error { + select { + case <-l.done: + return io.ErrClosedPipe + default: + } + close(l.done) + return nil +} + +func (l *Listener) Addr() net.Addr { + return addr{} +} + +type addr struct{} + +func (a addr) Network() string { + return "pipe" +} + +func (a addr) String() string { + return "pipe" +} diff --git a/common/process/searcher.go b/common/process/searcher.go new file mode 100644 index 00000000..64305237 --- /dev/null +++ b/common/process/searcher.go @@ -0,0 +1,39 @@ +package process + +import ( + "context" + "net/netip" + "os/user" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-tun" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +type Searcher interface { + FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) + Close() error +} + +var ErrNotFound = E.New("process not found") + +type Config struct { + Logger log.ContextLogger + PackageManager tun.PackageManager +} + +func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + info, err := searcher.FindProcessInfo(ctx, network, source, destination) + if err != nil { + return nil, err + } + if info.UserId != -1 && info.UserName == "" { + osUser, _ := user.LookupId(F.ToString(info.UserId)) + if osUser != nil { + info.UserName = osUser.Username + } + } + return info, nil +} diff --git a/common/process/searcher_android.go b/common/process/searcher_android.go new file mode 100644 index 00000000..287c7219 --- /dev/null +++ b/common/process/searcher_android.go @@ -0,0 +1,48 @@ +package process + +import ( + "context" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" +) + +var _ Searcher = (*androidSearcher)(nil) + +type androidSearcher struct { + packageManager tun.PackageManager +} + +func NewSearcher(config Config) (Searcher, error) { + return &androidSearcher{config.PackageManager}, nil +} + +func (s *androidSearcher) Close() error { + return nil +} + +func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + family, protocol, err := socketDiagSettings(network, source) + if err != nil { + return nil, err + } + _, uid, err := querySocketDiagOnce(family, protocol, source) + if err != nil { + return nil, err + } + appID := uid % 100000 + var packageNames []string + if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded { + packageNames = append(packageNames, sharedPackage) + } + if packages, loaded := s.packageManager.PackagesByID(appID); loaded { + packageNames = append(packageNames, packages...) + } + packageNames = common.Uniq(packageNames) + return &adapter.ConnectionOwner{ + UserId: int32(uid), + AndroidPackageNames: packageNames, + }, nil +} diff --git a/common/process/searcher_darwin.go b/common/process/searcher_darwin.go new file mode 100644 index 00000000..1b5c0dd6 --- /dev/null +++ b/common/process/searcher_darwin.go @@ -0,0 +1,45 @@ +//go:build darwin + +package process + +import ( + "context" + "net/netip" + "strconv" + "strings" + "syscall" + + "github.com/sagernet/sing-box/adapter" +) + +var _ Searcher = (*darwinSearcher)(nil) + +type darwinSearcher struct{} + +func NewSearcher(_ Config) (Searcher, error) { + return &darwinSearcher{}, nil +} + +func (d *darwinSearcher) Close() error { + return nil +} + +func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + return FindDarwinConnectionOwner(network, source, destination) +} + +var structSize = func() int { + value, _ := syscall.Sysctl("kern.osrelease") + major, _, _ := strings.Cut(value, ".") + n, _ := strconv.ParseInt(major, 10, 64) + switch true { + case n >= 22: + return 408 + default: + // from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n + // size/offset are round up (aligned) to 8 bytes in darwin + // rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) + + // 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n)) + return 384 + } +}() diff --git a/common/process/searcher_darwin_shared.go b/common/process/searcher_darwin_shared.go new file mode 100644 index 00000000..05925530 --- /dev/null +++ b/common/process/searcher_darwin_shared.go @@ -0,0 +1,269 @@ +//go:build darwin + +package process + +import ( + "encoding/binary" + "net/netip" + "os" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/sys/unix" +) + +const ( + darwinSnapshotTTL = 200 * time.Millisecond + + darwinXinpgenSize = 24 + darwinXsocketOffset = 104 + darwinXinpcbForeignPort = 16 + darwinXinpcbLocalPort = 18 + darwinXinpcbVFlag = 44 + darwinXinpcbForeignAddr = 48 + darwinXinpcbLocalAddr = 64 + darwinXinpcbIPv4Addr = 12 + darwinXsocketUID = 64 + darwinXsocketLastPID = 68 + darwinTCPExtraStructSize = 208 +) + +type darwinConnectionEntry struct { + localAddr netip.Addr + remoteAddr netip.Addr + localPort uint16 + remotePort uint16 + pid uint32 + uid int32 +} + +type darwinConnectionMatchKind uint8 + +const ( + darwinConnectionMatchExact darwinConnectionMatchKind = iota + darwinConnectionMatchLocalFallback + darwinConnectionMatchWildcardFallback +) + +type darwinSnapshot struct { + createdAt time.Time + entries []darwinConnectionEntry +} + +type darwinConnectionFinder struct { + access sync.Mutex + ttl time.Duration + snapshots map[string]darwinSnapshot + builder func(string) (darwinSnapshot, error) +} + +var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL) + +func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder { + return &darwinConnectionFinder{ + ttl: ttl, + snapshots: make(map[string]darwinSnapshot), + builder: buildDarwinSnapshot, + } +} + +func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + return sharedDarwinConnectionFinder.find(network, source, destination) +} + +func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + networkName := N.NetworkName(network) + source = normalizeDarwinAddrPort(source) + destination = normalizeDarwinAddrPort(destination) + var lastOwner *adapter.ConnectionOwner + for attempt := 0; attempt < 2; attempt++ { + snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0) + if err != nil { + return nil, err + } + entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination) + if err != nil { + if err == ErrNotFound && fromCache { + continue + } + return nil, err + } + if fromCache && matchKind != darwinConnectionMatchExact { + continue + } + owner := &adapter.ConnectionOwner{ + UserId: entry.uid, + } + lastOwner = owner + if entry.pid == 0 { + return owner, nil + } + processPath, err := getExecPathFromPID(entry.pid) + if err == nil { + owner.ProcessPath = processPath + return owner, nil + } + if fromCache { + continue + } + return owner, nil + } + if lastOwner != nil { + return lastOwner, nil + } + return nil, ErrNotFound +} + +func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) { + f.access.Lock() + defer f.access.Unlock() + if !forceRefresh { + if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl { + return snapshot, true, nil + } + } + snapshot, err := f.builder(network) + if err != nil { + return darwinSnapshot{}, false, err + } + f.snapshots[network] = snapshot + return snapshot, false, nil +} + +func buildDarwinSnapshot(network string) (darwinSnapshot, error) { + spath, itemSize, err := darwinSnapshotSettings(network) + if err != nil { + return darwinSnapshot{}, err + } + value, err := unix.SysctlRaw(spath) + if err != nil { + return darwinSnapshot{}, err + } + return darwinSnapshot{ + createdAt: time.Now(), + entries: parseDarwinSnapshot(value, itemSize), + }, nil +} + +func darwinSnapshotSettings(network string) (string, int, error) { + itemSize := structSize + switch network { + case N.NetworkTCP: + return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil + case N.NetworkUDP: + return "net.inet.udp.pcblist_n", itemSize, nil + default: + return "", 0, os.ErrInvalid + } +} + +func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry { + entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize) + for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize { + inp := i + so := i + darwinXsocketOffset + entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset]) + if ok { + entries = append(entries, entry) + } + } + return entries +} + +func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) { + if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset { + return darwinConnectionEntry{}, false + } + entry := darwinConnectionEntry{ + remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]), + localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]), + pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]), + uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])), + } + flag := inp[darwinXinpcbVFlag] + switch { + case flag&0x1 != 0: + entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4])) + entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4])) + return entry, true + case flag&0x2 != 0: + entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16])) + entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16])) + return entry, true + default: + return darwinConnectionEntry{}, false + } +} + +func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) { + sourceAddr := source.Addr() + if !sourceAddr.IsValid() { + return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid + } + var localFallback darwinConnectionEntry + var hasLocalFallback bool + var wildcardFallback darwinConnectionEntry + var hasWildcardFallback bool + for _, entry := range entries { + if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() { + continue + } + if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() { + return entry, darwinConnectionMatchExact, nil + } + if !destination.IsValid() && entry.localAddr == sourceAddr { + return entry, darwinConnectionMatchExact, nil + } + if network != N.NetworkUDP { + continue + } + if !hasLocalFallback && entry.localAddr == sourceAddr { + hasLocalFallback = true + localFallback = entry + } + if !hasWildcardFallback && entry.localAddr.IsUnspecified() { + hasWildcardFallback = true + wildcardFallback = entry + } + } + if hasLocalFallback { + return localFallback, darwinConnectionMatchLocalFallback, nil + } + if hasWildcardFallback { + return wildcardFallback, darwinConnectionMatchWildcardFallback, nil + } + return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound +} + +func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort { + if !addrPort.IsValid() { + return addrPort + } + return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()) +} + +func getExecPathFromPID(pid uint32) (string, error) { + const ( + procpidpathinfo = 0xb + procpidpathinfosize = 1024 + proccallnumpidinfo = 0x2 + ) + buf := make([]byte, procpidpathinfosize) + _, _, errno := syscall.Syscall6( + syscall.SYS_PROC_INFO, + proccallnumpidinfo, + uintptr(pid), + procpidpathinfo, + 0, + uintptr(unsafe.Pointer(&buf[0])), + procpidpathinfosize) + if errno != 0 { + return "", errno + } + return unix.ByteSliceToString(buf), nil +} diff --git a/common/process/searcher_linux.go b/common/process/searcher_linux.go new file mode 100644 index 00000000..9b1a9160 --- /dev/null +++ b/common/process/searcher_linux.go @@ -0,0 +1,85 @@ +//go:build linux && !android + +package process + +import ( + "context" + "errors" + "net/netip" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ Searcher = (*linuxSearcher)(nil) + +type linuxSearcher struct { + logger log.ContextLogger + diagConns [4]*socketDiagConn + processPathCache *uidProcessPathCache +} + +func NewSearcher(config Config) (Searcher, error) { + searcher := &linuxSearcher{ + logger: config.Logger, + processPathCache: newUIDProcessPathCache(time.Second), + } + for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} { + for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} { + searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol) + } + } + return searcher, nil +} + +func (s *linuxSearcher) Close() error { + var errs []error + for _, conn := range s.diagConns { + if conn == nil { + continue + } + errs = append(errs, conn.Close()) + } + return E.Errors(errs...) +} + +func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + inode, uid, err := s.resolveSocketByNetlink(network, source, destination) + if err != nil { + return nil, err + } + processInfo := &adapter.ConnectionOwner{ + UserId: int32(uid), + } + processPath, err := s.processPathCache.findProcessPath(inode, uid) + if err != nil { + s.logger.DebugContext(ctx, "find process path: ", err) + } else { + processInfo.ProcessPath = processPath + } + return processInfo, nil +} + +func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + family, protocol, err := socketDiagSettings(network, source) + if err != nil { + return 0, 0, err + } + conn := s.diagConns[socketDiagConnIndex(family, protocol)] + if conn == nil { + return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol) + } + if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() { + inode, uid, err = conn.query(source, destination) + if err == nil { + return inode, uid, nil + } + if !errors.Is(err, ErrNotFound) { + return 0, 0, err + } + } + return querySocketDiagOnce(family, protocol, source) +} diff --git a/common/process/searcher_linux_shared.go b/common/process/searcher_linux_shared.go new file mode 100644 index 00000000..cd0601bc --- /dev/null +++ b/common/process/searcher_linux_shared.go @@ -0,0 +1,383 @@ +//go:build linux + +package process + +import ( + "encoding/binary" + "errors" + "net/netip" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + "unicode" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" +) + +const ( + sizeOfSocketDiagRequestData = 56 + sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData + socketDiagResponseMinSize = 72 + socketDiagByFamily = 20 + pathProc = "/proc" +) + +type socketDiagConn struct { + access sync.Mutex + family uint8 + protocol uint8 + fd int +} + +type uidProcessPathCache struct { + cache freelru.Cache[uint32, *uidProcessPaths] +} + +type uidProcessPaths struct { + entries map[uint32]string +} + +func newSocketDiagConn(family, protocol uint8) *socketDiagConn { + return &socketDiagConn{ + family: family, + protocol: protocol, + fd: -1, + } +} + +func socketDiagConnIndex(family, protocol uint8) int { + index := 0 + if protocol == syscall.IPPROTO_UDP { + index += 2 + } + if family == syscall.AF_INET6 { + index++ + } + return index +} + +func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) { + switch network { + case N.NetworkTCP: + protocol = syscall.IPPROTO_TCP + case N.NetworkUDP: + protocol = syscall.IPPROTO_UDP + default: + return 0, 0, os.ErrInvalid + } + switch { + case source.Addr().Is4(): + family = syscall.AF_INET + case source.Addr().Is6(): + family = syscall.AF_INET6 + default: + return 0, 0, os.ErrInvalid + } + return family, protocol, nil +} + +func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache { + cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32)) + cache.SetLifetime(ttl) + return &uidProcessPathCache{cache: cache} +} + +func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) { + if cached, ok := c.cache.Get(uid); ok { + if processPath, found := cached.entries[targetInode]; found { + return processPath, nil + } + } + processPaths, err := buildProcessPathByUIDCache(uid) + if err != nil { + return "", err + } + c.cache.Add(uid, &uidProcessPaths{entries: processPaths}) + processPath, found := processPaths[targetInode] + if !found { + return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found") + } + return processPath, nil +} + +func (c *socketDiagConn) Close() error { + c.access.Lock() + defer c.access.Unlock() + return c.closeLocked() +} + +func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + c.access.Lock() + defer c.access.Unlock() + request := packSocketDiagRequest(c.family, c.protocol, source, destination, false) + for attempt := 0; attempt < 2; attempt++ { + err = c.ensureOpenLocked() + if err != nil { + return 0, 0, E.Cause(err, "dial netlink") + } + inode, uid, err = querySocketDiag(c.fd, request) + if err == nil || errors.Is(err, ErrNotFound) { + return inode, uid, err + } + if !shouldRetrySocketDiag(err) { + return 0, 0, err + } + _ = c.closeLocked() + } + return 0, 0, err +} + +func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) { + fd, err := openSocketDiag() + if err != nil { + return 0, 0, E.Cause(err, "dial netlink") + } + defer syscall.Close(fd) + return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) +} + +func (c *socketDiagConn) ensureOpenLocked() error { + if c.fd != -1 { + return nil + } + fd, err := openSocketDiag() + if err != nil { + return err + } + c.fd = fd + return nil +} + +func openSocketDiag() (int, error) { + fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG) + if err != nil { + return -1, err + } + timeout := &syscall.Timeval{Usec: 100} + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil { + syscall.Close(fd) + return -1, err + } + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil { + syscall.Close(fd) + return -1, err + } + if err = syscall.Connect(fd, &syscall.SockaddrNetlink{ + Family: syscall.AF_NETLINK, + Pid: 0, + Groups: 0, + }); err != nil { + syscall.Close(fd) + return -1, err + } + return fd, nil +} + +func (c *socketDiagConn) closeLocked() error { + if c.fd == -1 { + return nil + } + err := syscall.Close(c.fd) + c.fd = -1 + return err +} + +func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte { + request := make([]byte, sizeOfSocketDiagRequest) + + binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest) + binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily) + flags := uint16(syscall.NLM_F_REQUEST) + if dump { + flags |= syscall.NLM_F_DUMP + } + binary.NativeEndian.PutUint16(request[6:8], flags) + binary.NativeEndian.PutUint32(request[8:12], 0) + binary.NativeEndian.PutUint32(request[12:16], 0) + + request[16] = family + request[17] = protocol + request[18] = 0 + request[19] = 0 + if dump { + binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF) + } + requestSource := source + requestDestination := destination + if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() { + // udp_dump_one expects the exact-match endpoints reversed for historical reasons. + requestSource, requestDestination = destination, source + } + binary.BigEndian.PutUint16(request[24:26], requestSource.Port()) + binary.BigEndian.PutUint16(request[26:28], requestDestination.Port()) + if family == syscall.AF_INET6 { + copy(request[28:44], requestSource.Addr().AsSlice()) + if requestDestination.IsValid() { + copy(request[44:60], requestDestination.Addr().AsSlice()) + } + } else { + copy(request[28:32], requestSource.Addr().AsSlice()) + if requestDestination.IsValid() { + copy(request[44:48], requestDestination.Addr().AsSlice()) + } + } + binary.NativeEndian.PutUint32(request[60:64], 0) + binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF) + return request +} + +func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) { + _, err = syscall.Write(fd, request) + if err != nil { + return 0, 0, E.Cause(err, "write netlink request") + } + buffer := make([]byte, 64<<10) + n, err := syscall.Read(fd, buffer) + if err != nil { + return 0, 0, E.Cause(err, "read netlink response") + } + messages, err := syscall.ParseNetlinkMessage(buffer[:n]) + if err != nil { + return 0, 0, E.Cause(err, "parse netlink message") + } + return unpackSocketDiagMessages(messages) +} + +func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) { + for _, message := range messages { + switch message.Header.Type { + case syscall.NLMSG_DONE: + continue + case syscall.NLMSG_ERROR: + err = unpackSocketDiagError(&message) + if err != nil { + return 0, 0, err + } + case socketDiagByFamily: + inode, uid = unpackSocketDiagResponse(&message) + if inode != 0 || uid != 0 { + return inode, uid, nil + } + } + } + return 0, 0, ErrNotFound +} + +func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { + if len(msg.Data) < socketDiagResponseMinSize { + return 0, 0 + } + uid = binary.NativeEndian.Uint32(msg.Data[64:68]) + inode = binary.NativeEndian.Uint32(msg.Data[68:72]) + return inode, uid +} + +func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { + if len(msg.Data) < 4 { + return E.New("netlink message: NLMSG_ERROR") + } + errno := int32(binary.NativeEndian.Uint32(msg.Data[:4])) + if errno == 0 { + return nil + } + if errno < 0 { + errno = -errno + } + sysErr := syscall.Errno(errno) + switch sysErr { + case syscall.ENOENT, syscall.ESRCH: + return ErrNotFound + default: + return E.New("netlink message: ", sysErr) + } +} + +func shouldRetrySocketDiag(err error) bool { + return err != nil && !errors.Is(err, ErrNotFound) +} + +func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) { + files, err := os.ReadDir(pathProc) + if err != nil { + return nil, err + } + buffer := make([]byte, syscall.PathMax) + processPaths := make(map[uint32]string) + for _, file := range files { + if !file.IsDir() || !isPid(file.Name()) { + continue + } + info, err := file.Info() + if err != nil { + if isIgnorableProcError(err) { + continue + } + return nil, err + } + if info.Sys().(*syscall.Stat_t).Uid != uid { + continue + } + processPath := filepath.Join(pathProc, file.Name()) + fdPath := filepath.Join(processPath, "fd") + exePath, err := os.Readlink(filepath.Join(processPath, "exe")) + if err != nil { + if isIgnorableProcError(err) { + continue + } + return nil, err + } + fds, err := os.ReadDir(fdPath) + if err != nil { + continue + } + for _, fd := range fds { + n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer) + if err != nil { + continue + } + inode, ok := parseSocketInode(buffer[:n]) + if !ok { + continue + } + if _, loaded := processPaths[inode]; !loaded { + processPaths[inode] = exePath + } + } + } + return processPaths, nil +} + +func isIgnorableProcError(err error) bool { + return os.IsNotExist(err) || os.IsPermission(err) +} + +func parseSocketInode(link []byte) (uint32, bool) { + const socketPrefix = "socket:[" + if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' { + return 0, false + } + var inode uint64 + for _, char := range link[len(socketPrefix) : len(link)-1] { + if char < '0' || char > '9' { + return 0, false + } + inode = inode*10 + uint64(char-'0') + if inode > uint64(^uint32(0)) { + return 0, false + } + } + return uint32(inode), true +} + +func isPid(s string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !unicode.IsDigit(r) + }) == -1 +} diff --git a/common/process/searcher_linux_shared_test.go b/common/process/searcher_linux_shared_test.go new file mode 100644 index 00000000..1befff4e --- /dev/null +++ b/common/process/searcher_linux_shared_test.go @@ -0,0 +1,60 @@ +//go:build linux + +package process + +import ( + "net" + "net/netip" + "os" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestQuerySocketDiagUDPExact(t *testing.T) { + t.Parallel() + server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) + require.NoError(t, err) + defer server.Close() + + client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr)) + require.NoError(t, err) + defer client.Close() + + err = client.SetDeadline(time.Now().Add(time.Second)) + require.NoError(t, err) + _, err = client.Write([]byte{0}) + require.NoError(t, err) + + err = server.SetReadDeadline(time.Now().Add(time.Second)) + require.NoError(t, err) + buffer := make([]byte, 1) + _, _, err = server.ReadFromUDP(buffer) + require.NoError(t, err) + + source := addrPortFromUDPAddr(t, client.LocalAddr()) + destination := addrPortFromUDPAddr(t, client.RemoteAddr()) + + fd, err := openSocketDiag() + require.NoError(t, err) + defer syscall.Close(fd) + + inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false)) + require.NoError(t, err) + require.NotZero(t, inode) + require.EqualValues(t, os.Getuid(), uid) +} + +func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort { + t.Helper() + + udpAddr, ok := addr.(*net.UDPAddr) + require.True(t, ok) + + ip, ok := netip.AddrFromSlice(udpAddr.IP) + require.True(t, ok) + + return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port)) +} diff --git a/common/process/searcher_stub.go b/common/process/searcher_stub.go new file mode 100644 index 00000000..4665d91f --- /dev/null +++ b/common/process/searcher_stub.go @@ -0,0 +1,11 @@ +//go:build !linux && !windows && !darwin + +package process + +import ( + "os" +) + +func NewSearcher(_ Config) (Searcher, error) { + return nil, os.ErrInvalid +} diff --git a/common/process/searcher_windows.go b/common/process/searcher_windows.go new file mode 100644 index 00000000..39695355 --- /dev/null +++ b/common/process/searcher_windows.go @@ -0,0 +1,66 @@ +package process + +import ( + "context" + "net/netip" + "syscall" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/winiphlpapi" + + "golang.org/x/sys/windows" +) + +var _ Searcher = (*windowsSearcher)(nil) + +type windowsSearcher struct{} + +func NewSearcher(_ Config) (Searcher, error) { + err := initWin32API() + if err != nil { + return nil, E.Cause(err, "init win32 api") + } + return &windowsSearcher{}, nil +} + +func initWin32API() error { + return winiphlpapi.LoadExtendedTable() +} + +func (s *windowsSearcher) Close() error { + return nil +} + +func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + pid, err := winiphlpapi.FindPid(network, source) + if err != nil { + return nil, err + } + path, err := getProcessPath(pid) + if err != nil { + return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err + } + return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil +} + +func getProcessPath(pid uint32) (string, error) { + switch pid { + case 0: + return ":System Idle Process", nil + case 4: + return ":System", nil + } + handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + if err != nil { + return "", err + } + defer windows.CloseHandle(handle) + size := uint32(syscall.MAX_LONG_PATH) + buf := make([]uint16, syscall.MAX_LONG_PATH) + err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size) + if err != nil { + return "", err + } + return windows.UTF16ToString(buf[:size]), nil +} diff --git a/common/redir/redir_darwin.go b/common/redir/redir_darwin.go new file mode 100644 index 00000000..d8691234 --- /dev/null +++ b/common/redir/redir_darwin.go @@ -0,0 +1,64 @@ +package redir + +import ( + "net" + "net/netip" + "syscall" + "unsafe" + + M "github.com/sagernet/sing/common/metadata" +) + +const ( + PF_OUT = 0x2 + DIOCNATLOOK = 0xc0544417 +) + +func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) { + fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY) + if err != nil { + return netip.AddrPort{}, err + } + defer syscall.Close(fd) + nl := struct { + saddr, daddr, rsaddr, rdaddr [16]byte + sxport, dxport, rsxport, rdxport [4]byte + af, proto, protoVariant, direction uint8 + }{ + af: syscall.AF_INET, + proto: syscall.IPPROTO_TCP, + direction: PF_OUT, + } + la := conn.LocalAddr().(*net.TCPAddr) + ra := conn.RemoteAddr().(*net.TCPAddr) + raIP, laIP := ra.IP, la.IP + raPort, laPort := ra.Port, la.Port + switch { + case raIP.To4() != nil: + copy(nl.saddr[:net.IPv4len], raIP.To4()) + copy(nl.daddr[:net.IPv4len], laIP.To4()) + nl.af = syscall.AF_INET + default: + copy(nl.saddr[:], raIP.To16()) + copy(nl.daddr[:], laIP.To16()) + nl.af = syscall.AF_INET6 + } + nl.sxport[0], nl.sxport[1] = byte(raPort>>8), byte(raPort) + nl.dxport[0], nl.dxport[1] = byte(laPort>>8), byte(laPort) + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 { + return netip.AddrPort{}, errno + } + + var ip net.IP + switch nl.af { + case syscall.AF_INET: + ip = make(net.IP, net.IPv4len) + copy(ip, nl.rdaddr[:net.IPv4len]) + case syscall.AF_INET6: + ip = make(net.IP, net.IPv6len) + copy(ip, nl.rdaddr[:]) + } + port := uint16(nl.rdxport[0])<<8 | uint16(nl.rdxport[1]) + destination = netip.AddrPortFrom(M.AddrFromIP(ip), port) + return +} diff --git a/common/redir/redir_linux.go b/common/redir/redir_linux.go new file mode 100644 index 00000000..5f61bb2a --- /dev/null +++ b/common/redir/redir_linux.go @@ -0,0 +1,40 @@ +package redir + +import ( + "encoding/binary" + "net" + "net/netip" + "os" + "syscall" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + M "github.com/sagernet/sing/common/metadata" +) + +func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) { + syscallConn, ok := common.Cast[syscall.Conn](conn) + if !ok { + return netip.AddrPort{}, os.ErrInvalid + } + err = control.Conn(syscallConn, func(fd uintptr) error { + const SO_ORIGINAL_DST = 80 + if conn.RemoteAddr().(*net.TCPAddr).IP.To4() != nil { + raw, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST) + if err != nil { + return err + } + destination = netip.AddrPortFrom(M.AddrFromIP(raw.Multiaddr[4:8]), uint16(raw.Multiaddr[2])<<8+uint16(raw.Multiaddr[3])) + } else { + raw, err := syscall.GetsockoptIPv6MTUInfo(int(fd), syscall.IPPROTO_IPV6, SO_ORIGINAL_DST) + if err != nil { + return err + } + var port [2]byte + binary.BigEndian.PutUint16(port[:], raw.Addr.Port) + destination = netip.AddrPortFrom(M.AddrFromIP(raw.Addr.Addr[:]), binary.LittleEndian.Uint16(port[:])) + } + return nil + }) + return +} diff --git a/common/redir/redir_other.go b/common/redir/redir_other.go new file mode 100644 index 00000000..3d60afeb --- /dev/null +++ b/common/redir/redir_other.go @@ -0,0 +1,13 @@ +//go:build !linux && !darwin + +package redir + +import ( + "net" + "net/netip" + "os" +) + +func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) { + return netip.AddrPort{}, os.ErrInvalid +} diff --git a/common/redir/tproxy_linux.go b/common/redir/tproxy_linux.go new file mode 100644 index 00000000..a3ccade0 --- /dev/null +++ b/common/redir/tproxy_linux.go @@ -0,0 +1,59 @@ +package redir + +import ( + "encoding/binary" + "net/netip" + "syscall" + + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + + "golang.org/x/sys/unix" +) + +func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error { + err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if err == nil { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) + } + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) + } + if isUDP { + if err == nil { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) + } + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) + } + } + return err +} + +func TProxyWriteBack() control.Func { + return func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + if M.ParseSocksaddr(address).Addr.Is6() { + return syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) + } else { + return syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) + } + }) + } +} + +func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) { + controlMessages, err := unix.ParseSocketControlMessage(oob) + if err != nil { + return netip.AddrPort{}, err + } + for _, message := range controlMessages { + if message.Header.Level == unix.SOL_IP && message.Header.Type == unix.IP_RECVORIGDSTADDR { + return netip.AddrPortFrom(M.AddrFromIP(message.Data[4:8]), binary.BigEndian.Uint16(message.Data[2:4])), nil + } else if message.Header.Level == unix.SOL_IPV6 && message.Header.Type == unix.IPV6_RECVORIGDSTADDR { + return netip.AddrPortFrom(M.AddrFromIP(message.Data[8:24]), binary.BigEndian.Uint16(message.Data[2:4])), nil + } + } + return netip.AddrPort{}, E.New("not found") +} diff --git a/common/redir/tproxy_other.go b/common/redir/tproxy_other.go new file mode 100644 index 00000000..42e31fee --- /dev/null +++ b/common/redir/tproxy_other.go @@ -0,0 +1,22 @@ +//go:build !linux + +package redir + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing/common/control" +) + +func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error { + return os.ErrInvalid +} + +func TProxyWriteBack() control.Func { + return nil +} + +func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) { + return netip.AddrPort{}, os.ErrInvalid +} diff --git a/common/settings/proxy_android.go b/common/settings/proxy_android.go new file mode 100644 index 00000000..d3768ced --- /dev/null +++ b/common/settings/proxy_android.go @@ -0,0 +1,73 @@ +package settings + +import ( + "context" + "os" + "strings" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/shell" +) + +type AndroidSystemProxy struct { + useRish bool + rishPath string + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} + +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) { + userId := os.Getuid() + var ( + useRish bool + rishPath string + ) + if userId == 0 || userId == 1000 || userId == 2000 { + useRish = false + } else { + rishPath, useRish = C.FindPath("rish") + if !useRish { + return nil, E.Cause(os.ErrPermission, "root or system (adb) permission is required for set system proxy") + } + } + return &AndroidSystemProxy{ + useRish: useRish, + rishPath: rishPath, + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, + }, nil +} + +func (p *AndroidSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *AndroidSystemProxy) Enable() error { + err := p.runAndroidShell("settings", "put", "global", "http_proxy", p.serverAddr.String()) + if err != nil { + return err + } + p.isEnabled = true + return nil +} + +func (p *AndroidSystemProxy) Disable() error { + err := p.runAndroidShell("settings", "put", "global", "http_proxy", ":0") + if err != nil { + return err + } + p.isEnabled = false + return nil +} + +func (p *AndroidSystemProxy) runAndroidShell(name string, args ...string) error { + if !p.useRish { + return shell.Exec(name, args...).Attach().Run() + } else { + return shell.Exec("sh", p.rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() + } +} diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go new file mode 100644 index 00000000..53ed0fe0 --- /dev/null +++ b/common/settings/proxy_darwin.go @@ -0,0 +1,121 @@ +package settings + +import ( + "context" + "strconv" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/shell" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" +) + +type DarwinSystemProxy struct { + monitor tun.DefaultInterfaceMonitor + interfaceName string + element *list.Element[tun.DefaultInterfaceUpdateCallback] + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} + +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { + interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() + if interfaceMonitor == nil { + return nil, E.New("missing interface monitor") + } + proxy := &DarwinSystemProxy{ + monitor: interfaceMonitor, + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, + } + proxy.element = interfaceMonitor.RegisterCallback(proxy.routeUpdate) + return proxy, nil +} + +func (p *DarwinSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *DarwinSystemProxy) Enable() error { + return p.update0() +} + +func (p *DarwinSystemProxy) Disable() error { + interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) + if err != nil { + return err + } + if p.supportSOCKS { + err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run() + } + if err == nil { + err = shell.Exec("networksetup", "-setwebproxystate", interfaceDisplayName, "off").Attach().Run() + } + if err == nil { + err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run() + } + if err == nil { + p.isEnabled = false + } + return err +} + +func (p *DarwinSystemProxy) routeUpdate(defaultInterface *control.Interface, flags int) { + if !p.isEnabled || defaultInterface == nil { + return + } + _ = p.update0() +} + +func (p *DarwinSystemProxy) update0() error { + newInterface := p.monitor.DefaultInterface() + if p.interfaceName == newInterface.Name { + return nil + } + if p.interfaceName != "" { + _ = p.Disable() + } + p.interfaceName = newInterface.Name + interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) + if err != nil { + return err + } + if p.supportSOCKS { + err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() + } + if err != nil { + return err + } + err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() + if err != nil { + return err + } + err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() + if err != nil { + return err + } + p.isEnabled = true + return nil +} + +func getInterfaceDisplayName(name string) (string, error) { + content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput() + if err != nil { + return "", err + } + for _, deviceSpan := range strings.Split(string(content), "Ethernet Address") { + if strings.Contains(deviceSpan, "Device: "+name) { + substr := "Hardware Port: " + deviceSpan = deviceSpan[strings.Index(deviceSpan, substr)+len(substr):] + deviceSpan = deviceSpan[:strings.Index(deviceSpan, "\n")] + return deviceSpan, nil + } + } + return "", E.New(name, " not found in networksetup -listallhardwareports") +} diff --git a/common/settings/proxy_linux.go b/common/settings/proxy_linux.go new file mode 100644 index 00000000..84e27b12 --- /dev/null +++ b/common/settings/proxy_linux.go @@ -0,0 +1,176 @@ +//go:build linux && !android + +package settings + +import ( + "context" + "os" + "os/exec" + "strings" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/shell" +) + +type LinuxSystemProxy struct { + hasGSettings bool + kWriteConfigCmd string + sudoUser string + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} + +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) { + hasGSettings := common.Error(exec.LookPath("gsettings")) == nil + kWriteConfigCmds := []string{ + "kwriteconfig5", + "kwriteconfig6", + } + var kWriteConfigCmd string + for _, cmd := range kWriteConfigCmds { + if common.Error(exec.LookPath(cmd)) == nil { + kWriteConfigCmd = cmd + break + } + } + var sudoUser string + if os.Getuid() == 0 { + sudoUser = os.Getenv("SUDO_USER") + } + if !hasGSettings && kWriteConfigCmd == "" { + return nil, E.New("unsupported desktop environment") + } + return &LinuxSystemProxy{ + hasGSettings: hasGSettings, + kWriteConfigCmd: kWriteConfigCmd, + sudoUser: sudoUser, + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, + }, nil +} + +func (p *LinuxSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *LinuxSystemProxy) Enable() error { + if p.hasGSettings { + err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true") + if err != nil { + return err + } + if p.supportSOCKS { + err = p.setGnomeProxy("ftp", "http", "https", "socks") + } else { + err = p.setGnomeProxy("http", "https") + } + if err != nil { + return err + } + err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(p.supportSOCKS)) + if err != nil { + return err + } + err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual") + if err != nil { + return err + } + } + if p.kWriteConfigCmd != "" { + err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1") + if err != nil { + return err + } + if p.supportSOCKS { + err = p.setKDEProxy("ftp", "http", "https", "socks") + } else { + err = p.setKDEProxy("http", "https") + } + if err != nil { + return err + } + err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0") + if err != nil { + return err + } + err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") + if err != nil { + return err + } + } + p.isEnabled = true + return nil +} + +func (p *LinuxSystemProxy) Disable() error { + if p.hasGSettings { + err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none") + if err != nil { + return err + } + } + if p.kWriteConfigCmd != "" { + err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0") + if err != nil { + return err + } + err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") + if err != nil { + return err + } + } + p.isEnabled = false + return nil +} + +func (p *LinuxSystemProxy) runAsUser(name string, args ...string) error { + if os.Getuid() != 0 { + return shell.Exec(name, args...).Attach().Run() + } else if p.sudoUser != "" { + return shell.Exec("su", "-", p.sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() + } else { + return E.New("set system proxy: unable to set as root") + } +} + +func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error { + for _, proxyType := range proxyTypes { + err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString()) + if err != nil { + return err + } + err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port)) + if err != nil { + return err + } + } + return nil +} + +func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error { + for _, proxyType := range proxyTypes { + var proxyUrl string + if proxyType == "socks" { + proxyUrl = "socks://" + p.serverAddr.String() + } else { + proxyUrl = "http://" + p.serverAddr.String() + } + err := p.runAsUser( + p.kWriteConfigCmd, + "--file", + "kioslaverc", + "--group", + "Proxy Settings", + "--key", proxyType+"Proxy", + proxyUrl, + ) + if err != nil { + return err + } + } + return nil +} diff --git a/common/settings/proxy_stub.go b/common/settings/proxy_stub.go new file mode 100644 index 00000000..56eb4b65 --- /dev/null +++ b/common/settings/proxy_stub.go @@ -0,0 +1,14 @@ +//go:build !(windows || linux || darwin) + +package settings + +import ( + "context" + "os" + + M "github.com/sagernet/sing/common/metadata" +) + +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (SystemProxy, error) { + return nil, os.ErrInvalid +} diff --git a/common/settings/proxy_windows.go b/common/settings/proxy_windows.go new file mode 100644 index 00000000..793ac1d1 --- /dev/null +++ b/common/settings/proxy_windows.go @@ -0,0 +1,43 @@ +package settings + +import ( + "context" + + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/wininet" +) + +type WindowsSystemProxy struct { + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} + +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*WindowsSystemProxy, error) { + return &WindowsSystemProxy{ + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, + }, nil +} + +func (p *WindowsSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *WindowsSystemProxy) Enable() error { + err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), "") + if err != nil { + return err + } + p.isEnabled = true + return nil +} + +func (p *WindowsSystemProxy) Disable() error { + err := wininet.ClearSystemProxy() + if err != nil { + return err + } + p.isEnabled = false + return nil +} diff --git a/common/settings/system_proxy.go b/common/settings/system_proxy.go new file mode 100644 index 00000000..0635c6f6 --- /dev/null +++ b/common/settings/system_proxy.go @@ -0,0 +1,7 @@ +package settings + +type SystemProxy interface { + IsEnabled() bool + Enable() error + Disable() error +} diff --git a/common/settings/wifi.go b/common/settings/wifi.go new file mode 100644 index 00000000..62bef706 --- /dev/null +++ b/common/settings/wifi.go @@ -0,0 +1,9 @@ +package settings + +import "github.com/sagernet/sing-box/adapter" + +type WIFIMonitor interface { + ReadWIFIState() adapter.WIFIState + Start() error + Close() error +} diff --git a/common/settings/wifi_linux.go b/common/settings/wifi_linux.go new file mode 100644 index 00000000..9deed3c8 --- /dev/null +++ b/common/settings/wifi_linux.go @@ -0,0 +1,46 @@ +package settings + +import ( + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" +) + +type LinuxWIFIMonitor struct { + monitor WIFIMonitor +} + +func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + monitors := []func(func(adapter.WIFIState)) (WIFIMonitor, error){ + newNetworkManagerMonitor, + newIWDMonitor, + newWpaSupplicantMonitor, + newConnManMonitor, + } + var errors []error + for _, factory := range monitors { + monitor, err := factory(callback) + if err == nil { + return &LinuxWIFIMonitor{monitor: monitor}, nil + } + errors = append(errors, err) + } + return nil, E.Cause(E.Errors(errors...), "no supported WIFI manager found") +} + +func (m *LinuxWIFIMonitor) ReadWIFIState() adapter.WIFIState { + return m.monitor.ReadWIFIState() +} + +func (m *LinuxWIFIMonitor) Start() error { + if m.monitor != nil { + return m.monitor.Start() + } + return nil +} + +func (m *LinuxWIFIMonitor) Close() error { + if m.monitor != nil { + return m.monitor.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_connman.go b/common/settings/wifi_linux_connman.go new file mode 100644 index 00000000..74706a7b --- /dev/null +++ b/common/settings/wifi_linux_connman.go @@ -0,0 +1,168 @@ +//go:build linux + +package settings + +import ( + "context" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + + "github.com/godbus/dbus/v5" +) + +type connmanMonitor struct { + conn *dbus.Conn + callback func(adapter.WIFIState) + cancel context.CancelFunc + signalChan chan *dbus.Signal +} + +func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, err + } + cmObj := conn.Object("net.connman", "/") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0) + if call.Err != nil { + conn.Close() + return nil, call.Err + } + return &connmanMonitor{conn: conn, callback: callback}, nil +} + +func (m *connmanMonitor) ReadWIFIState() adapter.WIFIState { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + cmObj := m.conn.Object("net.connman", "/") + var services []interface{} + err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services) + if err != nil { + return adapter.WIFIState{} + } + + for _, service := range services { + servicePair, ok := service.([]interface{}) + if !ok || len(servicePair) != 2 { + continue + } + + serviceProps, ok := servicePair[1].(map[string]dbus.Variant) + if !ok { + continue + } + + typeVariant, hasType := serviceProps["Type"] + if !hasType { + continue + } + serviceType, ok := typeVariant.Value().(string) + if !ok || serviceType != "wifi" { + continue + } + + stateVariant, hasState := serviceProps["State"] + if !hasState { + continue + } + state, ok := stateVariant.Value().(string) + if !ok || (state != "online" && state != "ready") { + continue + } + + nameVariant, hasName := serviceProps["Name"] + if !hasName { + continue + } + ssid, ok := nameVariant.Value().(string) + if !ok || ssid == "" { + continue + } + + bssidVariant, hasBSSID := serviceProps["BSSID"] + if !hasBSSID { + return adapter.WIFIState{SSID: ssid} + } + bssid, ok := bssidVariant.Value().(string) + if !ok { + return adapter.WIFIState{SSID: ssid} + } + + return adapter.WIFIState{ + SSID: ssid, + BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), + } + } + + return adapter.WIFIState{} +} + +func (m *connmanMonitor) Start() error { + if m.callback == nil { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + m.signalChan = make(chan *dbus.Signal, 10) + m.conn.Signal(m.signalChan) + + err := m.conn.AddMatchSignal( + dbus.WithMatchInterface("net.connman.Service"), + dbus.WithMatchSender("net.connman"), + ) + if err != nil { + return err + } + + state := m.ReadWIFIState() + go m.monitorSignals(ctx, m.signalChan, state) + m.callback(state) + + return nil +} + +func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { + for { + select { + case <-ctx.Done(): + return + case signal, ok := <-signalChan: + if !ok { + return + } + // godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.PropertyChanged"), + // not just the member name. This differs from the D-Bus signal member in the match rule. + if signal.Name == "net.connman.Service.PropertyChanged" { + state := m.ReadWIFIState() + if state != lastState { + lastState = state + m.callback(state) + } + } + } + } +} + +func (m *connmanMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + if m.signalChan != nil { + m.conn.RemoveSignal(m.signalChan) + close(m.signalChan) + } + if m.conn != nil { + m.conn.RemoveMatchSignal( + dbus.WithMatchInterface("net.connman.Service"), + dbus.WithMatchSender("net.connman"), + ) + return m.conn.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_iwd.go b/common/settings/wifi_linux_iwd.go new file mode 100644 index 00000000..327f9c47 --- /dev/null +++ b/common/settings/wifi_linux_iwd.go @@ -0,0 +1,190 @@ +//go:build linux + +package settings + +import ( + "context" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + + "github.com/godbus/dbus/v5" +) + +type iwdMonitor struct { + conn *dbus.Conn + callback func(adapter.WIFIState) + cancel context.CancelFunc + signalChan chan *dbus.Signal +} + +func newIWDMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, err + } + iwdObj := conn.Object("net.connman.iwd", "/") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + call := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0) + if call.Err != nil { + conn.Close() + return nil, call.Err + } + return &iwdMonitor{conn: conn, callback: callback}, nil +} + +func (m *iwdMonitor) ReadWIFIState() adapter.WIFIState { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + iwdObj := m.conn.Object("net.connman.iwd", "/") + var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant + err := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects) + if err != nil { + return adapter.WIFIState{} + } + + for _, interfaces := range objects { + stationProps, hasStation := interfaces["net.connman.iwd.Station"] + if !hasStation { + continue + } + + stateVariant, hasState := stationProps["State"] + if !hasState { + continue + } + state, ok := stateVariant.Value().(string) + if !ok || state != "connected" { + continue + } + + connectedNetworkVariant, hasNetwork := stationProps["ConnectedNetwork"] + if !hasNetwork { + continue + } + networkPath, ok := connectedNetworkVariant.Value().(dbus.ObjectPath) + if !ok || networkPath == "/" { + continue + } + + networkInterfaces, hasNetworkPath := objects[networkPath] + if !hasNetworkPath { + continue + } + + networkProps, hasNetworkInterface := networkInterfaces["net.connman.iwd.Network"] + if !hasNetworkInterface { + continue + } + + nameVariant, hasName := networkProps["Name"] + if !hasName { + continue + } + ssid, ok := nameVariant.Value().(string) + if !ok { + continue + } + + connectedBSSVariant, hasBSS := stationProps["ConnectedAccessPoint"] + if !hasBSS { + return adapter.WIFIState{SSID: ssid} + } + bssPath, ok := connectedBSSVariant.Value().(dbus.ObjectPath) + if !ok || bssPath == "/" { + return adapter.WIFIState{SSID: ssid} + } + + bssInterfaces, hasBSSPath := objects[bssPath] + if !hasBSSPath { + return adapter.WIFIState{SSID: ssid} + } + + bssProps, hasBSSInterface := bssInterfaces["net.connman.iwd.BasicServiceSet"] + if !hasBSSInterface { + return adapter.WIFIState{SSID: ssid} + } + + addressVariant, hasAddress := bssProps["Address"] + if !hasAddress { + return adapter.WIFIState{SSID: ssid} + } + bssid, ok := addressVariant.Value().(string) + if !ok { + return adapter.WIFIState{SSID: ssid} + } + + return adapter.WIFIState{ + SSID: ssid, + BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), + } + } + + return adapter.WIFIState{} +} + +func (m *iwdMonitor) Start() error { + if m.callback == nil { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + m.signalChan = make(chan *dbus.Signal, 10) + m.conn.Signal(m.signalChan) + + err := m.conn.AddMatchSignal( + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchSender("net.connman.iwd"), + ) + if err != nil { + return err + } + + state := m.ReadWIFIState() + go m.monitorSignals(ctx, m.signalChan, state) + m.callback(state) + + return nil +} + +func (m *iwdMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { + for { + select { + case <-ctx.Done(): + return + case signal, ok := <-signalChan: + if !ok { + return + } + if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" { + state := m.ReadWIFIState() + if state != lastState { + lastState = state + m.callback(state) + } + } + } + } +} + +func (m *iwdMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + if m.signalChan != nil { + m.conn.RemoveSignal(m.signalChan) + close(m.signalChan) + } + if m.conn != nil { + m.conn.RemoveMatchSignal( + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchSender("net.connman.iwd"), + ) + return m.conn.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_nm.go b/common/settings/wifi_linux_nm.go new file mode 100644 index 00000000..77d897d4 --- /dev/null +++ b/common/settings/wifi_linux_nm.go @@ -0,0 +1,165 @@ +//go:build linux + +package settings + +import ( + "context" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + + "github.com/godbus/dbus/v5" +) + +type networkManagerMonitor struct { + conn *dbus.Conn + callback func(adapter.WIFIState) + cancel context.CancelFunc + signalChan chan *dbus.Signal +} + +func newNetworkManagerMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, err + } + nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + var state uint32 + err = nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "State").Store(&state) + if err != nil { + conn.Close() + return nil, err + } + return &networkManagerMonitor{conn: conn, callback: callback}, nil +} + +func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") + + var activeConnectionPaths []dbus.ObjectPath + err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "ActiveConnections").Store(&activeConnectionPaths) + if err != nil || len(activeConnectionPaths) == 0 { + return adapter.WIFIState{} + } + + for _, connectionPath := range activeConnectionPaths { + connObj := m.conn.Object("org.freedesktop.NetworkManager", connectionPath) + + var devicePaths []dbus.ObjectPath + err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths) + if err != nil || len(devicePaths) == 0 { + continue + } + + for _, devicePath := range devicePaths { + deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath) + + var deviceType uint32 + err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType) + if err != nil || deviceType != 2 { + continue + } + + var accessPointPath dbus.ObjectPath + err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath) + if err != nil || accessPointPath == "/" { + continue + } + + apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath) + + var ssidBytes []byte + err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes) + if err != nil { + continue + } + + var hwAddress string + err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress) + if err != nil { + continue + } + + ssid := strings.TrimSpace(string(ssidBytes)) + if ssid == "" { + continue + } + + return adapter.WIFIState{ + SSID: ssid, + BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")), + } + } + } + + return adapter.WIFIState{} +} + +func (m *networkManagerMonitor) Start() error { + if m.callback == nil { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + m.signalChan = make(chan *dbus.Signal, 10) + m.conn.Signal(m.signalChan) + + err := m.conn.AddMatchSignal( + dbus.WithMatchSender("org.freedesktop.NetworkManager"), + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + ) + if err != nil { + return err + } + + state := m.ReadWIFIState() + go m.monitorSignals(ctx, m.signalChan, state) + m.callback(state) + + return nil +} + +func (m *networkManagerMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { + for { + select { + case <-ctx.Done(): + return + case signal, ok := <-signalChan: + if !ok { + return + } + if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" { + state := m.ReadWIFIState() + if state != lastState { + lastState = state + m.callback(state) + } + } + } + } +} + +func (m *networkManagerMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + if m.signalChan != nil { + m.conn.RemoveSignal(m.signalChan) + close(m.signalChan) + } + if m.conn != nil { + m.conn.RemoveMatchSignal( + dbus.WithMatchSender("org.freedesktop.NetworkManager"), + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + ) + return m.conn.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_wpa.go b/common/settings/wifi_linux_wpa.go new file mode 100644 index 00000000..51e76c1c --- /dev/null +++ b/common/settings/wifi_linux_wpa.go @@ -0,0 +1,225 @@ +package settings + +import ( + "bufio" + "context" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" +) + +var wpaSocketCounter atomic.Uint64 + +type wpaSupplicantMonitor struct { + socketPath string + callback func(adapter.WIFIState) + cancel context.CancelFunc + monitorConn *net.UnixConn + connMutex sync.Mutex +} + +func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + socketDirs := []string{"/var/run/wpa_supplicant", "/run/wpa_supplicant"} + for _, socketDir := range socketDirs { + entries, err := os.ReadDir(socketDir) + if err != nil { + continue + } + for _, entry := range entries { + if entry.IsDir() || entry.Name() == "." || entry.Name() == ".." { + continue + } + socketPath := filepath.Join(socketDir, entry.Name()) + id := wpaSocketCounter.Add(1) + localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"} + remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"} + conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) + if err != nil { + continue + } + conn.Close() + return &wpaSupplicantMonitor{socketPath: socketPath, callback: callback}, nil + } + } + return nil, os.ErrNotExist +} + +func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState { + id := wpaSocketCounter.Add(1) + localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"} + remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} + conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) + if err != nil { + return adapter.WIFIState{} + } + defer conn.Close() + + conn.SetDeadline(time.Now().Add(3 * time.Second)) + + status, err := m.sendCommand(conn, "STATUS") + if err != nil { + return adapter.WIFIState{} + } + + var ssid, bssid string + var connected bool + scanner := bufio.NewScanner(strings.NewReader(status)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "wpa_state=") { + state := strings.TrimPrefix(line, "wpa_state=") + connected = state == "COMPLETED" + } else if strings.HasPrefix(line, "ssid=") { + ssid = strings.TrimPrefix(line, "ssid=") + } else if strings.HasPrefix(line, "bssid=") { + bssid = strings.TrimPrefix(line, "bssid=") + } + } + + if !connected || ssid == "" { + return adapter.WIFIState{} + } + + return adapter.WIFIState{ + SSID: ssid, + BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), + } +} + +// sendCommand sends a command to wpa_supplicant and returns the response. +// Commands are sent without trailing newlines per the wpa_supplicant control +// interface protocol - the official wpa_ctrl.c sends raw command strings. +func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) { + _, err := conn.Write([]byte(command)) + if err != nil { + return "", err + } + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return "", err + } + + response := string(buf[:n]) + if strings.HasPrefix(response, "FAIL") { + return "", os.ErrInvalid + } + + return strings.TrimSpace(response), nil +} + +func (m *wpaSupplicantMonitor) Start() error { + if m.callback == nil { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + state := m.ReadWIFIState() + go m.monitorEvents(ctx, state) + m.callback(state) + + return nil +} + +func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) { + var consecutiveErrors int + var debounceTimer *time.Timer + var debounceMutex sync.Mutex + + localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"} + remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} + conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) + if err != nil { + return + } + defer conn.Close() + + m.connMutex.Lock() + m.monitorConn = conn + m.connMutex.Unlock() + + // ATTACH/DETACH commands use os_strcmp() for exact matching in wpa_supplicant, + // so they must be sent without trailing newlines. + // See: https://w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface_unix.c + _, err = conn.Write([]byte("ATTACH")) + if err != nil { + return + } + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil || !strings.HasPrefix(string(buf[:n]), "OK") { + return + } + + for { + select { + case <-ctx.Done(): + debounceMutex.Lock() + if debounceTimer != nil { + debounceTimer.Stop() + } + debounceMutex.Unlock() + conn.Write([]byte("DETACH")) + return + default: + } + + conn.SetReadDeadline(time.Now().Add(30 * time.Second)) + n, err := conn.Read(buf) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue + } + select { + case <-ctx.Done(): + return + default: + } + consecutiveErrors++ + if consecutiveErrors > 10 { + return + } + time.Sleep(time.Second) + continue + } + consecutiveErrors = 0 + + msg := string(buf[:n]) + if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") { + debounceMutex.Lock() + if debounceTimer != nil { + debounceTimer.Stop() + } + debounceTimer = time.AfterFunc(500*time.Millisecond, func() { + state := m.ReadWIFIState() + if state != lastState { + lastState = state + m.callback(state) + } + }) + debounceMutex.Unlock() + } + } +} + +func (m *wpaSupplicantMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + m.connMutex.Lock() + if m.monitorConn != nil { + m.monitorConn.Close() + } + m.connMutex.Unlock() + return nil +} diff --git a/common/settings/wifi_stub.go b/common/settings/wifi_stub.go new file mode 100644 index 00000000..fd39af9e --- /dev/null +++ b/common/settings/wifi_stub.go @@ -0,0 +1,27 @@ +//go:build !linux && !windows + +package settings + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" +) + +type stubWIFIMonitor struct{} + +func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + return nil, os.ErrInvalid +} + +func (m *stubWIFIMonitor) ReadWIFIState() adapter.WIFIState { + return adapter.WIFIState{} +} + +func (m *stubWIFIMonitor) Start() error { + return nil +} + +func (m *stubWIFIMonitor) Close() error { + return nil +} diff --git a/common/settings/wifi_windows.go b/common/settings/wifi_windows.go new file mode 100644 index 00000000..91b0d479 --- /dev/null +++ b/common/settings/wifi_windows.go @@ -0,0 +1,144 @@ +//go:build windows + +package settings + +import ( + "context" + "fmt" + "strings" + "sync" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/winwlanapi" + + "golang.org/x/sys/windows" +) + +type windowsWIFIMonitor struct { + handle windows.Handle + callback func(adapter.WIFIState) + cancel context.CancelFunc + lastState adapter.WIFIState + mutex sync.Mutex +} + +func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + handle, err := winwlanapi.OpenHandle() + if err != nil { + return nil, err + } + + interfaces, err := winwlanapi.EnumInterfaces(handle) + if err != nil { + winwlanapi.CloseHandle(handle) + return nil, err + } + if len(interfaces) == 0 { + winwlanapi.CloseHandle(handle) + return nil, fmt.Errorf("no wireless interfaces found") + } + + return &windowsWIFIMonitor{ + handle: handle, + callback: callback, + }, nil +} + +func (m *windowsWIFIMonitor) ReadWIFIState() adapter.WIFIState { + interfaces, err := winwlanapi.EnumInterfaces(m.handle) + if err != nil || len(interfaces) == 0 { + return adapter.WIFIState{} + } + + for _, iface := range interfaces { + if iface.InterfaceState != winwlanapi.InterfaceStateConnected { + continue + } + + guid := iface.InterfaceGUID + attrs, err := winwlanapi.QueryCurrentConnection(m.handle, &guid) + if err != nil { + continue + } + + ssidLength := attrs.AssociationAttributes.SSID.Length + if ssidLength == 0 || ssidLength > winwlanapi.Dot11SSIDMaxLength { + continue + } + + ssid := string(attrs.AssociationAttributes.SSID.SSID[:ssidLength]) + bssid := formatBSSID(attrs.AssociationAttributes.BSSID) + + return adapter.WIFIState{ + SSID: strings.TrimSpace(ssid), + BSSID: bssid, + } + } + + return adapter.WIFIState{} +} + +func formatBSSID(mac winwlanapi.Dot11MacAddress) string { + return fmt.Sprintf("%02X%02X%02X%02X%02X%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) +} + +func (m *windowsWIFIMonitor) Start() error { + if m.callback == nil { + return nil + } + + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + m.lastState = m.ReadWIFIState() + + callbackFunc := func(data *winwlanapi.NotificationData, callbackContext uintptr) uintptr { + if data.NotificationSource != winwlanapi.NotificationSourceACM { + return 0 + } + switch data.NotificationCode { + case winwlanapi.NotificationACMConnectionComplete, + winwlanapi.NotificationACMDisconnected: + m.checkAndNotify() + } + return 0 + } + + callbackPointer := syscall.NewCallback(callbackFunc) + + err := winwlanapi.RegisterNotification(m.handle, winwlanapi.NotificationSourceACM, callbackPointer, 0) + if err != nil { + cancel() + return err + } + + go func() { + <-ctx.Done() + }() + + m.callback(m.lastState) + return nil +} + +func (m *windowsWIFIMonitor) checkAndNotify() { + m.mutex.Lock() + defer m.mutex.Unlock() + + state := m.ReadWIFIState() + if state != m.lastState { + m.lastState = state + if m.callback != nil { + m.callback(state) + } + } +} + +func (m *windowsWIFIMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + winwlanapi.UnregisterNotification(m.handle) + return winwlanapi.CloseHandle(m.handle) +} diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go new file mode 100644 index 00000000..e4d9f4b8 --- /dev/null +++ b/common/sniff/bittorrent.go @@ -0,0 +1,107 @@ +package sniff + +import ( + "bytes" + "context" + "encoding/binary" + "io" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + trackerConnectFlag = 0 + trackerProtocolID = 0x41727101980 + trackerConnectMinSize = 16 +) + +// BitTorrent detects if the stream is a BitTorrent connection. +// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html +func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + var first byte + err := binary.Read(reader, binary.BigEndian, &first) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + + if first != 19 { + return os.ErrInvalid + } + + const header = "BitTorrent protocol" + var protocol [19]byte + var n int + n, err = reader.Read(protocol[:]) + if string(protocol[:n]) != header[:n] { + return os.ErrInvalid + } + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if n < 19 { + return ErrNeedMoreData + } + + metadata.Protocol = C.ProtocolBitTorrent + return nil +} + +// UTP detects if the packet is a uTP connection packet. +// For the uTP protocol specification, see +// 1. https://www.bittorrent.org/beps/bep_0029.html +// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112 +func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { + // A valid uTP packet must be at least 20 bytes long. + if len(packet) < 20 { + return os.ErrInvalid + } + + version := packet[0] & 0x0F + ty := packet[0] >> 4 + if version != 1 || ty > 4 { + return os.ErrInvalid + } + + // Validate the extensions + extension := packet[1] + reader := bytes.NewReader(packet[20:]) + for extension != 0 { + err := binary.Read(reader, binary.BigEndian, &extension) + if err != nil { + return err + } + if extension > 0x04 { + return os.ErrInvalid + } + var length byte + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return err + } + _, err = reader.Seek(int64(length), io.SeekCurrent) + if err != nil { + return err + } + } + metadata.Protocol = C.ProtocolBitTorrent + return nil +} + +// UDPTracker detects if the packet is a UDP Tracker Protocol packet. +// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html +func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { + if len(packet) < trackerConnectMinSize { + return os.ErrInvalid + } + if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID { + return os.ErrInvalid + } + if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolBitTorrent + return nil +} diff --git a/common/sniff/bittorrent_test.go b/common/sniff/bittorrent_test.go new file mode 100644 index 00000000..fcb5f6fa --- /dev/null +++ b/common/sniff/bittorrent_test.go @@ -0,0 +1,110 @@ +package sniff_test + +import ( + "bytes" + "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 TestSniffBittorrent(t *testing.T) { + t.Parallel() + + packets := []string{ + "13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1", + "13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638", + "13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130", + } + + for _, pkt := range packets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) + } +} + +func TestSniffIncompleteBittorrent(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("13426974546f7272656e74") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) +} + +func TestSniffNotBittorrent(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("13426974546f7272656e75") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NotEmpty(t, err) + require.NotErrorIs(t, err, sniff.ErrNeedMoreData) +} + +func TestSniffUTP(t *testing.T) { + t.Parallel() + + packets := []string{ + "010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8", + "01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb", + "21001ecb6817f2805d044fd700100000dbd03029", + "410277ef0b1fb1f60000000000040000c233000000080000000000000000", + } + + 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.NoError(t, err) + require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) + } +} + +func TestSniffUDPTracker(t *testing.T) { + t.Parallel() + + connectPackets := []string{ + "00000417271019800000000078e90560", + "00000417271019800000000022c5d64d", + "000004172710198000000000b3863541", + } + + for _, pkt := range connectPackets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + + var metadata adapter.InboundContext + err = sniff.UDPTracker(context.TODO(), &metadata, pkt) + require.NoError(t, err) + 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) + } +} diff --git a/common/sniff/dns.go b/common/sniff/dns.go new file mode 100644 index 00000000..7125a08e --- /dev/null +++ b/common/sniff/dns.go @@ -0,0 +1,57 @@ +package sniff + +import ( + "context" + "encoding/binary" + "io" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + + mDNS "github.com/miekg/dns" +) + +func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + var length uint16 + err := binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if length < 12 { + return os.ErrInvalid + } + buffer := buf.NewSize(int(length)) + defer buffer.Release() + var n int + n, err = buffer.ReadFullFrom(reader, buffer.FreeLen()) + packet := buffer.Bytes() + if n > 2 && packet[2]&0x80 != 0 { // QR + return os.ErrInvalid + } + if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT + return os.ErrInvalid + } + for i := 6; i < 10; i++ { + // ANCOUNT, NSCOUNT + if n > i && packet[i] != 0 { + return os.ErrInvalid + } + } + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + return DomainNameQuery(readCtx, metadata, packet) +} + +func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { + var msg mDNS.Msg + err := msg.Unpack(packet) + if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 { + return err + } + metadata.Protocol = C.ProtocolDNS + return nil +} diff --git a/common/sniff/dns_test.go b/common/sniff/dns_test.go new file mode 100644 index 00000000..d78b0bf5 --- /dev/null +++ b/common/sniff/dns_test.go @@ -0,0 +1,53 @@ +package sniff_test + +import ( + "bytes" + "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) +} + +func TestSniffStreamDNS(t *testing.T) { + t.Parallel() + query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query)) + require.NoError(t, err) + require.Equal(t, C.ProtocolDNS, metadata.Protocol) +} + +func TestSniffIncompleteStreamDNS(t *testing.T) { + t.Parallel() + query, err := hex.DecodeString("001e740701000001000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query)) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) +} + +func TestSniffNotStreamDNS(t *testing.T) { + t.Parallel() + query, err := hex.DecodeString("001e740701000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query)) + require.NotEmpty(t, err) + require.NotErrorIs(t, err, sniff.ErrNeedMoreData) +} diff --git a/common/sniff/dtls.go b/common/sniff/dtls.go new file mode 100644 index 00000000..e2704a27 --- /dev/null +++ b/common/sniff/dtls.go @@ -0,0 +1,32 @@ +package sniff + +import ( + "context" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { + const fixedHeaderSize = 13 + if len(packet) < fixedHeaderSize { + return os.ErrInvalid + } + contentType := packet[0] + switch contentType { + case 20, 21, 22, 23, 25: + default: + return os.ErrInvalid + } + versionMajor := packet[1] + if versionMajor != 0xfe { + return os.ErrInvalid + } + versionMinor := packet[2] + if versionMinor != 0xff && versionMinor != 0xfd { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolDTLS + return nil +} diff --git a/common/sniff/dtls_test.go b/common/sniff/dtls_test.go new file mode 100644 index 00000000..48c9b42b --- /dev/null +++ b/common/sniff/dtls_test.go @@ -0,0 +1,33 @@ +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 TestSniffDTLSClientHello(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.DTLSRecord(context.Background(), &metadata, packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolDTLS) +} + +func TestSniffDTLSClientApplicationData(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.DTLSRecord(context.Background(), &metadata, packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolDTLS) +} diff --git a/common/sniff/http.go b/common/sniff/http.go new file mode 100644 index 00000000..012f2c99 --- /dev/null +++ b/common/sniff/http.go @@ -0,0 +1,28 @@ +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" +) + +func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + request, err := http.ReadRequest(std_bufio.NewReader(reader)) + if err != nil { + 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() + return nil +} diff --git a/common/sniff/http_test.go b/common/sniff/http_test.go new file mode 100644 index 00000000..9f64efa8 --- /dev/null +++ b/common/sniff/http_test.go @@ -0,0 +1,30 @@ +package sniff_test + +import ( + "context" + "strings" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + + "github.com/stretchr/testify/require" +) + +func TestSniffHTTP1(t *testing.T) { + t.Parallel() + pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n" + var metadata adapter.InboundContext + err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, metadata.Domain, "www.google.com") +} + +func TestSniffHTTP1WithPort(t *testing.T) { + t.Parallel() + pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n" + var metadata adapter.InboundContext + err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, metadata.Domain, "www.gov.cn") +} diff --git a/common/sniff/internal/qtls/qtls.go b/common/sniff/internal/qtls/qtls.go new file mode 100644 index 00000000..9742de1e --- /dev/null +++ b/common/sniff/internal/qtls/qtls.go @@ -0,0 +1,148 @@ +package qtls + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "encoding/binary" + "io" + + "golang.org/x/crypto/hkdf" +) + +const ( + VersionDraft29 = 0xff00001d + Version1 = 0x1 + Version2 = 0x6b3343cf +) + +var ( + SaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} + SaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + SaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9} +) + +const ( + HKDFLabelKeyV1 = "quic key" + HKDFLabelKeyV2 = "quicv2 key" + HKDFLabelIVV1 = "quic iv" + HKDFLabelIVV2 = "quicv2 iv" + HKDFLabelHeaderProtectionV1 = "quic hp" + HKDFLabelHeaderProtectionV2 = "quicv2 hp" +) + +func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD { + if len(nonceMask) != 12 { + panic("tls: internal error: wrong nonce length") + } + aes, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + aead, err := cipher.NewGCM(aes) + if err != nil { + panic(err) + } + + ret := &xorNonceAEAD{aead: aead} + copy(ret.nonceMask[:], nonceMask) + return ret +} + +type xorNonceAEAD struct { + nonceMask [12]byte + aead cipher.AEAD +} + +func (f *xorNonceAEAD) NonceSize() int { return 8 } // 64-bit sequence number +func (f *xorNonceAEAD) Overhead() int { return f.aead.Overhead() } +func (f *xorNonceAEAD) explicitNonceLen() int { return 0 } + +func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte { + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData) + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + + return result +} + +func (f *xorNonceAEAD) Open(out, nonce, ciphertext, additionalData []byte) ([]byte, error) { + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + result, err := f.aead.Open(out, f.nonceMask[:], ciphertext, additionalData) + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + + return result, err +} + +func HKDFExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { + b := make([]byte, 3, 3+6+len(label)+1+len(context)) + binary.BigEndian.PutUint16(b, uint16(length)) + b[2] = uint8(6 + len(label)) + b = append(b, []byte("tls13 ")...) + b = append(b, []byte(label)...) + b = b[:3+6+len(label)+1] + b[3+6+len(label)] = uint8(len(context)) + b = append(b, context...) + out := make([]byte, length) + n, err := hkdf.Expand(hash.New, secret, b).Read(out) + if err != nil || n != length { + panic("quic: HKDF-Expand-Label invocation failed unexpectedly") + } + return out +} + +func ReadUvarint(r io.ByteReader) (uint64, error) { + firstByte, err := r.ReadByte() + if err != nil { + return 0, err + } + // the first two bits of the first byte encode the length + len := 1 << ((firstByte & 0xc0) >> 6) + b1 := firstByte & (0xff - 0xc0) + if len == 1 { + return uint64(b1), nil + } + b2, err := r.ReadByte() + if err != nil { + return 0, err + } + if len == 2 { + return uint64(b2) + uint64(b1)<<8, nil + } + b3, err := r.ReadByte() + if err != nil { + return 0, err + } + b4, err := r.ReadByte() + if err != nil { + return 0, err + } + if len == 4 { + return uint64(b4) + uint64(b3)<<8 + uint64(b2)<<16 + uint64(b1)<<24, nil + } + b5, err := r.ReadByte() + if err != nil { + return 0, err + } + b6, err := r.ReadByte() + if err != nil { + return 0, err + } + b7, err := r.ReadByte() + if err != nil { + return 0, err + } + b8, err := r.ReadByte() + if err != nil { + return 0, err + } + return uint64(b8) + uint64(b7)<<8 + uint64(b6)<<16 + uint64(b5)<<24 + uint64(b4)<<32 + uint64(b3)<<40 + uint64(b2)<<48 + uint64(b1)<<56, nil +} diff --git a/common/sniff/ntp.go b/common/sniff/ntp.go new file mode 100644 index 00000000..8b844c5b --- /dev/null +++ b/common/sniff/ntp.go @@ -0,0 +1,58 @@ +package sniff + +import ( + "context" + "encoding/binary" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { + // NTP packets must be at least 48 bytes long (standard NTP header size). + pLen := len(packet) + if pLen < 48 { + return os.ErrInvalid + } + // Check the LI (Leap Indicator) and Version Number (VN) in the first byte. + // We'll primarily focus on ensuring the version is valid for NTP. + // Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations) + firstByte := packet[0] + li := (firstByte >> 6) & 0x03 // Extract LI + vn := (firstByte >> 3) & 0x07 // Extract VN + mode := firstByte & 0x07 // Extract Mode + + // Leap Indicator should be a valid value (0-3). + if li > 3 { + return os.ErrInvalid + } + + // Version Check (common NTP versions are 3 and 4) + if vn != 3 && vn != 4 { + return os.ErrInvalid + } + + // Check the Mode field for a client request (Mode 3). This validates it *is* a request. + if mode != 3 { + return os.ErrInvalid + } + + // Check Root Delay and Root Dispersion. While not strictly *required* for a request, + // we can check if they appear to be reasonable values (not excessively large). + rootDelay := binary.BigEndian.Uint32(packet[4:8]) + rootDispersion := binary.BigEndian.Uint32(packet[8:12]) + + // Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds. + // Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds. + if float64(rootDelay)/65536.0 > 16.0 { + return os.ErrInvalid + } + if float64(rootDispersion)/65536.0 > 16.0 { + return os.ErrInvalid + } + + metadata.Protocol = C.ProtocolNTP + + return nil +} diff --git a/common/sniff/ntp_test.go b/common/sniff/ntp_test.go new file mode 100644 index 00000000..5da94785 --- /dev/null +++ b/common/sniff/ntp_test.go @@ -0,0 +1,33 @@ +package sniff_test + +import ( + "context" + "encoding/hex" + "os" + "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 TestSniffNTP(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.NTP(context.Background(), &metadata, packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolNTP) +} + +func TestSniffNTPFailed(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.NTP(context.Background(), &metadata, packet) + require.ErrorIs(t, err, os.ErrInvalid) +} diff --git a/common/sniff/quic.go b/common/sniff/quic.go new file mode 100644 index 00000000..049bd2c1 --- /dev/null +++ b/common/sniff/quic.go @@ -0,0 +1,373 @@ +package sniff + +import ( + "bytes" + "context" + "crypto" + "crypto/aes" + "crypto/tls" + "encoding/binary" + "io" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/ja3" + "github.com/sagernet/sing-box/common/sniff/internal/qtls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/crypto/hkdf" +) + +func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { + reader := bytes.NewReader(packet) + typeByte, err := reader.ReadByte() + if err != nil { + return err + } + if typeByte&0x40 == 0 { + return E.New("bad type byte") + } + var versionNumber uint32 + err = binary.Read(reader, binary.BigEndian, &versionNumber) + if err != nil { + return err + } + if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 { + return E.New("bad version") + } + packetType := (typeByte & 0x30) >> 4 + if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 { + return E.New("bad packet type") + } + + destConnIDLen, err := reader.ReadByte() + if err != nil { + return err + } + + if destConnIDLen == 0 || destConnIDLen > 20 { + return E.New("bad destination connection id length") + } + + destConnID := make([]byte, destConnIDLen) + _, err = io.ReadFull(reader, destConnID) + if err != nil { + return err + } + + srcConnIDLen, err := reader.ReadByte() + if err != nil { + return err + } + + _, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen)) + if err != nil { + return err + } + + tokenLen, err := qtls.ReadUvarint(reader) + if err != nil { + return err + } + + _, err = io.CopyN(io.Discard, reader, int64(tokenLen)) + if err != nil { + return err + } + + packetLen, err := qtls.ReadUvarint(reader) + if err != nil { + return err + } + + hdrLen := int(reader.Size()) - reader.Len() + if hdrLen+int(packetLen) > len(packet) { + return os.ErrInvalid + } + + _, err = io.CopyN(io.Discard, reader, 4) + if err != nil { + return err + } + + pnBytes := make([]byte, aes.BlockSize) + _, err = io.ReadFull(reader, pnBytes) + if err != nil { + return err + } + + var salt []byte + switch versionNumber { + case qtls.Version1: + salt = qtls.SaltV1 + case qtls.Version2: + salt = qtls.SaltV2 + default: + salt = qtls.SaltOld + } + var hkdfHeaderProtectionLabel string + switch versionNumber { + case qtls.Version2: + hkdfHeaderProtectionLabel = qtls.HKDFLabelHeaderProtectionV2 + default: + hkdfHeaderProtectionLabel = qtls.HKDFLabelHeaderProtectionV1 + } + initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) + secret := qtls.HKDFExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) + hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16) + block, err := aes.NewCipher(hpKey) + if err != nil { + return err + } + mask := make([]byte, aes.BlockSize) + block.Encrypt(mask, pnBytes) + newPacket := make([]byte, len(packet)) + copy(newPacket, packet) + newPacket[0] ^= mask[0] & 0xf + for i := range newPacket[hdrLen : hdrLen+4] { + newPacket[hdrLen+i] ^= mask[i+1] + } + packetNumberLength := newPacket[0]&0x3 + 1 + if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen { + return os.ErrInvalid + } + var packetNumber uint32 + switch packetNumberLength { + case 1: + packetNumber = uint32(newPacket[hdrLen]) + case 2: + packetNumber = uint32(binary.BigEndian.Uint16(newPacket[hdrLen:])) + case 3: + packetNumber = uint32(newPacket[hdrLen+2]) | uint32(newPacket[hdrLen+1])<<8 | uint32(newPacket[hdrLen])<<16 + case 4: + packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:]) + default: + return E.New("bad packet number length") + } + extHdrLen := hdrLen + int(packetNumberLength) + copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:]) + data := newPacket[extHdrLen : int(packetLen)+hdrLen] + + var keyLabel string + var ivLabel string + switch versionNumber { + case qtls.Version2: + keyLabel = qtls.HKDFLabelKeyV2 + ivLabel = qtls.HKDFLabelIVV2 + default: + keyLabel = qtls.HKDFLabelKeyV1 + ivLabel = qtls.HKDFLabelIVV1 + } + + key := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, keyLabel, 16) + iv := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, ivLabel, 12) + cipher := qtls.AEADAESGCMTLS13(key, iv) + nonce := make([]byte, int32(cipher.NonceSize())) + binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) + decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen]) + if err != nil { + return err + } + var frameType byte + var fragments []qCryptoFragment + decryptedReader := bytes.NewReader(decrypted) + const ( + frameTypePadding = 0x00 + frameTypePing = 0x01 + frameTypeAck = 0x02 + frameTypeAck2 = 0x03 + frameTypeCrypto = 0x06 + frameTypeConnectionClose = 0x1c + ) + var frameTypeList []uint8 + for { + frameType, err = decryptedReader.ReadByte() + if err == io.EOF { + break + } + frameTypeList = append(frameTypeList, frameType) + switch frameType { + case frameTypePadding: + continue + case frameTypePing: + continue + case frameTypeAck, frameTypeAck2: + _, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged + if err != nil { + return err + } + _, err = qtls.ReadUvarint(decryptedReader) // ACK Delay + if err != nil { + return err + } + ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count + if err != nil { + return err + } + _, err = qtls.ReadUvarint(decryptedReader) // First ACK Range + if err != nil { + return err + } + for i := 0; i < int(ackRangeCount); i++ { + _, err = qtls.ReadUvarint(decryptedReader) // Gap + if err != nil { + return err + } + _, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length + if err != nil { + return err + } + } + if frameType == 0x03 { + _, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count + if err != nil { + return err + } + _, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count + if err != nil { + return err + } + _, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count + if err != nil { + return err + } + } + case frameTypeCrypto: + var offset uint64 + offset, err = qtls.ReadUvarint(decryptedReader) + if err != nil { + return err + } + var length uint64 + length, err = qtls.ReadUvarint(decryptedReader) + if err != nil { + return err + } + index := len(decrypted) - decryptedReader.Len() + fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]}) + _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) + if err != nil { + return err + } + case frameTypeConnectionClose: + _, err = qtls.ReadUvarint(decryptedReader) // Error Code + if err != nil { + return err + } + _, err = qtls.ReadUvarint(decryptedReader) // Frame Type + if err != nil { + return err + } + var length uint64 + length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length + if err != nil { + return err + } + _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase + if err != nil { + return err + } + default: + return os.ErrInvalid + } + } + if metadata.SniffContext != nil { + fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...) + metadata.SniffContext = nil + } + var frameLen uint64 + for _, fragment := range fragments { + frameLen += fragment.length + } + buffer := buf.NewSize(5 + int(frameLen)) + defer buffer.Release() + buffer.WriteByte(0x16) + binary.Write(buffer, binary.BigEndian, uint16(0x0303)) + binary.Write(buffer, binary.BigEndian, uint16(frameLen)) + var index uint64 + var length int +find: + for { + for _, fragment := range fragments { + if fragment.offset == index { + buffer.Write(fragment.payload) + index = fragment.offset + fragment.length + length++ + continue find + } + } + break + } + metadata.Protocol = C.ProtocolQUIC + fingerprint, err := ja3.Compute(buffer.Bytes()) + if err != nil { + metadata.SniffContext = fragments + return E.Cause1(ErrNeedMoreData, err) + } + metadata.Domain = fingerprint.ServerName + for metadata.Client == "" { + if len(frameTypeList) == 1 { + metadata.Client = C.ClientFirefox + break + } + if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) { + if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A && + len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A { + metadata.Client = C.ClientSafari + break + } + if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 && + len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) && + len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) { + metadata.Client = C.ClientSafari + break + } + } + + if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) { + metadata.Client = C.ClientQUICGo + break + } + + if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 { + if isQUICGo(fingerprint) { + metadata.Client = C.ClientQUICGo + } else { + metadata.Client = C.ClientChromium + } + break + } + + metadata.Client = C.ClientUnknown + //nolint:staticcheck + break + } + return nil +} + +func isZero(slices []uint8) bool { + for _, slice := range slices { + if slice != 0 { + return false + } + } + return true +} + +func count(slices []uint8, value uint8) int { + var times int + for _, slice := range slices { + if slice == value { + times++ + } + } + return times +} + +type qCryptoFragment struct { + offset uint64 + length uint64 + payload []byte +} diff --git a/common/sniff/quic_blacklist.go b/common/sniff/quic_blacklist.go new file mode 100644 index 00000000..56a15152 --- /dev/null +++ b/common/sniff/quic_blacklist.go @@ -0,0 +1,29 @@ +package sniff + +import ( + "github.com/sagernet/sing-box/common/ja3" +) + +const ( + // X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls + x25519Kyber768Draft00 uint16 = 0x11EC // 4588 + // renegotiation_info extension used by Go crypto/tls + extensionRenegotiationInfo uint16 = 0xFF01 // 65281 +) + +// isQUICGo detects native quic-go by checking for Go crypto/tls specific features. +// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium +// since it uses the same TLS fingerprint, so it will be identified as Chromium. +func isQUICGo(fingerprint *ja3.ClientHello) bool { + for _, curve := range fingerprint.EllipticCurves { + if curve == x25519Kyber768Draft00 { + return true + } + } + for _, ext := range fingerprint.Extensions { + if ext == extensionRenegotiationInfo { + return true + } + } + return false +} diff --git a/common/sniff/quic_capture_test.go b/common/sniff/quic_capture_test.go new file mode 100644 index 00000000..4c9eb838 --- /dev/null +++ b/common/sniff/quic_capture_test.go @@ -0,0 +1,188 @@ +package sniff_test + +import ( + "context" + "crypto/tls" + "encoding/hex" + "errors" + "net" + "testing" + "time" + + "github.com/sagernet/quic-go" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + + "github.com/stretchr/testify/require" +) + +func TestSniffQUICQuicGoFingerprint(t *testing.T) { + t.Parallel() + const testSNI = "test.example.com" + + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) + require.NoError(t, err) + defer udpConn.Close() + + serverAddr := udpConn.LocalAddr().(*net.UDPAddr) + packetsChan := make(chan [][]byte, 1) + + go func() { + var packets [][]byte + udpConn.SetReadDeadline(time.Now().Add(3 * time.Second)) + for i := 0; i < 10; i++ { + buf := make([]byte, 2048) + n, _, err := udpConn.ReadFromUDP(buf) + if err != nil { + break + } + packets = append(packets, buf[:n]) + } + packetsChan <- packets + }() + + clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) + require.NoError(t, err) + defer clientConn.Close() + + tlsConfig := &tls.Config{ + ServerName: testSNI, + InsecureSkipVerify: true, + NextProtos: []string{"h3"}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{}) + + select { + case packets := <-packetsChan: + t.Logf("Captured %d packets", len(packets)) + + var metadata adapter.InboundContext + for i, pkt := range packets { + err := sniff.QUICClientHello(context.Background(), &metadata, pkt) + t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client) + if metadata.Domain != "" { + break + } + } + + t.Logf("\n=== quic-go TLS Fingerprint Analysis ===") + t.Logf("Domain: %s", metadata.Domain) + t.Logf("Client: %s", metadata.Client) + t.Logf("Protocol: %s", metadata.Protocol) + + // The client should be identified as quic-go, not chromium + // Current issue: it's being identified as chromium + if metadata.Client == "chromium" { + t.Log("WARNING: quic-go is being misidentified as chromium!") + } + + case <-time.After(5 * time.Second): + t.Fatal("Timeout") + } +} + +func TestSniffQUICInitialFromQuicGo(t *testing.T) { + t.Parallel() + + const testSNI = "test.example.com" + + // Create UDP listener to capture ALL initial packets + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) + require.NoError(t, err) + defer udpConn.Close() + + serverAddr := udpConn.LocalAddr().(*net.UDPAddr) + + // Channel to receive captured packets + packetsChan := make(chan [][]byte, 1) + + // Start goroutine to capture packets + go func() { + var packets [][]byte + udpConn.SetReadDeadline(time.Now().Add(3 * time.Second)) + for i := 0; i < 5; i++ { // Capture up to 5 packets + buf := make([]byte, 2048) + n, _, err := udpConn.ReadFromUDP(buf) + if err != nil { + break + } + packets = append(packets, buf[:n]) + } + packetsChan <- packets + }() + + // Create QUIC client connection (will fail but we capture the initial packet) + clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) + require.NoError(t, err) + defer clientConn.Close() + + tlsConfig := &tls.Config{ + ServerName: testSNI, + InsecureSkipVerify: true, + NextProtos: []string{"h3"}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // This will fail (no server) but sends initial packet + _, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{}) + + // Wait for captured packets + select { + case packets := <-packetsChan: + t.Logf("Captured %d QUIC packets", len(packets)) + + for i, packet := range packets { + t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))]) + } + + // Test sniffer with first packet + if len(packets) > 0 { + var metadata adapter.InboundContext + err := sniff.QUICClientHello(context.Background(), &metadata, packets[0]) + + t.Logf("First packet sniff error: %v", err) + t.Logf("Protocol: %s", metadata.Protocol) + t.Logf("Domain: %s", metadata.Domain) + t.Logf("Client: %s", metadata.Client) + + // If first packet needs more data, try with subsequent packets + // IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext + if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 { + t.Log("First packet needs more data, trying subsequent packets with shared context...") + for i := 1; i < len(packets); i++ { + // Reuse same metadata to accumulate fragments + err = sniff.QUICClientHello(context.Background(), &metadata, packets[i]) + t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil) + if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) { + break + } + } + } + + // Print hex dump for debugging + t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))])) + + // Log final results + t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client) + + // Verify SNI extraction + if metadata.Domain == "" { + t.Errorf("Failed to extract SNI, expected: %s", testSNI) + } else { + require.Equal(t, testSNI, metadata.Domain, "SNI should match") + } + + // Check client identification - quic-go should be identified as quic-go, not chromium + t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client) + } + + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for QUIC packets") + } +} diff --git a/common/sniff/quic_test.go b/common/sniff/quic_test.go new file mode 100644 index 00000000..e2f53724 --- /dev/null +++ b/common/sniff/quic_test.go @@ -0,0 +1,93 @@ +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 TestSniffQUICChromeNew(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Empty(t, metadata.Client) + 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.ErrNeedMoreData) + pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f") + require.NoError(t, err) + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, "www.google.com", metadata.Domain) +} + +func TestSniffQUICChromium(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Empty(t, metadata.Client) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) + pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") + require.NoError(t, err) + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Domain, "google.com") +} + +func TestSniffUQUICChrome115(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientChromium) + require.Equal(t, metadata.Domain, "www.google.com") +} + +func TestSniffQUICFirefox(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientFirefox) + require.Equal(t, metadata.Domain, "www.google.com") +} + +func TestSniffQUICSafari(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientSafari) + require.Equal(t, metadata.Domain, "www.google.com") +} + +func FuzzSniffQUIC(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + var metadata adapter.InboundContext + err := sniff.QUICClientHello(context.Background(), &metadata, data) + require.Error(t, err) + }) +} diff --git a/common/sniff/rdp.go b/common/sniff/rdp.go new file mode 100644 index 00000000..37551fef --- /dev/null +++ b/common/sniff/rdp.go @@ -0,0 +1,91 @@ +package sniff + +import ( + "context" + "encoding/binary" + "io" + "os" + + "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" +) + +func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + var tpktVersion uint8 + err := binary.Read(reader, binary.BigEndian, &tpktVersion) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if tpktVersion != 0x03 { + return os.ErrInvalid + } + + var tpktReserved uint8 + err = binary.Read(reader, binary.BigEndian, &tpktReserved) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if tpktReserved != 0x00 { + return os.ErrInvalid + } + + var tpktLength uint16 + err = binary.Read(reader, binary.BigEndian, &tpktLength) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + + if tpktLength != 19 { + return os.ErrInvalid + } + + var cotpLength uint8 + err = binary.Read(reader, binary.BigEndian, &cotpLength) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + + if cotpLength != 14 { + return os.ErrInvalid + } + + var cotpTpduType uint8 + err = binary.Read(reader, binary.BigEndian, &cotpTpduType) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if cotpTpduType != 0xE0 { + return os.ErrInvalid + } + + err = rw.SkipN(reader, 5) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + + var rdpType uint8 + err = binary.Read(reader, binary.BigEndian, &rdpType) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if rdpType != 0x01 { + return os.ErrInvalid + } + var rdpFlags uint8 + err = binary.Read(reader, binary.BigEndian, &rdpFlags) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + var rdpLength uint8 + err = binary.Read(reader, binary.BigEndian, &rdpLength) + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + if rdpLength != 8 { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolRDP + return nil +} diff --git a/common/sniff/rdp_test.go b/common/sniff/rdp_test.go new file mode 100644 index 00000000..06fa3ab2 --- /dev/null +++ b/common/sniff/rdp_test.go @@ -0,0 +1,25 @@ +package sniff_test + +import ( + "bytes" + "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 TestSniffRDP(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("030000130ee00000000000010008000b000000010008000b000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.RDP(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolRDP, metadata.Protocol) +} diff --git a/common/sniff/sniff.go b/common/sniff/sniff.go new file mode 100644 index 00000000..b3651e1f --- /dev/null +++ b/common/sniff/sniff.go @@ -0,0 +1,88 @@ +package sniff + +import ( + "bytes" + "context" + "errors" + "io" + "net" + "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" + E "github.com/sagernet/sing/common/exceptions" +) + +type ( + StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error + 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 { + case 25, 465, 587: + // SMTP + return true + case 143, 993: + // IMAP + return true + case 110, 995: + // POP3 + return true + } + return false +} + +func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error { + if timeout == 0 { + timeout = C.ReadPayloadTimeout + } + deadline := time.Now().Add(timeout) + var sniffError error + for i := 0; ; i++ { + err := conn.SetReadDeadline(deadline) + if err != nil { + return E.Cause(err, "set read deadline") + } + _, err = buffer.ReadOnceFrom(conn) + _ = conn.SetReadDeadline(time.Time{}) + if err != nil { + if i > 0 { + break + } + return E.Cause(err, "read payload") + } + 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()) + })...) + err = sniffer(ctx, metadata, reader) + if err == nil { + return nil + } + sniffError = E.Errors(sniffError, err) + } + if !errors.Is(sniffError, ErrNeedMoreData) { + break + } + } + return sniffError +} + +func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { + var sniffError []error + for _, sniffer := range sniffers { + err := sniffer(ctx, metadata, packet) + if err == nil { + return nil + } + sniffError = append(sniffError, err) + } + return E.Errors(sniffError...) +} diff --git a/common/sniff/ssh.go b/common/sniff/ssh.go new file mode 100644 index 00000000..dce5d54f --- /dev/null +++ b/common/sniff/ssh.go @@ -0,0 +1,31 @@ +package sniff + +import ( + "bufio" + "context" + "io" + "os" + + "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 { + const sshPrefix = "SSH-2.0-" + bReader := bufio.NewReader(reader) + prefix, err := bReader.Peek(len(sshPrefix)) + if string(prefix[:]) != sshPrefix[:len(prefix)] { + return os.ErrInvalid + } + if err != nil { + return E.Cause1(ErrNeedMoreData, err) + } + fistLine, _, err := bReader.ReadLine() + if err != nil { + return err + } + metadata.Protocol = C.ProtocolSSH + metadata.Client = string(fistLine)[8:] + return nil +} diff --git a/common/sniff/ssh_test.go b/common/sniff/ssh_test.go new file mode 100644 index 00000000..7cea5aab --- /dev/null +++ b/common/sniff/ssh_test.go @@ -0,0 +1,47 @@ +package sniff_test + +import ( + "bytes" + "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 TestSniffSSH(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolSSH, metadata.Protocol) + require.Equal(t, "dropbear", metadata.Client) +} + +func TestSniffIncompleteSSH(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("5353482d322e30") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.ErrorIs(t, err, sniff.ErrNeedMoreData) +} + +func TestSniffNotSSH(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("5353482d322e31") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NotEmpty(t, err) + require.NotErrorIs(t, err, sniff.ErrNeedMoreData) +} diff --git a/common/sniff/stun.go b/common/sniff/stun.go new file mode 100644 index 00000000..dfd11259 --- /dev/null +++ b/common/sniff/stun.go @@ -0,0 +1,25 @@ +package sniff + +import ( + "context" + "encoding/binary" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { + pLen := len(packet) + if pLen < 20 { + return os.ErrInvalid + } + if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 { + return os.ErrInvalid + } + if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolSTUN + return nil +} diff --git a/common/sniff/stun_test.go b/common/sniff/stun_test.go new file mode 100644 index 00000000..f465763e --- /dev/null +++ b/common/sniff/stun_test.go @@ -0,0 +1,32 @@ +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 TestSniffSTUN(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.STUNMessage(context.Background(), &metadata, packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolSTUN) +} + +func FuzzSniffSTUN(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + var metadata adapter.InboundContext + if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil { + t.Fail() + } + }) +} diff --git a/common/sniff/tls.go b/common/sniff/tls.go new file mode 100644 index 00000000..613086e8 --- /dev/null +++ b/common/sniff/tls.go @@ -0,0 +1,33 @@ +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 { + var clientHello *tls.ClientHelloInfo + err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{ + GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { + clientHello = argHello + return nil, nil + }, + }).HandshakeContext(ctx) + if clientHello != nil { + metadata.Protocol = C.ProtocolTLS + metadata.Domain = clientHello.ServerName + return nil + } + if errors.Is(err, io.ErrUnexpectedEOF) { + return E.Cause1(ErrNeedMoreData, err) + } else { + return err + } +} diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 00000000..d5b644ae --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,704 @@ +package srs + +import ( + "bufio" + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + "unsafe" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/varbin" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemAdGuardDomain + ruleItemProcessPathRegex + ruleItemNetworkType + ruleItemNetworkIsExpensive + ruleItemNetworkIsConstrained + ruleItemNetworkInterfaceAddress + ruleItemDefaultInterfaceAddress + ruleItemPackageNameRegex + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetCompat, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule-set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSetCompat, err + } + if version > C.RuleSetVersionCurrent { + return ruleSetCompat, E.New("unsupported version: ", version) + } + compressReader, err := zlib.NewReader(reader) + if err != nil { + return + } + bReader := bufio.NewReader(compressReader) + length, err := binary.ReadUvarint(bReader) + if err != nil { + return + } + ruleSetCompat.Version = version + ruleSetCompat.Options.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSetCompat.Options.Rules[i], err = readRule(bReader, recover) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, generateVersion) + if err != nil { + return err + } + compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + bWriter := bufio.NewWriter(compressWriter) + _, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(bWriter, rule, generateVersion) + if err != nil { + return err + } + } + err = bWriter.Flush() + if err != nil { + return err + } + return compressWriter.Close() +} + +func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.Type = C.RuleTypeDefault + rule.DefaultOptions, err = readDefaultRule(reader, recover) + case 1: + rule.Type = C.RuleTypeLogical + rule.LogicalOptions, err = readLogicalRule(reader, recover) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateVersion uint8) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions, generateVersion) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions, generateVersion) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + if recover { + rule.Domain, rule.DomainSuffix = matcher.Dump() + } + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recover { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recover { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemProcessPathRegex: + rule.ProcessPathRegex, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemPackageNameRegex: + rule.PackageNameRegex, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemAdGuardDomain: + var matcher *domain.AdGuardMatcher + matcher, err = domain.ReadAdGuardMatcher(reader) + if err != nil { + return + } + rule.AdGuardDomainMatcher = matcher + if recover { + rule.AdGuardDomain = matcher.Dump() + } + case ruleItemNetworkType: + rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader) + case ruleItemNetworkIsExpensive: + rule.NetworkIsExpensive = true + case ruleItemNetworkIsConstrained: + rule.NetworkIsConstrained = true + case ruleItemNetworkInterfaceAddress: + rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]]) + var size uint64 + size, err = binary.ReadUvarint(reader) + if err != nil { + return + } + for i := uint64(0); i < size; i++ { + var key uint8 + err = binary.Read(reader, binary.BigEndian, &key) + if err != nil { + return + } + var value []*badoption.Prefixable + var prefixCount uint64 + prefixCount, err = binary.ReadUvarint(reader) + if err != nil { + return + } + for j := uint64(0); j < prefixCount; j++ { + var prefix netip.Prefix + prefix, err = readPrefix(reader) + if err != nil { + return + } + value = append(value, common.Ptr(badoption.Prefixable(prefix))) + } + rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value) + } + case ruleItemDefaultInterfaceAddress: + var value []*badoption.Prefixable + var prefixCount uint64 + prefixCount, err = binary.ReadUvarint(reader) + if err != nil { + return + } + for j := uint64(0); j < prefixCount; j++ { + var prefix netip.Prefix + prefix, err = readPrefix(reader) + if err != nil { + return + } + value = append(value, common.Ptr(badoption.Prefixable(prefix))) + } + rule.DefaultInterfaceAddress = value + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateVersion uint8) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, generateVersion == C.RuleSetVersion1).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ip_cidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.ProcessPathRegex) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPathRegex, rule.ProcessPathRegex) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.PackageNameRegex) > 0 { + if generateVersion < C.RuleSetVersion5 { + return E.New("`package_name_regex` rule item is only supported in version 5 or later") + } + err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex) + if err != nil { + return err + } + } + if len(rule.NetworkType) > 0 { + if generateVersion < C.RuleSetVersion3 { + return E.New("`network_type` rule item is only supported in version 3 or later") + } + err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType) + if err != nil { + return err + } + } + if rule.NetworkIsExpensive { + if generateVersion < C.RuleSetVersion3 { + return E.New("`network_is_expensive` rule item is only supported in version 3 or later") + } + err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) + if err != nil { + return err + } + } + if rule.NetworkIsConstrained { + if generateVersion < C.RuleSetVersion3 { + return E.New("`network_is_constrained` rule item is only supported in version 3 or later") + } + err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) + if err != nil { + return err + } + } + if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 { + if generateVersion < C.RuleSetVersion4 { + return E.New("`network_interface_address` rule item is only supported in version 4 or later") + } + err = writer.WriteByte(ruleItemNetworkInterfaceAddress) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size())) + if err != nil { + return err + } + for _, entry := range rule.NetworkInterfaceAddress.Entries() { + err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build())) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(entry.Value))) + if err != nil { + return err + } + for _, rawPrefix := range entry.Value { + err = writePrefix(writer, rawPrefix.Build(netip.Prefix{})) + if err != nil { + return err + } + } + } + } + if len(rule.DefaultInterfaceAddress) > 0 { + if generateVersion < C.RuleSetVersion4 { + return E.New("`default_interface_address` rule item is only supported in version 4 or later") + } + err = writer.WriteByte(ruleItemDefaultInterfaceAddress) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress))) + if err != nil { + return err + } + for _, rawPrefix := range rule.DefaultInterfaceAddress { + err = writePrefix(writer, rawPrefix.Build(netip.Prefix{})) + if err != nil { + return err + } + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + if len(rule.AdGuardDomain) > 0 { + if generateVersion < C.RuleSetVersion2 { + return E.New("AdGuard rule items is only supported in version 2 or later") + } + err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain) + if err != nil { + return err + } + err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader varbin.Reader) ([]string, error) { + length, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + result := make([]string, length) + for i := range result { + strLen, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + buf := make([]byte, strLen) + _, err = io.ReadFull(reader, buf) + if err != nil { + return nil, err + } + result[i] = string(buf) + } + return result, nil +} + +func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error { + err := writer.WriteByte(itemType) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + for _, s := range value { + _, err = varbin.WriteUvarint(writer, uint64(len(s))) + if err != nil { + return err + } + _, err = writer.Write([]byte(s)) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) { + length, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + result := make([]E, length) + _, err = io.ReadFull(reader, *(*[]byte)(unsafe.Pointer(&result))) + if err != nil { + return nil, err + } + return result, nil +} + +func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error { + err := writer.WriteByte(itemType) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + _, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value))) + return err +} + +func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) { + length, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + result := make([]uint16, length) + err = binary.Read(reader, binary.BigEndian, result) + if err != nil { + return nil, err + } + return result, nil +} + +func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error { + err := writer.WriteByte(itemType) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + return binary.Write(writer, binary.BigEndian, value) +} + +func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + mode, err := reader.ReadByte() + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := binary.ReadUvarint(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateVersion uint8) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule, generateVersion) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/compat_test.go b/common/srs/compat_test.go new file mode 100644 index 00000000..98552b32 --- /dev/null +++ b/common/srs/compat_test.go @@ -0,0 +1,494 @@ +package srs + +import ( + "bufio" + "bytes" + "encoding/binary" + "net/netip" + "strings" + "testing" + "unsafe" + + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" + + "github.com/stretchr/testify/require" + "go4.org/netipx" +) + +// Old implementations using varbin reflection-based serialization + +func oldWriteStringSlice(writer varbin.Writer, value []string) error { + //nolint:staticcheck + return varbin.Write(writer, binary.BigEndian, value) +} + +func oldReadStringSlice(reader varbin.Reader) ([]string, error) { + //nolint:staticcheck + return varbin.ReadValue[[]string](reader, binary.BigEndian) +} + +func oldWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error { + //nolint:staticcheck + return varbin.Write(writer, binary.BigEndian, value) +} + +func oldReadUint8Slice[E ~uint8](reader varbin.Reader) ([]E, error) { + //nolint:staticcheck + return varbin.ReadValue[[]E](reader, binary.BigEndian) +} + +func oldWriteUint16Slice(writer varbin.Writer, value []uint16) error { + //nolint:staticcheck + return varbin.Write(writer, binary.BigEndian, value) +} + +func oldReadUint16Slice(reader varbin.Reader) ([]uint16, error) { + //nolint:staticcheck + return varbin.ReadValue[[]uint16](reader, binary.BigEndian) +} + +func oldWritePrefix(writer varbin.Writer, prefix netip.Prefix) error { + //nolint:staticcheck + err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice()) + if err != nil { + return err + } + return binary.Write(writer, binary.BigEndian, uint8(prefix.Bits())) +} + +type oldIPRangeData struct { + From []byte + To []byte +} + +// Note: The old writeIPSet had a bug where varbin.Write(writer, binary.BigEndian, data) +// with a struct VALUE (not pointer) silently wrote nothing because field.CanSet() returned false. +// This caused IP range data to be missing from the output. +// The new implementation correctly writes all range data. +// +// The old readIPSet used varbin.Read with a pre-allocated slice, which worked because +// slice elements are addressable and CanSet() returns true for them. +// +// For compatibility testing, we verify: +// 1. New write produces correct output with range data +// 2. New read can parse the new format correctly +// 3. Round-trip works correctly + +func oldReadIPSet(reader varbin.Reader) (*netipx.IPSet, error) { + version, err := reader.ReadByte() + if err != nil { + return nil, err + } + if version != 1 { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + ranges := make([]oldIPRangeData, length) + //nolint:staticcheck + err = varbin.Read(reader, binary.BigEndian, &ranges) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, len(ranges)), + } + for i, rangeData := range ranges { + mySet.rr[i].from = M.AddrFromIP(rangeData.From) + mySet.rr[i].to = M.AddrFromIP(rangeData.To) + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +// New write functions (without itemType prefix for testing) + +func newWriteStringSlice(writer varbin.Writer, value []string) error { + _, err := varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + for _, s := range value { + _, err = varbin.WriteUvarint(writer, uint64(len(s))) + if err != nil { + return err + } + _, err = writer.Write([]byte(s)) + if err != nil { + return err + } + } + return nil +} + +func newWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error { + _, err := varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + _, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value))) + return err +} + +func newWriteUint16Slice(writer varbin.Writer, value []uint16) error { + _, err := varbin.WriteUvarint(writer, uint64(len(value))) + if err != nil { + return err + } + return binary.Write(writer, binary.BigEndian, value) +} + +func newWritePrefix(writer varbin.Writer, prefix netip.Prefix) error { + addrSlice := prefix.Addr().AsSlice() + _, err := varbin.WriteUvarint(writer, uint64(len(addrSlice))) + if err != nil { + return err + } + _, err = writer.Write(addrSlice) + if err != nil { + return err + } + return writer.WriteByte(uint8(prefix.Bits())) +} + +// Tests + +func TestStringSliceCompat(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input []string + }{ + {"nil", nil}, + {"empty", []string{}}, + {"single_empty", []string{""}}, + {"single", []string{"test"}}, + {"multi", []string{"a", "b", "c"}}, + {"with_empty", []string{"a", "", "c"}}, + {"utf8", []string{"测试", "テスト", "тест"}}, + {"long_string", []string{strings.Repeat("x", 128)}}, + {"many_elements", generateStrings(128)}, + {"many_elements_256", generateStrings(256)}, + {"127_byte_string", []string{strings.Repeat("x", 127)}}, + {"128_byte_string", []string{strings.Repeat("x", 128)}}, + {"mixed_lengths", []string{"a", strings.Repeat("b", 100), "", strings.Repeat("c", 200)}}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Old write + var oldBuf bytes.Buffer + err := oldWriteStringSlice(&oldBuf, tc.input) + require.NoError(t, err) + + // New write + var newBuf bytes.Buffer + err = newWriteStringSlice(&newBuf, tc.input) + require.NoError(t, err) + + // Bytes must match + require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), + "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) + + // New write -> old read + readBack, err := oldReadStringSlice(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + requireStringSliceEqual(t, tc.input, readBack) + + // Old write -> new read + readBack2, err := readRuleItemString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) + require.NoError(t, err) + requireStringSliceEqual(t, tc.input, readBack2) + }) + } +} + +func TestUint8SliceCompat(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input []uint8 + }{ + {"nil", nil}, + {"empty", []uint8{}}, + {"single_zero", []uint8{0}}, + {"single_max", []uint8{255}}, + {"multi", []uint8{0, 1, 127, 128, 255}}, + {"boundary", []uint8{0x00, 0x7f, 0x80, 0xff}}, + {"sequential", generateUint8Slice(256)}, + {"127_elements", generateUint8Slice(127)}, + {"128_elements", generateUint8Slice(128)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Old write + var oldBuf bytes.Buffer + err := oldWriteUint8Slice(&oldBuf, tc.input) + require.NoError(t, err) + + // New write + var newBuf bytes.Buffer + err = newWriteUint8Slice(&newBuf, tc.input) + require.NoError(t, err) + + // Bytes must match + require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), + "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) + + // New write -> old read + readBack, err := oldReadUint8Slice[uint8](bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + requireUint8SliceEqual(t, tc.input, readBack) + + // Old write -> new read + readBack2, err := readRuleItemUint8[uint8](bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) + require.NoError(t, err) + requireUint8SliceEqual(t, tc.input, readBack2) + }) + } +} + +func TestUint16SliceCompat(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input []uint16 + }{ + {"nil", nil}, + {"empty", []uint16{}}, + {"single_zero", []uint16{0}}, + {"single_max", []uint16{65535}}, + {"multi", []uint16{0, 255, 256, 32767, 32768, 65535}}, + {"ports", []uint16{80, 443, 8080, 8443}}, + {"127_elements", generateUint16Slice(127)}, + {"128_elements", generateUint16Slice(128)}, + {"256_elements", generateUint16Slice(256)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Old write + var oldBuf bytes.Buffer + err := oldWriteUint16Slice(&oldBuf, tc.input) + require.NoError(t, err) + + // New write + var newBuf bytes.Buffer + err = newWriteUint16Slice(&newBuf, tc.input) + require.NoError(t, err) + + // Bytes must match + require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), + "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) + + // New write -> old read + readBack, err := oldReadUint16Slice(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + requireUint16SliceEqual(t, tc.input, readBack) + + // Old write -> new read + readBack2, err := readRuleItemUint16(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) + require.NoError(t, err) + requireUint16SliceEqual(t, tc.input, readBack2) + }) + } +} + +func TestPrefixCompat(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input netip.Prefix + }{ + {"ipv4_0", netip.MustParsePrefix("0.0.0.0/0")}, + {"ipv4_8", netip.MustParsePrefix("10.0.0.0/8")}, + {"ipv4_16", netip.MustParsePrefix("192.168.0.0/16")}, + {"ipv4_24", netip.MustParsePrefix("192.168.1.0/24")}, + {"ipv4_32", netip.MustParsePrefix("1.2.3.4/32")}, + {"ipv6_0", netip.MustParsePrefix("::/0")}, + {"ipv6_64", netip.MustParsePrefix("2001:db8::/64")}, + {"ipv6_128", netip.MustParsePrefix("::1/128")}, + {"ipv6_full", netip.MustParsePrefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")}, + {"ipv4_private", netip.MustParsePrefix("172.16.0.0/12")}, + {"ipv6_link_local", netip.MustParsePrefix("fe80::/10")}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Old write + var oldBuf bytes.Buffer + err := oldWritePrefix(&oldBuf, tc.input) + require.NoError(t, err) + + // New write + var newBuf bytes.Buffer + err = newWritePrefix(&newBuf, tc.input) + require.NoError(t, err) + + // Bytes must match + require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), + "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) + + // New write -> new read (no old read for prefix) + readBack, err := readPrefix(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + require.Equal(t, tc.input, readBack) + + // Old write -> new read + readBack2, err := readPrefix(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) + require.NoError(t, err) + require.Equal(t, tc.input, readBack2) + }) + } +} + +func TestIPSetCompat(t *testing.T) { + t.Parallel() + + // Note: The old writeIPSet was buggy (varbin.Write with struct values wrote nothing). + // This test verifies the new implementation writes correct data and round-trips correctly. + + cases := []struct { + name string + input *netipx.IPSet + }{ + {"single_ipv4", buildIPSet("1.2.3.4")}, + {"ipv4_range", buildIPSet("192.168.0.0/16")}, + {"multi_ipv4", buildIPSet("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")}, + {"single_ipv6", buildIPSet("::1")}, + {"ipv6_range", buildIPSet("2001:db8::/32")}, + {"mixed", buildIPSet("10.0.0.0/8", "::1", "2001:db8::/32")}, + {"large", buildLargeIPSet(100)}, + {"adjacent_ranges", buildIPSet("192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24")}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // New write + var newBuf bytes.Buffer + err := writeIPSet(&newBuf, tc.input) + require.NoError(t, err) + + // Verify format starts with version byte (1) + uint64 count + require.True(t, len(newBuf.Bytes()) >= 9, "output too short") + require.Equal(t, byte(1), newBuf.Bytes()[0], "version byte mismatch") + + // New write -> old read (varbin.Read with pre-allocated slice works correctly) + readBack, err := oldReadIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + requireIPSetEqual(t, tc.input, readBack) + + // New write -> new read + readBack2, err := readIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) + require.NoError(t, err) + requireIPSetEqual(t, tc.input, readBack2) + }) + } +} + +// Helper functions + +func generateStrings(count int) []string { + result := make([]string, count) + for i := range result { + result[i] = strings.Repeat("x", i%50) + } + return result +} + +func generateUint8Slice(count int) []uint8 { + result := make([]uint8, count) + for i := range result { + result[i] = uint8(i % 256) + } + return result +} + +func generateUint16Slice(count int) []uint16 { + result := make([]uint16, count) + for i := range result { + result[i] = uint16(i * 257) + } + return result +} + +func buildIPSet(cidrs ...string) *netipx.IPSet { + var builder netipx.IPSetBuilder + for _, cidr := range cidrs { + prefix, err := netip.ParsePrefix(cidr) + if err != nil { + addr, err := netip.ParseAddr(cidr) + if err != nil { + panic(err) + } + builder.Add(addr) + } else { + builder.AddPrefix(prefix) + } + } + set, _ := builder.IPSet() + return set +} + +func buildLargeIPSet(count int) *netipx.IPSet { + var builder netipx.IPSetBuilder + for i := 0; i < count; i++ { + prefix := netip.PrefixFrom(netip.AddrFrom4([4]byte{10, byte(i / 256), byte(i % 256), 0}), 24) + builder.AddPrefix(prefix) + } + set, _ := builder.IPSet() + return set +} + +func requireStringSliceEqual(t *testing.T, expected, actual []string) { + t.Helper() + if len(expected) == 0 && len(actual) == 0 { + return + } + require.Equal(t, expected, actual) +} + +func requireUint8SliceEqual(t *testing.T, expected, actual []uint8) { + t.Helper() + if len(expected) == 0 && len(actual) == 0 { + return + } + require.Equal(t, expected, actual) +} + +func requireUint16SliceEqual(t *testing.T, expected, actual []uint16) { + t.Helper() + if len(expected) == 0 && len(actual) == 0 { + return + } + require.Equal(t, expected, actual) +} + +func requireIPSetEqual(t *testing.T, expected, actual *netipx.IPSet) { + t.Helper() + expectedRanges := expected.Ranges() + actualRanges := actual.Ranges() + require.Equal(t, len(expectedRanges), len(actualRanges), "range count mismatch") + for i := range expectedRanges { + require.Equal(t, expectedRanges[i].From(), actualRanges[i].From(), "range[%d].from mismatch", i) + require.Equal(t, expectedRanges[i].To(), actualRanges[i].To(), "range[%d].to mismatch", i) + } +} diff --git a/common/srs/ip_cidr.go b/common/srs/ip_cidr.go new file mode 100644 index 00000000..7c81abda --- /dev/null +++ b/common/srs/ip_cidr.go @@ -0,0 +1,44 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" +) + +func readPrefix(reader varbin.Reader) (netip.Prefix, error) { + addrLen, err := binary.ReadUvarint(reader) + if err != nil { + return netip.Prefix{}, err + } + addrSlice := make([]byte, addrLen) + _, err = io.ReadFull(reader, addrSlice) + if err != nil { + return netip.Prefix{}, err + } + prefixBits, err := reader.ReadByte() + if err != nil { + return netip.Prefix{}, err + } + return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil +} + +func writePrefix(writer varbin.Writer, prefix netip.Prefix) error { + addrSlice := prefix.Addr().AsSlice() + _, err := varbin.WriteUvarint(writer, uint64(len(addrSlice))) + if err != nil { + return err + } + _, err = writer.Write(addrSlice) + if err != nil { + return err + } + err = writer.WriteByte(uint8(prefix.Bits())) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 00000000..a10ac08c --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,98 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "os" + "unsafe" + + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) { + version, err := reader.ReadByte() + if err != nil { + return nil, err + } + if version != 1 { + return nil, os.ErrInvalid + } + // WTF why using uint64 here + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := range mySet.rr { + fromLen, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + toLen, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + mySet.rr[i].from = M.AddrFromIP(fromBytes) + mySet.rr[i].to = M.AddrFromIP(toBytes) + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error { + err := writer.WriteByte(1) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + fromBytes := rr.from.AsSlice() + _, err = varbin.WriteUvarint(writer, uint64(len(fromBytes))) + if err != nil { + return err + } + _, err = writer.Write(fromBytes) + if err != nil { + return err + } + toBytes := rr.to.AsSlice() + _, err = varbin.WriteUvarint(writer, uint64(len(toBytes))) + if err != nil { + return err + } + _, err = writer.Write(toBytes) + if err != nil { + return err + } + } + return nil +} diff --git a/common/stun/stun.go b/common/stun/stun.go new file mode 100644 index 00000000..a4bb9d5c --- /dev/null +++ b/common/stun/stun.go @@ -0,0 +1,612 @@ +package stun + +import ( + "context" + "crypto/rand" + "encoding/binary" + "fmt" + "net" + "net/netip" + "time" + + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + DefaultServer = "stun.voipgate.com:3478" + + magicCookie = 0x2112A442 + headerSize = 20 + + bindingRequest = 0x0001 + bindingSuccessResponse = 0x0101 + bindingErrorResponse = 0x0111 + + attrMappedAddress = 0x0001 + attrChangeRequest = 0x0003 + attrErrorCode = 0x0009 + attrXORMappedAddress = 0x0020 + attrOtherAddress = 0x802c + + familyIPv4 = 0x01 + familyIPv6 = 0x02 + + changeIP = 0x04 + changePort = 0x02 + + defaultRTO = 500 * time.Millisecond + minRTO = 250 * time.Millisecond + maxRetransmit = 2 +) + +type Phase int32 + +const ( + PhaseBinding Phase = iota + PhaseNATMapping + PhaseNATFiltering + PhaseDone +) + +type NATMapping int32 + +const ( + NATMappingUnknown NATMapping = iota + _ // reserved + NATMappingEndpointIndependent + NATMappingAddressDependent + NATMappingAddressAndPortDependent +) + +func (m NATMapping) String() string { + switch m { + case NATMappingEndpointIndependent: + return "Endpoint Independent" + case NATMappingAddressDependent: + return "Address Dependent" + case NATMappingAddressAndPortDependent: + return "Address and Port Dependent" + default: + return "Unknown" + } +} + +type NATFiltering int32 + +const ( + NATFilteringUnknown NATFiltering = iota + NATFilteringEndpointIndependent + NATFilteringAddressDependent + NATFilteringAddressAndPortDependent +) + +func (f NATFiltering) String() string { + switch f { + case NATFilteringEndpointIndependent: + return "Endpoint Independent" + case NATFilteringAddressDependent: + return "Address Dependent" + case NATFilteringAddressAndPortDependent: + return "Address and Port Dependent" + default: + return "Unknown" + } +} + +type TransactionID [12]byte + +type Options struct { + Server string + Dialer N.Dialer + Context context.Context + OnProgress func(Progress) +} + +type Progress struct { + Phase Phase + ExternalAddr string + LatencyMs int32 + NATMapping NATMapping + NATFiltering NATFiltering +} + +type Result struct { + ExternalAddr string + LatencyMs int32 + NATMapping NATMapping + NATFiltering NATFiltering + NATTypeSupported bool +} + +type parsedResponse struct { + xorMappedAddr netip.AddrPort + mappedAddr netip.AddrPort + otherAddr netip.AddrPort +} + +func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) { + if r.xorMappedAddr.IsValid() { + return r.xorMappedAddr, true + } + if r.mappedAddr.IsValid() { + return r.mappedAddr, true + } + return netip.AddrPort{}, false +} + +type stunAttribute struct { + typ uint16 + value []byte +} + +func newTransactionID() TransactionID { + var id TransactionID + _, _ = rand.Read(id[:]) + return id +} + +func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte { + attrLen := 0 + for _, attr := range attrs { + attrLen += 4 + len(attr.value) + paddingLen(len(attr.value)) + } + + buf := make([]byte, headerSize+attrLen) + binary.BigEndian.PutUint16(buf[0:2], bindingRequest) + binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen)) + binary.BigEndian.PutUint32(buf[4:8], magicCookie) + copy(buf[8:20], txID[:]) + + offset := headerSize + for _, attr := range attrs { + binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ) + binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value))) + copy(buf[offset+4:offset+4+len(attr.value)], attr.value) + offset += 4 + len(attr.value) + paddingLen(len(attr.value)) + } + + return buf +} + +func changeRequestAttr(flags byte) stunAttribute { + return stunAttribute{ + typ: attrChangeRequest, + value: []byte{0, 0, 0, flags}, + } +} + +func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) { + if len(data) < headerSize { + return nil, E.New("response too short") + } + + msgType := binary.BigEndian.Uint16(data[0:2]) + if msgType&0xC000 != 0 { + return nil, E.New("invalid STUN message: top 2 bits not zero") + } + + cookie := binary.BigEndian.Uint32(data[4:8]) + if cookie != magicCookie { + return nil, E.New("invalid magic cookie") + } + + var txID TransactionID + copy(txID[:], data[8:20]) + if txID != expectedTxID { + return nil, E.New("transaction ID mismatch") + } + + msgLen := int(binary.BigEndian.Uint16(data[2:4])) + if msgLen > len(data)-headerSize { + return nil, E.New("message length exceeds data") + } + + attrData := data[headerSize : headerSize+msgLen] + + if msgType == bindingErrorResponse { + return nil, parseErrorResponse(attrData) + } + if msgType != bindingSuccessResponse { + return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType)) + } + + resp := &parsedResponse{} + offset := 0 + for offset+4 <= len(attrData) { + attrType := binary.BigEndian.Uint16(attrData[offset : offset+2]) + attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4])) + if offset+4+attrLen > len(attrData) { + break + } + attrValue := attrData[offset+4 : offset+4+attrLen] + + switch attrType { + case attrXORMappedAddress: + addr, err := parseXORMappedAddress(attrValue, txID) + if err == nil { + resp.xorMappedAddr = addr + } + case attrMappedAddress: + addr, err := parseMappedAddress(attrValue) + if err == nil { + resp.mappedAddr = addr + } + case attrOtherAddress: + addr, err := parseMappedAddress(attrValue) + if err == nil { + resp.otherAddr = addr + } + } + + offset += 4 + attrLen + paddingLen(attrLen) + } + + return resp, nil +} + +func parseErrorResponse(data []byte) error { + offset := 0 + for offset+4 <= len(data) { + attrType := binary.BigEndian.Uint16(data[offset : offset+2]) + attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4])) + if offset+4+attrLen > len(data) { + break + } + if attrType == attrErrorCode && attrLen >= 4 { + attrValue := data[offset+4 : offset+4+attrLen] + class := int(attrValue[2] & 0x07) + number := int(attrValue[3]) + code := class*100 + number + if attrLen > 4 { + return E.New("STUN error ", code, ": ", string(attrValue[4:])) + } + return E.New("STUN error ", code) + } + offset += 4 + attrLen + paddingLen(attrLen) + } + return E.New("STUN error response") +} + +func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) { + if len(data) < 4 { + return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short") + } + + family := data[1] + xPort := binary.BigEndian.Uint16(data[2:4]) + port := xPort ^ uint16(magicCookie>>16) + + switch family { + case familyIPv4: + if len(data) < 8 { + return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short") + } + var ip [4]byte + binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie) + return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil + case familyIPv6: + if len(data) < 20 { + return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short") + } + var ip [16]byte + var xorKey [16]byte + binary.BigEndian.PutUint32(xorKey[0:4], magicCookie) + copy(xorKey[4:16], txID[:]) + for i := range 16 { + ip[i] = data[4+i] ^ xorKey[i] + } + return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil + default: + return netip.AddrPort{}, E.New("unknown address family: ", family) + } +} + +func parseMappedAddress(data []byte) (netip.AddrPort, error) { + if len(data) < 4 { + return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short") + } + + family := data[1] + port := binary.BigEndian.Uint16(data[2:4]) + + switch family { + case familyIPv4: + if len(data) < 8 { + return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short") + } + return netip.AddrPortFrom( + netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port, + ), nil + case familyIPv6: + if len(data) < 20 { + return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short") + } + var ip [16]byte + copy(ip[:], data[4:20]) + return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil + default: + return netip.AddrPort{}, E.New("unknown address family: ", family) + } +} + +func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) { + request := buildBindingRequest(txID, attrs...) + currentRTO := rto + retransmitCount := 0 + + sendTime := time.Now() + _, err := conn.WriteTo(request, addr) + if err != nil { + return nil, 0, E.Cause(err, "send STUN request") + } + + buf := make([]byte, 1024) + for { + err = conn.SetReadDeadline(sendTime.Add(currentRTO)) + if err != nil { + return nil, 0, E.Cause(err, "set read deadline") + } + + n, _, readErr := conn.ReadFrom(buf) + if readErr != nil { + if E.IsTimeout(readErr) && retransmitCount < maxRetransmit { + retransmitCount++ + currentRTO *= 2 + sendTime = time.Now() + _, err = conn.WriteTo(request, addr) + if err != nil { + return nil, 0, E.Cause(err, "retransmit STUN request") + } + continue + } + return nil, 0, E.Cause(readErr, "read STUN response") + } + + if n < headerSize || buf[0]&0xC0 != 0 || + binary.BigEndian.Uint32(buf[4:8]) != magicCookie { + continue + } + var receivedTxID TransactionID + copy(receivedTxID[:], buf[8:20]) + if receivedTxID != txID { + continue + } + + latency := time.Since(sendTime) + + resp, parseErr := parseResponse(buf[:n], txID) + if parseErr != nil { + return nil, 0, parseErr + } + + return resp, latency, nil + } +} + +func Run(options Options) (*Result, error) { + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + + server := options.Server + if server == "" { + server = DefaultServer + } + serverSocksaddr := M.ParseSocksaddr(server) + if serverSocksaddr.Port == 0 { + serverSocksaddr.Port = 3478 + } + + reportProgress := options.OnProgress + if reportProgress == nil { + reportProgress = func(Progress) {} + } + + var ( + packetConn net.PacketConn + serverAddr net.Addr + err error + ) + + if options.Dialer != nil { + packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr) + if err != nil { + return nil, E.Cause(err, "create UDP socket") + } + serverAddr = serverSocksaddr + } else { + serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String()) + if resolveErr != nil { + return nil, E.Cause(resolveErr, "resolve STUN server") + } + packetConn, err = net.ListenPacket("udp", "") + if err != nil { + return nil, E.Cause(err, "create UDP socket") + } + serverAddr = serverUDPAddr + } + defer func() { + _ = packetConn.Close() + }() + if deadline.NeedAdditionalReadDeadline(packetConn) { + packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn)) + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + rto := defaultRTO + + // Phase 1: Binding + reportProgress(Progress{Phase: PhaseBinding}) + + txID := newTransactionID() + resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto) + if err != nil { + return nil, E.Cause(err, "binding request") + } + + rto = max(minRTO, 3*latency) + + externalAddr, ok := resp.externalAddr() + if !ok { + return nil, E.New("no mapped address in response") + } + + result := &Result{ + ExternalAddr: externalAddr.String(), + LatencyMs: int32(latency.Milliseconds()), + } + + reportProgress(Progress{ + Phase: PhaseBinding, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + }) + + otherAddr := resp.otherAddr + if !otherAddr.IsValid() { + result.NATTypeSupported = false + reportProgress(Progress{ + Phase: PhaseDone, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + }) + return result, nil + } + result.NATTypeSupported = true + + select { + case <-ctx.Done(): + return result, nil + default: + } + + // Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3) + reportProgress(Progress{ + Phase: PhaseNATMapping, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + }) + + result.NATMapping = detectNATMapping( + packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto, + ) + + reportProgress(Progress{ + Phase: PhaseNATMapping, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: result.NATMapping, + }) + + select { + case <-ctx.Done(): + return result, nil + default: + } + + // Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4) + reportProgress(Progress{ + Phase: PhaseNATFiltering, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: result.NATMapping, + }) + + result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto) + + reportProgress(Progress{ + Phase: PhaseDone, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: result.NATMapping, + NATFiltering: result.NATFiltering, + }) + + return result, nil +} + +func detectNATMapping( + conn net.PacketConn, + serverPort uint16, + externalAddr netip.AddrPort, + otherAddr netip.AddrPort, + rto time.Duration, +) NATMapping { + // Mapping Test II: Send to other_ip:server_port + testIIAddr := net.UDPAddrFromAddrPort( + netip.AddrPortFrom(otherAddr.Addr(), serverPort), + ) + txID2 := newTransactionID() + resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto) + if err != nil { + return NATMappingUnknown + } + + externalAddr2, ok := resp2.externalAddr() + if !ok { + return NATMappingUnknown + } + + if externalAddr == externalAddr2 { + return NATMappingEndpointIndependent + } + + // Mapping Test III: Send to other_ip:other_port + testIIIAddr := net.UDPAddrFromAddrPort(otherAddr) + txID3 := newTransactionID() + resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto) + if err != nil { + return NATMappingUnknown + } + + externalAddr3, ok := resp3.externalAddr() + if !ok { + return NATMappingUnknown + } + + if externalAddr2 == externalAddr3 { + return NATMappingAddressDependent + } + return NATMappingAddressAndPortDependent +} + +func detectNATFiltering( + conn net.PacketConn, + serverAddr net.Addr, + rto time.Duration, +) NATFiltering { + // Filtering Test II: Request response from different IP and port + txID := newTransactionID() + _, _, err := roundTrip(conn, serverAddr, txID, + []stunAttribute{changeRequestAttr(changeIP | changePort)}, rto) + if err == nil { + return NATFilteringEndpointIndependent + } + + // Filtering Test III: Request response from different port only + txID = newTransactionID() + _, _, err = roundTrip(conn, serverAddr, txID, + []stunAttribute{changeRequestAttr(changePort)}, rto) + if err == nil { + return NATFilteringAddressDependent + } + + return NATFilteringAddressAndPortDependent +} + +func paddingLen(n int) int { + if n%4 == 0 { + return 0 + } + return 4 - n%4 +} diff --git a/common/taskmonitor/monitor.go b/common/taskmonitor/monitor.go new file mode 100644 index 00000000..a23990fa --- /dev/null +++ b/common/taskmonitor/monitor.go @@ -0,0 +1,31 @@ +package taskmonitor + +import ( + "time" + + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" +) + +type Monitor struct { + logger logger.Logger + timeout time.Duration + timer *time.Timer +} + +func New(logger logger.Logger, timeout time.Duration) *Monitor { + return &Monitor{ + logger: logger, + timeout: timeout, + } +} + +func (m *Monitor) Start(taskName ...any) { + m.timer = time.AfterFunc(m.timeout, func() { + m.logger.Warn(F.ToString(taskName...), " take too much time to finish!") + }) +} + +func (m *Monitor) Finish() { + m.timer.Stop() +} diff --git a/common/tls/acme.go b/common/tls/acme.go new file mode 100644 index 00000000..d576fc6b --- /dev/null +++ b/common/tls/acme.go @@ -0,0 +1,134 @@ +//go:build with_acme + +package tls + +import ( + "context" + "crypto/tls" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "github.com/caddyserver/certmagic" + "github.com/libdns/acmedns" + "github.com/libdns/alidns" + "github.com/libdns/cloudflare" + "github.com/mholt/acmez/v3/acme" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type acmeWrapper struct { + ctx context.Context + cfg *certmagic.Config + cache *certmagic.Cache + domain []string +} + +func (w *acmeWrapper) Start() error { + return w.cfg.ManageSync(w.ctx, w.domain) +} + +func (w *acmeWrapper) Close() error { + w.cache.Stop() + return nil +} + +func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { + var acmeServer string + switch options.Provider { + case "", "letsencrypt": + acmeServer = certmagic.LetsEncryptProductionCA + case "zerossl": + acmeServer = certmagic.ZeroSSLProductionCA + default: + if !strings.HasPrefix(options.Provider, "https://") { + return nil, nil, E.New("unsupported acme provider: " + options.Provider) + } + acmeServer = options.Provider + } + var storage certmagic.Storage + if options.DataDirectory != "" { + storage = &certmagic.FileStorage{ + Path: options.DataDirectory, + } + } else { + storage = certmagic.Default.Storage + } + zapLogger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(ACMEEncoderConfig()), + &ACMELogWriter{Logger: logger}, + zap.DebugLevel, + )) + config := &certmagic.Config{ + DefaultServerName: options.DefaultServerName, + Storage: storage, + Logger: zapLogger, + } + acmeConfig := certmagic.ACMEIssuer{ + CA: acmeServer, + Email: options.Email, + Agreed: true, + DisableHTTPChallenge: options.DisableHTTPChallenge, + DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, + AltHTTPPort: int(options.AlternativeHTTPPort), + AltTLSALPNPort: int(options.AlternativeTLSPort), + Logger: zapLogger, + } + if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" { + var solver certmagic.DNS01Solver + switch dnsOptions.Provider { + case C.DNSProviderAliDNS: + solver.DNSProvider = &alidns.Provider{ + CredentialInfo: alidns.CredentialInfo{ + AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID, + AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret, + RegionID: dnsOptions.AliDNSOptions.RegionID, + SecurityToken: dnsOptions.AliDNSOptions.SecurityToken, + }, + } + case C.DNSProviderCloudflare: + solver.DNSProvider = &cloudflare.Provider{ + APIToken: dnsOptions.CloudflareOptions.APIToken, + ZoneToken: dnsOptions.CloudflareOptions.ZoneToken, + } + case C.DNSProviderACMEDNS: + solver.DNSProvider = &acmedns.Provider{ + Username: dnsOptions.ACMEDNSOptions.Username, + Password: dnsOptions.ACMEDNSOptions.Password, + Subdomain: dnsOptions.ACMEDNSOptions.Subdomain, + ServerURL: dnsOptions.ACMEDNSOptions.ServerURL, + } + default: + return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider) + } + acmeConfig.DNS01Solver = &solver + } + if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" { + acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount) + } + config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)} + cache := certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { + return config, nil + }, + Logger: zapLogger, + }) + config = certmagic.New(cache, *config) + var tlsConfig *tls.Config + if acmeConfig.DisableTLSALPNChallenge || acmeConfig.DNS01Solver != nil { + tlsConfig = &tls.Config{ + GetCertificate: config.GetCertificate, + } + } else { + tlsConfig = &tls.Config{ + GetCertificate: config.GetCertificate, + NextProtos: []string{C.ACMETLS1Protocol}, + } + } + return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil +} diff --git a/common/tls/acme_logger.go b/common/tls/acme_logger.go new file mode 100644 index 00000000..cb3a1e3c --- /dev/null +++ b/common/tls/acme_logger.go @@ -0,0 +1,41 @@ +package tls + +import ( + "strings" + + "github.com/sagernet/sing/common/logger" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type ACMELogWriter struct { + Logger logger.Logger +} + +func (w *ACMELogWriter) Write(p []byte) (n int, err error) { + logLine := strings.ReplaceAll(string(p), " ", ": ") + switch { + case strings.HasPrefix(logLine, "error: "): + w.Logger.Error(logLine[7:]) + case strings.HasPrefix(logLine, "warn: "): + w.Logger.Warn(logLine[6:]) + case strings.HasPrefix(logLine, "info: "): + w.Logger.Info(logLine[6:]) + case strings.HasPrefix(logLine, "debug: "): + w.Logger.Debug(logLine[7:]) + default: + w.Logger.Debug(logLine) + } + return len(p), nil +} + +func (w *ACMELogWriter) Sync() error { + return nil +} + +func ACMEEncoderConfig() zapcore.EncoderConfig { + config := zap.NewProductionEncoderConfig() + config.TimeKey = zapcore.OmitKey + return config +} diff --git a/common/tls/acme_stub.go b/common/tls/acme_stub.go new file mode 100644 index 00000000..53ced06d --- /dev/null +++ b/common/tls/acme_stub.go @@ -0,0 +1,17 @@ +//go:build !with_acme + +package tls + +import ( + "context" + "crypto/tls" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +func startACME(ctx context.Context, logger logger.Logger, 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`) +} diff --git a/common/tls/client.go b/common/tls/client.go new file mode 100644 index 00000000..83969954 --- /dev/null +++ b/common/tls/client.go @@ -0,0 +1,139 @@ +package tls + +import ( + "context" + "crypto/tls" + "errors" + "net" + "os" + + "github.com/sagernet/sing-box/common/badtls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" +) + +func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { + if !options.Enabled { + return dialer, nil + } + config, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger, + ServerAddress: serverAddress, + Options: options, + }) + if err != nil { + return nil, err + } + return NewDialer(dialer, config), nil +} + +func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger, + ServerAddress: serverAddress, + Options: options, + }) +} + +type ClientOptions struct { + Context context.Context + Logger logger.ContextLogger + ServerAddress string + Options option.OutboundTLSOptions + KTLSCompatible bool +} + +func NewClientWithOptions(options ClientOptions) (Config, error) { + if !options.Options.Enabled { + return nil, nil + } + if !options.KTLSCompatible { + if options.Options.KernelTx { + options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx") + } + } + if options.Options.KernelRx { + options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx") + } + if options.Options.Reality != nil && options.Options.Reality.Enabled { + return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options) + } else if options.Options.UTLS != nil && options.Options.UTLS.Enabled { + return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options) + } + return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options) +} + +func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { + ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) + defer cancel() + tlsConn, err := aTLS.ClientHandshake(ctx, conn, config) + if err != nil { + return nil, err + } + readWaitConn, err := badtls.NewReadWaitConn(tlsConn) + if err == nil { + return readWaitConn, nil + } else if err != os.ErrInvalid { + return nil, err + } + return tlsConn, nil +} + +type Dialer interface { + N.Dialer + DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) +} + +type defaultDialer struct { + dialer N.Dialer + config Config +} + +func NewDialer(dialer N.Dialer, config Config) Dialer { + return &defaultDialer{dialer, config} +} + +func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if N.NetworkName(network) != N.NetworkTCP { + return nil, os.ErrInvalid + } + return d.DialTLSContext(ctx, destination) +} + +func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) { + return d.dialContext(ctx, destination, true) +} + +func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) { + conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination) + if err != nil { + return nil, err + } + tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config) + if err != nil { + conn.Close() + var echErr *tls.ECHRejectionError + if echRetry && errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 { + if echConfig, isECH := d.config.(ECHCapableConfig); isECH { + echConfig.SetECHConfigList(echErr.RetryConfigList) + return d.dialContext(ctx, destination, false) + } + } + return nil, err + } + return tlsConn, nil +} + +func (d *defaultDialer) Upstream() any { + return d.dialer +} diff --git a/common/tls/common.go b/common/tls/common.go new file mode 100644 index 00000000..66cf1223 --- /dev/null +++ b/common/tls/common.go @@ -0,0 +1,12 @@ +package tls + +const ( + VersionTLS10 = 0x0301 + VersionTLS11 = 0x0302 + VersionTLS12 = 0x0303 + VersionTLS13 = 0x0304 + + // Deprecated: SSLv3 is cryptographically broken, and is no longer + // supported by this package. See golang.org/issue/32716. + VersionSSL30 = 0x0300 +) diff --git a/common/tls/config.go b/common/tls/config.go new file mode 100644 index 00000000..72bbd194 --- /dev/null +++ b/common/tls/config.go @@ -0,0 +1,37 @@ +package tls + +import ( + "crypto/tls" + + E "github.com/sagernet/sing/common/exceptions" + aTLS "github.com/sagernet/sing/common/tls" +) + +type ( + Config = aTLS.Config + ConfigCompat = aTLS.ConfigCompat + ServerConfig = aTLS.ServerConfig + ServerConfigCompat = aTLS.ServerConfigCompat + WithSessionIDGenerator = aTLS.WithSessionIDGenerator + Conn = aTLS.Conn + + STDConfig = tls.Config + STDConn = tls.Conn + ConnectionState = tls.ConnectionState + CurveID = tls.CurveID +) + +func ParseTLSVersion(version string) (uint16, error) { + switch version { + case "1.0": + return tls.VersionTLS10, nil + case "1.1": + return tls.VersionTLS11, nil + case "1.2": + return tls.VersionTLS12, nil + case "1.3": + return tls.VersionTLS13, nil + default: + return 0, E.New("unknown tls version:", version) + } +} diff --git a/common/tls/ech.go b/common/tls/ech.go new file mode 100644 index 00000000..8c884cab --- /dev/null +++ b/common/tls/ech.go @@ -0,0 +1,208 @@ +//go:build go1.24 + +package tls + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/pem" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + aTLS "github.com/sagernet/sing/common/tls" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" + "golang.org/x/crypto/cryptobyte" +) + +func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) { + var echConfig []byte + if len(options.ECH.Config) > 0 { + echConfig = []byte(strings.Join(options.ECH.Config, "\n")) + } else if options.ECH.ConfigPath != "" { + content, err := os.ReadFile(options.ECH.ConfigPath) + if err != nil { + return nil, E.Cause(err, "read ECH config") + } + echConfig = content + } + //nolint:staticcheck + if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled { + return nil, E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0") + } + if len(echConfig) > 0 { + block, rest := pem.Decode(echConfig) + if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { + return nil, E.New("invalid ECH configs pem") + } + clientConfig.SetECHConfigList(block.Bytes) + return clientConfig, nil + } else { + return &ECHClientConfig{ + ECHCapableConfig: clientConfig, + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + queryServerName: options.ECH.QueryServerName, + }, nil + } +} + +func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error { + var echKey []byte + if len(options.ECH.Key) > 0 { + echKey = []byte(strings.Join(options.ECH.Key, "\n")) + } else if options.ECH.KeyPath != "" { + content, err := os.ReadFile(options.ECH.KeyPath) + if err != nil { + return E.Cause(err, "read ECH keys") + } + echKey = content + *echKeyPath = options.ECH.KeyPath + } else { + return E.New("missing ECH keys") + } + echKeys, err := parseECHKeys(echKey) + if err != nil { + return E.Cause(err, "parse ECH keys") + } + tlsConfig.EncryptedClientHelloKeys = echKeys + //nolint:staticcheck + if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled { + return E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0") + } + return nil +} + +func (c *STDServerConfig) setECHServerConfig(echKey []byte) error { + echKeys, err := parseECHKeys(echKey) + if err != nil { + return err + } + c.access.Lock() + config := c.config.Clone() + config.EncryptedClientHelloKeys = echKeys + c.config = config + c.access.Unlock() + return nil +} + +func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) { + block, _ := pem.Decode(echKey) + if block == nil || block.Type != "ECH KEYS" { + return nil, E.New("invalid ECH keys pem") + } + echKeys, err := UnmarshalECHKeys(block.Bytes) + if err != nil { + return nil, E.Cause(err, "parse ECH keys") + } + return echKeys, nil +} + +type ECHClientConfig struct { + ECHCapableConfig + access sync.Mutex + dnsRouter adapter.DNSRouter + queryServerName string + lastTTL time.Duration + lastUpdate time.Time +} + +func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { + tlsConn, err := s.fetchAndHandshake(ctx, conn) + if err != nil { + return nil, err + } + err = tlsConn.HandshakeContext(ctx) + if err != nil { + return nil, err + } + return tlsConn, nil +} + +func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { + s.access.Lock() + defer s.access.Unlock() + if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL { + queryServerName := s.queryServerName + if queryServerName == "" { + queryServerName = s.ServerName() + } + message := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{ + { + Name: mDNS.Fqdn(queryServerName), + Qtype: mDNS.TypeHTTPS, + Qclass: mDNS.ClassINET, + }, + }, + } + response, err := s.dnsRouter.Exchange(ctx, message, adapter.DNSQueryOptions{}) + if err != nil { + return nil, E.Cause(err, "fetch ECH config list") + } + 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: + for _, value := range resource.Value { + if value.Key().String() == "ech" { + echConfigList, err := base64.StdEncoding.DecodeString(value.String()) + if err != nil { + return nil, E.Cause(err, "decode ECH config") + } + s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second + s.lastUpdate = time.Now() + s.SetECHConfigList(echConfigList) + break match + } + } + } + } + if len(s.ECHConfigList()) == 0 { + return nil, E.New("no ECH config found in DNS records") + } + } + return s.Client(conn) +} + +func (s *ECHClientConfig) Clone() Config { + return &ECHClientConfig{ + ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), + dnsRouter: s.dnsRouter, + queryServerName: s.queryServerName, + lastUpdate: s.lastUpdate, + } +} + +func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { + var keys []tls.EncryptedClientHelloKey + rawString := cryptobyte.String(raw) + for !rawString.Empty() { + var key tls.EncryptedClientHelloKey + if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) { + return nil, E.New("error parsing private key") + } + if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) { + return nil, E.New("error parsing config") + } + keys = append(keys, key) + } + if len(keys) == 0 { + return nil, E.New("empty ECH keys") + } + return keys, nil +} diff --git a/common/tls/ech_shared.go b/common/tls/ech_shared.go new file mode 100644 index 00000000..1cdfb7e1 --- /dev/null +++ b/common/tls/ech_shared.go @@ -0,0 +1,81 @@ +package tls + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/pem" + + "golang.org/x/crypto/cryptobyte" +) + +type ECHCapableConfig interface { + Config + ECHConfigList() []byte + SetECHConfigList([]byte) +} + +func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) { + echKey, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return + } + echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0) + if err != nil { + return + } + configBuilder := cryptobyte.NewBuilder(nil) + configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echConfig) + }) + configBytes, err := configBuilder.Bytes() + if err != nil { + return + } + keyBuilder := cryptobyte.NewBuilder(nil) + keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echKey.Bytes()) + }) + keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echConfig) + }) + keyBytes, err := keyBuilder.Bytes() + if err != nil { + return + } + configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes})) + keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes})) + return +} + +func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) { + const extensionEncryptedClientHello = 0xfe0d + const DHKEM_X25519_HKDF_SHA256 = 0x0020 + const KDF_HKDF_SHA256 = 0x0001 + builder := cryptobyte.NewBuilder(nil) + builder.AddUint16(extensionEncryptedClientHello) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddUint8(id) + + builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(pubKey) + }) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + const ( + AEAD_AES_128_GCM = 0x0001 + AEAD_AES_256_GCM = 0x0002 + AEAD_ChaCha20Poly1305 = 0x0003 + ) + for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} { + builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support + builder.AddUint16(aeadID) + } + }) + builder.AddUint8(maxNameLen) + builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes([]byte(publicName)) + }) + builder.AddUint16(0) // extensions + }) + return builder.Bytes() +} diff --git a/common/tls/ech_tag_stub.go b/common/tls/ech_tag_stub.go new file mode 100644 index 00000000..1a9cbd56 --- /dev/null +++ b/common/tls/ech_tag_stub.go @@ -0,0 +1,5 @@ +//go:build with_ech + +package tls + +var _ int = "Due to the migration to stdlib, the separate `with_ech` build tag has been deprecated and is no longer needed, please update your build configuration." diff --git a/common/tls/ktls.go b/common/tls/ktls.go new file mode 100644 index 00000000..dd564f53 --- /dev/null +++ b/common/tls/ktls.go @@ -0,0 +1,67 @@ +package tls + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/common/ktls" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + aTLS "github.com/sagernet/sing/common/tls" +) + +type KTLSClientConfig struct { + Config + logger logger.ContextLogger + kernelTx, kernelRx bool +} + +func (w *KTLSClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { + tlsConn, err := aTLS.ClientHandshake(ctx, conn, w.Config) + if err != nil { + return nil, err + } + kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx) + if err != nil { + tlsConn.Close() + return nil, E.Cause(err, "initialize kernel TLS") + } + return kConn, nil +} + +func (w *KTLSClientConfig) Clone() Config { + return &KTLSClientConfig{ + w.Config.Clone(), + w.logger, + w.kernelTx, + w.kernelRx, + } +} + +type KTlSServerConfig struct { + ServerConfig + logger logger.ContextLogger + kernelTx, kernelRx bool +} + +func (w *KTlSServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { + tlsConn, err := aTLS.ServerHandshake(ctx, conn, w.ServerConfig) + if err != nil { + return nil, err + } + kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx) + if err != nil { + tlsConn.Close() + return nil, E.Cause(err, "initialize kernel TLS") + } + return kConn, nil +} + +func (w *KTlSServerConfig) Clone() Config { + return &KTlSServerConfig{ + w.ServerConfig.Clone().(ServerConfig), + w.logger, + w.kernelTx, + w.kernelRx, + } +} diff --git a/common/tls/mkcert.go b/common/tls/mkcert.go new file mode 100644 index 00000000..4e0ed102 --- /dev/null +++ b/common/tls/mkcert.go @@ -0,0 +1,65 @@ +package tls + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" +) + +func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) { + if timeFunc == nil { + timeFunc = time.Now + } + privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour)) + if err != nil { + return nil, err + } + certificate, err := tls.X509KeyPair(publicKeyPem, privateKeyPem) + if err != nil { + return nil, err + } + return &certificate, err +} + +func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return + } + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return + } + 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 + parentKey = key + } + publicDer, err := x509.CreateCertificate(rand.Reader, template, parent, key.Public(), parentKey) + if err != nil { + return + } + privateDer, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return + } + publicKeyPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}) + privateKeyPem = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}) + return +} diff --git a/common/tls/reality_client.go b/common/tls/reality_client.go new file mode 100644 index 00000000..9362d2f8 --- /dev/null +++ b/common/tls/reality_client.go @@ -0,0 +1,332 @@ +//go:build with_utls + +package tls + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + mRand "math/rand" + "net" + "net/http" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/debug" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/ntp" + aTLS "github.com/sagernet/sing/common/tls" + + utls "github.com/metacubex/utls" + "golang.org/x/crypto/hkdf" + "golang.org/x/net/http2" +) + +var _ ConfigCompat = (*RealityClientConfig)(nil) + +type RealityClientConfig struct { + ctx context.Context + uClient *UTLSClientConfig + publicKey []byte + shortID [8]byte +} + +func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + if options.UTLS == nil || !options.UTLS.Enabled { + return nil, E.New("uTLS is required by reality client") + } + + uClient, err := NewUTLSClient(ctx, logger, serverAddress, options) + if err != nil { + return nil, err + } + + publicKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public_key") + } + if len(publicKey) != 32 { + return nil, E.New("invalid public_key") + } + var shortID [8]byte + decodedLen, err := hex.Decode(shortID[:], []byte(options.Reality.ShortID)) + if err != nil { + return nil, E.Cause(err, "decode short_id") + } + if decodedLen > 8 { + return nil, E.New("invalid short_id") + } + + var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID} + if options.KernelRx || options.KernelTx { + if !C.IsLinux { + return nil, E.New("kTLS is only supported on Linux") + } + config = &KTLSClientConfig{ + Config: config, + logger: logger, + kernelTx: options.KernelTx, + kernelRx: options.KernelRx, + } + } + return config, nil +} + +func (e *RealityClientConfig) ServerName() string { + return e.uClient.ServerName() +} + +func (e *RealityClientConfig) SetServerName(serverName string) { + e.uClient.SetServerName(serverName) +} + +func (e *RealityClientConfig) NextProtos() []string { + return e.uClient.NextProtos() +} + +func (e *RealityClientConfig) SetNextProtos(nextProto []string) { + e.uClient.SetNextProtos(nextProto) +} + +func (e *RealityClientConfig) STDConfig() (*STDConfig, error) { + return nil, E.New("unsupported usage for reality") +} + +func (e *RealityClientConfig) Client(conn net.Conn) (Conn, error) { + return ClientHandshake(context.Background(), conn, e) +} + +func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { + verifier := &realityVerifier{ + serverName: e.uClient.ServerName(), + } + uConfig := e.uClient.config.Clone() + uConfig.InsecureSkipVerify = true + uConfig.SessionTicketsDisabled = true + uConfig.VerifyPeerCertificate = verifier.VerifyPeerCertificate + uConn := utls.UClient(conn, uConfig, e.uClient.id) + verifier.UConn = uConn + err := uConn.BuildHandshakeState() + if err != nil { + return nil, err + } + for _, extension := range uConn.Extensions { + if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { + ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool { + return curveID != utls.X25519MLKEM768 + }) + } + if ks, ok := extension.(*utls.KeyShareExtension); ok { + ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool { + return share.Group != utls.X25519MLKEM768 + }) + } + } + err = uConn.BuildHandshakeState() + if err != nil { + return nil, err + } + + if len(uConfig.NextProtos) > 0 { + for _, extension := range uConn.Extensions { + if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN { + alpnExtension.AlpnProtocols = uConfig.NextProtos + break + } + } + } + + hello := uConn.HandshakeState.Hello + hello.SessionId = make([]byte, 32) + copy(hello.Raw[39:], hello.SessionId) + + var nowTime time.Time + if uConfig.Time != nil { + nowTime = uConfig.Time() + } else { + nowTime = time.Now() + } + binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix())) + + hello.SessionId[0] = 1 + hello.SessionId[1] = 8 + hello.SessionId[2] = 1 + binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) + copy(hello.SessionId[8:], e.shortID[:]) + if debug.Enabled { + fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16]) + } + publicKey, err := ecdh.X25519().NewPublicKey(e.publicKey) + if err != nil { + return nil, err + } + keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys + if keyShareKeys == nil { + return nil, E.New("nil KeyShareKeys") + } + ecdheKey := keyShareKeys.Ecdhe + if ecdheKey == nil { + return nil, E.New("nil ecdheKey") + } + authKey, err := ecdheKey.ECDH(publicKey) + if err != nil { + return nil, err + } + if authKey == nil { + return nil, E.New("nil auth_key") + } + verifier.authKey = authKey + _, err = hkdf.New(sha256.New, authKey, hello.Random[:20], []byte("REALITY")).Read(authKey) + if err != nil { + return nil, err + } + aesBlock, _ := aes.NewCipher(authKey) + aesGcmCipher, _ := cipher.NewGCM(aesBlock) + aesGcmCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) + copy(hello.Raw[39:], hello.SessionId) + if debug.Enabled { + fmt.Printf("REALITY hello.sessionId: %v\n", hello.SessionId) + fmt.Printf("REALITY uConn.AuthKey: %v\n", authKey) + } + + err = uConn.HandshakeContext(ctx) + if err != nil { + return nil, err + } + + if debug.Enabled { + fmt.Printf("REALITY Conn.Verified: %v\n", verifier.verified) + } + + if !verifier.verified { + go realityClientFallback(e.ctx, uConn, e.uClient.ServerName(), e.uClient.id) + return nil, E.New("reality verification failed") + } + + return &realityClientConnWrapper{uConn}, nil +} + +func realityClientFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) { + defer uConn.Close() + client := &http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) { + return uConn, nil + }, + TLSClientConfig: &tls.Config{ + Time: ntp.TimeFuncFromContext(ctx), + RootCAs: adapter.RootPoolFromContext(ctx), + }, + }, + } + request, _ := http.NewRequest("GET", "https://"+serverName, nil) + request.Header.Set("User-Agent", fingerprint.Client) + request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", mRand.Intn(32)+30)}) + response, err := client.Do(request) + if err != nil { + return + } + _, _ = io.Copy(io.Discard, response.Body) + response.Body.Close() +} + +func (e *RealityClientConfig) Clone() Config { + return &RealityClientConfig{ + e.ctx, + e.uClient.Clone().(*UTLSClientConfig), + e.publicKey, + e.shortID, + } +} + +type realityVerifier struct { + *utls.UConn + serverName string + authKey []byte + verified bool +} + +func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") + certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + p.Offset)) + if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { + h := hmac.New(sha512.New, c.authKey) + h.Write(pub) + if bytes.Equal(h.Sum(nil), certs[0].Signature) { + c.verified = true + return nil + } + } + opts := x509.VerifyOptions{ + DNSName: c.serverName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err != nil { + return err + } + return nil +} + +type realityClientConnWrapper struct { + *utls.UConn +} + +func (c *realityClientConnWrapper) ConnectionState() tls.ConnectionState { + state := c.Conn.ConnectionState() + //nolint:staticcheck + return tls.ConnectionState{ + Version: state.Version, + HandshakeComplete: state.HandshakeComplete, + DidResume: state.DidResume, + CipherSuite: state.CipherSuite, + NegotiatedProtocol: state.NegotiatedProtocol, + NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, + ServerName: state.ServerName, + PeerCertificates: state.PeerCertificates, + VerifiedChains: state.VerifiedChains, + SignedCertificateTimestamps: state.SignedCertificateTimestamps, + OCSPResponse: state.OCSPResponse, + TLSUnique: state.TLSUnique, + } +} + +func (c *realityClientConnWrapper) Upstream() any { + return c.UConn +} + +// Due to low implementation quality, the reality server intercepted half close and caused memory leaks. +// We fixed it by calling Close() directly. +func (c *realityClientConnWrapper) CloseWrite() error { + return c.Close() +} + +func (c *realityClientConnWrapper) ReaderReplaceable() bool { + return true +} + +func (c *realityClientConnWrapper) WriterReplaceable() bool { + return true +} diff --git a/common/tls/reality_server.go b/common/tls/reality_server.go new file mode 100644 index 00000000..c2e70733 --- /dev/null +++ b/common/tls/reality_server.go @@ -0,0 +1,239 @@ +//go:build with_utls + +package tls + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "fmt" + "net" + "time" + + "github.com/sagernet/sing-box/common/dialer" + 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" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" + + utls "github.com/metacubex/utls" +) + +var _ ServerConfigCompat = (*RealityServerConfig)(nil) + +type RealityServerConfig struct { + config *utls.RealityConfig +} + +func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { + var tlsConfig utls.RealityConfig + + if options.CertificateProvider != nil { + return nil, E.New("certificate_provider is unavailable in reality") + } + //nolint:staticcheck + if options.ACME != nil && len(options.ACME.Domain) > 0 { + return nil, E.New("acme is unavailable in reality") + } + tlsConfig.Time = ntp.TimeFuncFromContext(ctx) + if options.ServerName != "" { + tlsConfig.ServerName = options.ServerName + } + if len(options.ALPN) > 0 { + tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...) + } + if options.MinVersion != "" { + minVersion, err := ParseTLSVersion(options.MinVersion) + if err != nil { + return nil, E.Cause(err, "parse min_version") + } + tlsConfig.MinVersion = minVersion + } + if options.MaxVersion != "" { + maxVersion, err := ParseTLSVersion(options.MaxVersion) + if err != nil { + return nil, E.Cause(err, "parse max_version") + } + tlsConfig.MaxVersion = maxVersion + } + if options.CipherSuites != nil { + find: + for _, cipherSuite := range options.CipherSuites { + for _, tlsCipherSuite := range tls.CipherSuites() { + if cipherSuite == tlsCipherSuite.Name { + tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) + continue find + } + } + return nil, E.New("unknown cipher_suite: ", cipherSuite) + } + } + if len(options.CurvePreferences) > 0 { + return nil, E.New("curve preferences is unavailable in reality") + } + if len(options.Certificate) > 0 || options.CertificatePath != "" || len(options.ClientCertificatePublicKeySHA256) > 0 { + return nil, E.New("certificate is unavailable in reality") + } + if len(options.Key) > 0 || options.KeyPath != "" { + return nil, E.New("key is unavailable in reality") + } + + tlsConfig.SessionTicketsDisabled = true + tlsConfig.Log = func(format string, v ...any) { + if logger != nil { + logger.Trace(fmt.Sprintf(format, v...)) + } + } + tlsConfig.Type = N.NetworkTCP + tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String() + + tlsConfig.ServerNames = map[string]bool{options.ServerName: true} + privateKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PrivateKey) + if err != nil { + return nil, E.Cause(err, "decode private key") + } + if len(privateKey) != 32 { + return nil, E.New("invalid private key") + } + tlsConfig.PrivateKey = privateKey + tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference) + + tlsConfig.ShortIds = make(map[[8]byte]bool) + 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 + } + } + + handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain()) + if err != nil { + return nil, err + } + tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + + if options.ECH != nil && options.ECH.Enabled { + return nil, E.New("Reality is conflict with ECH") + } + var config ServerConfig = &RealityServerConfig{&tlsConfig} + if options.KernelTx || options.KernelRx { + if !C.IsLinux { + return nil, E.New("kTLS is only supported on Linux") + } + config = &KTlSServerConfig{ + ServerConfig: config, + logger: logger, + kernelTx: options.KernelTx, + kernelRx: options.KernelRx, + } + } + return config, nil +} + +func (c *RealityServerConfig) ServerName() string { + return c.config.ServerName +} + +func (c *RealityServerConfig) SetServerName(serverName string) { + c.config.ServerName = serverName +} + +func (c *RealityServerConfig) NextProtos() []string { + return c.config.NextProtos +} + +func (c *RealityServerConfig) SetNextProtos(nextProto []string) { + c.config.NextProtos = nextProto +} + +func (c *RealityServerConfig) STDConfig() (*tls.Config, error) { + return nil, E.New("unsupported usage for reality") +} + +func (c *RealityServerConfig) Client(conn net.Conn) (Conn, error) { + return ClientHandshake(context.Background(), conn, c) +} + +func (c *RealityServerConfig) Start() error { + return nil +} + +func (c *RealityServerConfig) Close() error { + return nil +} + +func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) { + return ServerHandshake(context.Background(), conn, c) +} + +func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) { + tlsConn, err := utls.RealityServer(ctx, conn, c.config) + if err != nil { + return nil, err + } + return &realityConnWrapper{Conn: tlsConn}, nil +} + +func (c *RealityServerConfig) Clone() Config { + return &RealityServerConfig{ + config: c.config.Clone(), + } +} + +var _ Conn = (*realityConnWrapper)(nil) + +type realityConnWrapper struct { + *utls.Conn +} + +func (c *realityConnWrapper) ConnectionState() ConnectionState { + state := c.Conn.ConnectionState() + //nolint:staticcheck + return tls.ConnectionState{ + Version: state.Version, + HandshakeComplete: state.HandshakeComplete, + DidResume: state.DidResume, + CipherSuite: state.CipherSuite, + NegotiatedProtocol: state.NegotiatedProtocol, + NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, + ServerName: state.ServerName, + PeerCertificates: state.PeerCertificates, + VerifiedChains: state.VerifiedChains, + SignedCertificateTimestamps: state.SignedCertificateTimestamps, + OCSPResponse: state.OCSPResponse, + TLSUnique: state.TLSUnique, + } +} + +func (c *realityConnWrapper) Upstream() any { + return c.Conn +} + +// Due to low implementation quality, the reality server intercepted half close and caused memory leaks. +// We fixed it by calling Close() directly. +func (c *realityConnWrapper) CloseWrite() error { + return c.Close() +} + +func (c *realityConnWrapper) ReaderReplaceable() bool { + return true +} + +func (c *realityConnWrapper) WriterReplaceable() bool { + return true +} diff --git a/common/tls/reality_stub.go b/common/tls/reality_stub.go new file mode 100644 index 00000000..0feb2aac --- /dev/null +++ b/common/tls/reality_stub.go @@ -0,0 +1,5 @@ +//go:build with_reality_server + +package tls + +var _ int = "The separate `with_reality_server` build tag has been merged into `with_utls` and is no longer needed, please update your build configuration." diff --git a/common/tls/server.go b/common/tls/server.go new file mode 100644 index 00000000..74b240fc --- /dev/null +++ b/common/tls/server.go @@ -0,0 +1,62 @@ +package tls + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/common/badtls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + aTLS "github.com/sagernet/sing/common/tls" +) + +type ServerOptions struct { + Context context.Context + Logger log.ContextLogger + Options option.InboundTLSOptions + KTLSCompatible bool +} + +func NewServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { + return NewServerWithOptions(ServerOptions{ + Context: ctx, + Logger: logger, + Options: options, + }) +} + +func NewServerWithOptions(options ServerOptions) (ServerConfig, error) { + if !options.Options.Enabled { + return nil, nil + } + if !options.KTLSCompatible { + if options.Options.KernelTx { + options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx") + } + } + if options.Options.KernelRx { + options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx") + } + if options.Options.Reality != nil && options.Options.Reality.Enabled { + return NewRealityServer(options.Context, options.Logger, options.Options) + } + return NewSTDServer(options.Context, options.Logger, options.Options) +} + +func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { + ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) + defer cancel() + tlsConn, err := aTLS.ServerHandshake(ctx, conn, config) + if err != nil { + return nil, err + } + readWaitConn, err := badtls.NewReadWaitConn(tlsConn) + if err == nil { + return readWaitConn, nil + } else if err != os.ErrInvalid { + return nil, err + } + return tlsConn, nil +} diff --git a/common/tls/std_client.go b/common/tls/std_client.go new file mode 100644 index 00000000..1611c83e --- /dev/null +++ b/common/tls/std_client.go @@ -0,0 +1,240 @@ +package tls + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "net" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tlsfragment" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/ntp" +) + +type STDClientConfig struct { + ctx context.Context + config *tls.Config + fragment bool + fragmentFallbackDelay time.Duration + recordFragment bool +} + +func (c *STDClientConfig) ServerName() string { + return c.config.ServerName +} + +func (c *STDClientConfig) SetServerName(serverName string) { + c.config.ServerName = serverName +} + +func (c *STDClientConfig) NextProtos() []string { + return c.config.NextProtos +} + +func (c *STDClientConfig) SetNextProtos(nextProto []string) { + c.config.NextProtos = nextProto +} + +func (c *STDClientConfig) STDConfig() (*STDConfig, error) { + return c.config, nil +} + +func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { + if c.recordFragment { + conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) + } + return tls.Client(conn, c.config), nil +} + +func (c *STDClientConfig) Clone() Config { + return &STDClientConfig{ + ctx: c.ctx, + config: c.config.Clone(), + fragment: c.fragment, + fragmentFallbackDelay: c.fragmentFallbackDelay, + recordFragment: c.recordFragment, + } +} + +func (c *STDClientConfig) ECHConfigList() []byte { + return c.config.EncryptedClientHelloConfigList +} + +func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) { + c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList +} + +func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + var serverName string + if options.ServerName != "" { + serverName = options.ServerName + } else if serverAddress != "" { + serverName = serverAddress + } + if serverName == "" && !options.Insecure { + return nil, E.New("missing server_name or insecure=true") + } + + var tlsConfig tls.Config + tlsConfig.Time = ntp.TimeFuncFromContext(ctx) + tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) + if !options.DisableSNI { + tlsConfig.ServerName = serverName + } + if options.Insecure { + tlsConfig.InsecureSkipVerify = options.Insecure + } else if options.DisableSNI { + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { + verifyOptions := x509.VerifyOptions{ + Roots: tlsConfig.RootCAs, + DNSName: serverName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range state.PeerCertificates[1:] { + verifyOptions.Intermediates.AddCert(cert) + } + if tlsConfig.Time != nil { + verifyOptions.CurrentTime = tlsConfig.Time() + } + _, err := state.PeerCertificates[0].Verify(verifyOptions) + return err + } + } + if len(options.CertificatePublicKeySHA256) > 0 { + if len(options.Certificate) > 0 || options.CertificatePath != "" { + return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") + } + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + } + } + if len(options.ALPN) > 0 { + tlsConfig.NextProtos = options.ALPN + } + if options.MinVersion != "" { + minVersion, err := ParseTLSVersion(options.MinVersion) + if err != nil { + return nil, E.Cause(err, "parse min_version") + } + tlsConfig.MinVersion = minVersion + } + if options.MaxVersion != "" { + maxVersion, err := ParseTLSVersion(options.MaxVersion) + if err != nil { + return nil, E.Cause(err, "parse max_version") + } + tlsConfig.MaxVersion = maxVersion + } + if options.CipherSuites != nil { + find: + for _, cipherSuite := range options.CipherSuites { + for _, tlsCipherSuite := range tls.CipherSuites() { + if cipherSuite == tlsCipherSuite.Name { + tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) + continue find + } + } + return nil, E.New("unknown cipher_suite: ", cipherSuite) + } + } + for _, curve := range options.CurvePreferences { + tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve)) + } + var certificate []byte + if len(options.Certificate) > 0 { + certificate = []byte(strings.Join(options.Certificate, "\n")) + } else if options.CertificatePath != "" { + content, err := os.ReadFile(options.CertificatePath) + if err != nil { + return nil, E.Cause(err, "read certificate") + } + certificate = content + } + if len(certificate) > 0 { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certificate) { + return nil, E.New("failed to parse certificate:\n\n", certificate) + } + tlsConfig.RootCAs = certPool + } + var clientCertificate []byte + if len(options.ClientCertificate) > 0 { + clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n")) + } else if options.ClientCertificatePath != "" { + content, err := os.ReadFile(options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "read client certificate") + } + clientCertificate = content + } + var clientKey []byte + if len(options.ClientKey) > 0 { + clientKey = []byte(strings.Join(options.ClientKey, "\n")) + } else if options.ClientKeyPath != "" { + content, err := os.ReadFile(options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "read client key") + } + clientKey = content + } + if len(clientCertificate) > 0 && len(clientKey) > 0 { + keyPair, err := tls.X509KeyPair(clientCertificate, clientKey) + if err != nil { + return nil, E.Cause(err, "parse client x509 key pair") + } + tlsConfig.Certificates = []tls.Certificate{keyPair} + } else if len(clientCertificate) > 0 || len(clientKey) > 0 { + return nil, E.New("client certificate and client key must be provided together") + } + var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} + if options.ECH != nil && options.ECH.Enabled { + var err error + config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) + if err != nil { + return nil, err + } + } + if options.KernelRx || options.KernelTx { + if !C.IsLinux { + return nil, E.New("kTLS is only supported on Linux") + } + config = &KTLSClientConfig{ + Config: config, + logger: logger, + kernelTx: options.KernelTx, + kernelRx: options.KernelRx, + } + } + return config, nil +} + +func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error { + leafCertificate, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return E.Cause(err, "failed to parse leaf certificate") + } + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(leafCertificate.PublicKey) + if err != nil { + return E.Cause(err, "failed to marshal public key") + } + hashValue := sha256.Sum256(pubKeyBytes) + for _, value := range knownHashValues { + if bytes.Equal(value, hashValue[:]) { + return nil + } + } + return E.New("unrecognized remote public key: ", base64.StdEncoding.EncodeToString(hashValue[:])) +} diff --git a/common/tls/std_server.go b/common/tls/std_server.go new file mode 100644 index 00000000..86584cd4 --- /dev/null +++ b/common/tls/std_server.go @@ -0,0 +1,528 @@ +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" +) + +var errInsecureUnused = E.New("tls: insecure unused") + +type managedCertificateProvider interface { + adapter.CertificateProvider + adapter.SimpleLifecycle +} + +type sharedCertificateProvider struct { + tag string + manager adapter.CertificateProviderManager + provider adapter.CertificateProviderService +} + +func (p *sharedCertificateProvider) Start() error { + provider, found := p.manager.Get(p.tag) + if !found { + return E.New("certificate provider not found: ", p.tag) + } + p.provider = provider + return nil +} + +func (p *sharedCertificateProvider) Close() error { + return nil +} + +func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return p.provider.GetCertificate(hello) +} + +func (p *sharedCertificateProvider) GetACMENextProtos() []string { + return getACMENextProtos(p.provider) +} + +type inlineCertificateProvider struct { + provider adapter.CertificateProviderService +} + +func (p *inlineCertificateProvider) Start() error { + for _, stage := range adapter.ListStartStages { + err := adapter.LegacyStart(p.provider, stage) + if err != nil { + return err + } + } + return nil +} + +func (p *inlineCertificateProvider) Close() error { + return p.provider.Close() +} + +func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return p.provider.GetCertificate(hello) +} + +func (p *inlineCertificateProvider) GetACMENextProtos() []string { + return getACMENextProtos(p.provider) +} + +func getACMENextProtos(provider adapter.CertificateProvider) []string { + if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME { + return acmeProvider.GetACMENextProtos() + } + return nil +} + +type STDServerConfig struct { + access sync.RWMutex + config *tls.Config + logger log.Logger + certificateProvider managedCertificateProvider + acmeService adapter.SimpleLifecycle + certificate []byte + key []byte + certificatePath string + keyPath string + clientCertificatePath []string + echKeyPath string + watcher *fswatch.Watcher +} + +func (c *STDServerConfig) ServerName() string { + c.access.RLock() + defer c.access.RUnlock() + return c.config.ServerName +} + +func (c *STDServerConfig) SetServerName(serverName string) { + c.access.Lock() + defer c.access.Unlock() + config := c.config.Clone() + config.ServerName = serverName + c.config = config +} + +func (c *STDServerConfig) NextProtos() []string { + c.access.RLock() + defer c.access.RUnlock() + if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol { + return c.config.NextProtos[1:] + } + return c.config.NextProtos +} + +func (c *STDServerConfig) SetNextProtos(nextProto []string) { + c.access.Lock() + defer c.access.Unlock() + config := c.config.Clone() + if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol { + config.NextProtos = append(c.config.NextProtos[:1], nextProto...) + } else { + config.NextProtos = nextProto + } + c.config = config +} + +func (c *STDServerConfig) hasACMEALPN() bool { + if c.acmeService != nil { + return true + } + if c.certificateProvider != nil { + if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME { + return len(acmeProvider.GetACMENextProtos()) > 0 + } + } + return false +} + +func (c *STDServerConfig) STDConfig() (*STDConfig, error) { + return c.config, nil +} + +func (c *STDServerConfig) Client(conn net.Conn) (Conn, error) { + return tls.Client(conn, c.config), nil +} + +func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) { + return tls.Server(conn, c.config), nil +} + +func (c *STDServerConfig) Clone() Config { + return &STDServerConfig{ + config: c.config.Clone(), + } +} + +func (c *STDServerConfig) Start() error { + if c.certificateProvider != nil { + err := c.certificateProvider.Start() + if err != nil { + return err + } + if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME { + nextProtos := acmeProvider.GetACMENextProtos() + if len(nextProtos) > 0 { + c.access.Lock() + config := c.config.Clone() + mergedNextProtos := append([]string{}, nextProtos...) + for _, nextProto := range config.NextProtos { + if !common.Contains(mergedNextProtos, nextProto) { + mergedNextProtos = append(mergedNextProtos, nextProto) + } + } + config.NextProtos = mergedNextProtos + c.config = config + c.access.Unlock() + } + } + } + if c.acmeService != nil { + err := c.acmeService.Start() + if err != nil { + return err + } + } + err := c.startWatcher() + if err != nil { + c.logger.Warn("create fsnotify watcher: ", err) + } + return nil +} + +func (c *STDServerConfig) startWatcher() error { + var watchPath []string + if c.certificatePath != "" { + watchPath = append(watchPath, c.certificatePath) + } + if c.keyPath != "" { + watchPath = append(watchPath, c.keyPath) + } + if c.echKeyPath != "" { + watchPath = append(watchPath, c.echKeyPath) + } + if len(c.clientCertificatePath) > 0 { + watchPath = append(watchPath, c.clientCertificatePath...) + } + if len(watchPath) == 0 { + return nil + } + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: watchPath, + Callback: func(path string) { + err := c.certificateUpdated(path) + if err != nil { + c.logger.Error(E.Cause(err, "reload certificate")) + } + }, + }) + if err != nil { + return err + } + err = watcher.Start() + if err != nil { + return err + } + c.watcher = watcher + return nil +} + +func (c *STDServerConfig) certificateUpdated(path string) error { + if path == c.certificatePath || path == c.keyPath { + if path == c.certificatePath { + certificate, err := os.ReadFile(c.certificatePath) + if err != nil { + return E.Cause(err, "reload certificate from ", c.certificatePath) + } + c.certificate = certificate + } else if path == c.keyPath { + key, err := os.ReadFile(c.keyPath) + if err != nil { + return E.Cause(err, "reload key from ", c.keyPath) + } + c.key = key + } + keyPair, err := tls.X509KeyPair(c.certificate, c.key) + if err != nil { + return E.Cause(err, "reload key pair") + } + c.access.Lock() + config := c.config.Clone() + config.Certificates = []tls.Certificate{keyPair} + c.config = config + c.access.Unlock() + c.logger.Info("reloaded TLS certificate") + } else if common.Contains(c.clientCertificatePath, path) { + clientCertificateCA := x509.NewCertPool() + var reloaded bool + for _, certPath := range c.clientCertificatePath { + content, err := os.ReadFile(certPath) + if err != nil { + c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath)) + continue + } + if !clientCertificateCA.AppendCertsFromPEM(content) { + c.logger.Error(E.New("invalid client certificate file: ", certPath)) + continue + } + reloaded = true + } + if !reloaded { + return E.New("client certificates is empty") + } + c.access.Lock() + config := c.config.Clone() + config.ClientCAs = clientCertificateCA + c.config = config + c.access.Unlock() + c.logger.Info("reloaded client certificates") + } else if path == c.echKeyPath { + echKey, err := os.ReadFile(c.echKeyPath) + if err != nil { + return E.Cause(err, "reload ECH keys from ", c.echKeyPath) + } + err = c.setECHServerConfig(echKey) + if err != nil { + return err + } + c.logger.Info("reloaded ECH keys") + } + return nil +} + +func (c *STDServerConfig) Close() error { + return common.Close(c.certificateProvider, c.acmeService, c.watcher) +} + +func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { + if !options.Enabled { + return nil, nil + } + //nolint:staticcheck + if options.CertificateProvider != nil && options.ACME != nil { + return nil, E.New("certificate_provider and acme are mutually exclusive") + } + var tlsConfig *tls.Config + var certificateProvider managedCertificateProvider + var acmeService adapter.SimpleLifecycle + var err error + if options.CertificateProvider != nil { + certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider) + if err != nil { + return nil, err + } + tlsConfig = &tls.Config{ + GetCertificate: certificateProvider.GetCertificate, + } + if options.Insecure { + return nil, errInsecureUnused + } + } else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck + deprecated.Report(ctx, deprecated.OptionInlineACME) + //nolint:staticcheck + tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME)) + if err != nil { + return nil, err + } + if options.Insecure { + return nil, errInsecureUnused + } + } else { + tlsConfig = &tls.Config{} + } + tlsConfig.Time = ntp.TimeFuncFromContext(ctx) + if options.ServerName != "" { + tlsConfig.ServerName = options.ServerName + } + if len(options.ALPN) > 0 { + tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...) + } + if options.MinVersion != "" { + minVersion, err := ParseTLSVersion(options.MinVersion) + if err != nil { + return nil, E.Cause(err, "parse min_version") + } + tlsConfig.MinVersion = minVersion + } + if options.MaxVersion != "" { + maxVersion, err := ParseTLSVersion(options.MaxVersion) + if err != nil { + return nil, E.Cause(err, "parse max_version") + } + tlsConfig.MaxVersion = maxVersion + } + if options.CipherSuites != nil { + find: + for _, cipherSuite := range options.CipherSuites { + for _, tlsCipherSuite := range tls.CipherSuites() { + if cipherSuite == tlsCipherSuite.Name { + tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) + continue find + } + } + return nil, E.New("unknown cipher_suite: ", cipherSuite) + } + } + for _, curveID := range options.CurvePreferences { + tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curveID)) + } + tlsConfig.ClientAuth = tls.ClientAuthType(options.ClientAuthentication) + var ( + certificate []byte + key []byte + ) + if certificateProvider == nil && acmeService == nil { + if len(options.Certificate) > 0 { + certificate = []byte(strings.Join(options.Certificate, "\n")) + } else if options.CertificatePath != "" { + content, err := os.ReadFile(options.CertificatePath) + if err != nil { + return nil, E.Cause(err, "read certificate") + } + certificate = content + } + if len(options.Key) > 0 { + key = []byte(strings.Join(options.Key, "\n")) + } else if options.KeyPath != "" { + content, err := os.ReadFile(options.KeyPath) + if err != nil { + return nil, E.Cause(err, "read key") + } + 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, timeFunc, info.ServerName) + } + } else { + if certificate == nil { + return nil, E.New("missing certificate") + } else if key == nil { + return nil, E.New("missing key") + } + + keyPair, err := tls.X509KeyPair(certificate, key) + if err != nil { + return nil, E.Cause(err, "parse x509 key pair") + } + tlsConfig.Certificates = []tls.Certificate{keyPair} + } + } + if len(options.ClientCertificate) > 0 || len(options.ClientCertificatePath) > 0 { + if tlsConfig.ClientAuth == tls.NoClientCert { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { + if len(options.ClientCertificate) > 0 { + clientCertificateCA := x509.NewCertPool() + if !clientCertificateCA.AppendCertsFromPEM([]byte(strings.Join(options.ClientCertificate, "\n"))) { + return nil, E.New("invalid client certificate strings") + } + tlsConfig.ClientCAs = clientCertificateCA + } else if len(options.ClientCertificatePath) > 0 { + clientCertificateCA := x509.NewCertPool() + for _, path := range options.ClientCertificatePath { + content, err := os.ReadFile(path) + if err != nil { + return nil, E.Cause(err, "read client certificate from ", path) + } + if !clientCertificateCA.AppendCertsFromPEM(content) { + return nil, E.New("invalid client certificate file: ", path) + } + } + tlsConfig.ClientCAs = clientCertificateCA + } else if len(options.ClientCertificatePublicKeySHA256) > 0 { + if tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { + tlsConfig.ClientAuth = tls.RequireAnyClientCert + } else if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven { + tlsConfig.ClientAuth = tls.RequestClientCert + } + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + } + } else { + return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication") + } + } + var echKeyPath string + if options.ECH != nil && options.ECH.Enabled { + err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath) + if err != nil { + return nil, err + } + } + serverConfig := &STDServerConfig{ + config: tlsConfig, + logger: logger, + certificateProvider: certificateProvider, + acmeService: acmeService, + certificate: certificate, + key: key, + certificatePath: options.CertificatePath, + clientCertificatePath: options.ClientCertificatePath, + keyPath: options.KeyPath, + echKeyPath: echKeyPath, + } + serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) { + serverConfig.access.RLock() + defer serverConfig.access.RUnlock() + return serverConfig.config, nil + } + var config ServerConfig = serverConfig + if options.KernelTx || options.KernelRx { + if !C.IsLinux { + return nil, E.New("kTLS is only supported on Linux") + } + config = &KTlSServerConfig{ + ServerConfig: config, + logger: logger, + kernelTx: options.KernelTx, + kernelRx: options.KernelRx, + } + } + return config, nil +} + +func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) { + if options.IsShared() { + manager := service.FromContext[adapter.CertificateProviderManager](ctx) + if manager == nil { + return nil, E.New("missing certificate provider manager in context") + } + return &sharedCertificateProvider{ + tag: options.Tag, + manager: manager, + }, nil + } + registry := service.FromContext[adapter.CertificateProviderRegistry](ctx) + if registry == nil { + return nil, E.New("missing certificate provider registry in context") + } + provider, err := registry.Create(ctx, logger, "", options.Type, options.Options) + if err != nil { + return nil, E.Cause(err, "create inline certificate provider") + } + return &inlineCertificateProvider{ + provider: provider, + }, nil +} diff --git a/common/tls/time_wrapper.go b/common/tls/time_wrapper.go new file mode 100644 index 00000000..5cbecedc --- /dev/null +++ b/common/tls/time_wrapper.go @@ -0,0 +1,25 @@ +package tls + +import ( + "time" + + "github.com/sagernet/sing/common/ntp" +) + +type TimeServiceWrapper struct { + ntp.TimeService +} + +func (w *TimeServiceWrapper) TimeFunc() func() time.Time { + return func() time.Time { + if w.TimeService != nil { + return w.TimeService.TimeFunc()() + } else { + return time.Now() + } + } +} + +func (w *TimeServiceWrapper) Upstream() any { + return w.TimeService +} diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go new file mode 100644 index 00000000..941192ba --- /dev/null +++ b/common/tls/utls_client.go @@ -0,0 +1,332 @@ +//go:build with_utls + +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "math/rand" + "net" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tlsfragment" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/ntp" + + utls "github.com/metacubex/utls" + "golang.org/x/net/http2" +) + +type UTLSClientConfig struct { + ctx context.Context + config *utls.Config + id utls.ClientHelloID + fragment bool + fragmentFallbackDelay time.Duration + recordFragment bool +} + +func (c *UTLSClientConfig) ServerName() string { + return c.config.ServerName +} + +func (c *UTLSClientConfig) SetServerName(serverName string) { + c.config.ServerName = serverName +} + +func (c *UTLSClientConfig) NextProtos() []string { + return c.config.NextProtos +} + +func (c *UTLSClientConfig) SetNextProtos(nextProto []string) { + if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS { + nextProto = append(nextProto, "http/1.1") + } + c.config.NextProtos = nextProto +} + +func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) { + return nil, E.New("unsupported usage for uTLS") +} + +func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { + if c.recordFragment { + conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) + } + return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil +} + +func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { + c.config.SessionIDGenerator = generator +} + +func (c *UTLSClientConfig) Clone() Config { + return &UTLSClientConfig{ + c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment, + } +} + +func (c *UTLSClientConfig) ECHConfigList() []byte { + return c.config.EncryptedClientHelloConfigList +} + +func (c *UTLSClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) { + c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList +} + +type utlsConnWrapper struct { + *utls.UConn +} + +func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState { + state := c.Conn.ConnectionState() + //nolint:staticcheck + return tls.ConnectionState{ + Version: state.Version, + HandshakeComplete: state.HandshakeComplete, + DidResume: state.DidResume, + CipherSuite: state.CipherSuite, + NegotiatedProtocol: state.NegotiatedProtocol, + NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, + ServerName: state.ServerName, + PeerCertificates: state.PeerCertificates, + VerifiedChains: state.VerifiedChains, + SignedCertificateTimestamps: state.SignedCertificateTimestamps, + OCSPResponse: state.OCSPResponse, + TLSUnique: state.TLSUnique, + } +} + +func (c *utlsConnWrapper) Upstream() any { + return c.UConn +} + +func (c *utlsConnWrapper) ReaderReplaceable() bool { + return true +} + +func (c *utlsConnWrapper) WriterReplaceable() bool { + return true +} + +type utlsALPNWrapper struct { + utlsConnWrapper + nextProtocols []string +} + +func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error { + if len(c.nextProtocols) > 0 { + err := c.BuildHandshakeState() + if err != nil { + return err + } + for _, extension := range c.Extensions { + if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN { + alpnExtension.AlpnProtocols = c.nextProtocols + err = c.BuildHandshakeState() + if err != nil { + return err + } + break + } + } + } + return c.UConn.HandshakeContext(ctx) +} + +func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + var serverName string + if options.ServerName != "" { + serverName = options.ServerName + } else if serverAddress != "" { + serverName = serverAddress + } + if serverName == "" && !options.Insecure { + return nil, E.New("missing server_name or insecure=true") + } + + var tlsConfig utls.Config + tlsConfig.Time = ntp.TimeFuncFromContext(ctx) + tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) + if !options.DisableSNI { + tlsConfig.ServerName = serverName + } + if options.Insecure { + tlsConfig.InsecureSkipVerify = options.Insecure + } else if options.DisableSNI { + if options.Reality != nil && options.Reality.Enabled { + return nil, E.New("disable_sni is unsupported in reality") + } + tlsConfig.InsecureServerNameToVerify = serverName + } + if len(options.CertificatePublicKeySHA256) > 0 { + if len(options.Certificate) > 0 || options.CertificatePath != "" { + return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") + } + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + } + } + if len(options.ALPN) > 0 { + tlsConfig.NextProtos = options.ALPN + } + if options.MinVersion != "" { + minVersion, err := ParseTLSVersion(options.MinVersion) + if err != nil { + return nil, E.Cause(err, "parse min_version") + } + tlsConfig.MinVersion = minVersion + } + if options.MaxVersion != "" { + maxVersion, err := ParseTLSVersion(options.MaxVersion) + if err != nil { + return nil, E.Cause(err, "parse max_version") + } + tlsConfig.MaxVersion = maxVersion + } + if options.CipherSuites != nil { + find: + for _, cipherSuite := range options.CipherSuites { + for _, tlsCipherSuite := range tls.CipherSuites() { + if cipherSuite == tlsCipherSuite.Name { + tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) + continue find + } + } + return nil, E.New("unknown cipher_suite: ", cipherSuite) + } + } + var certificate []byte + if len(options.Certificate) > 0 { + certificate = []byte(strings.Join(options.Certificate, "\n")) + } else if options.CertificatePath != "" { + content, err := os.ReadFile(options.CertificatePath) + if err != nil { + return nil, E.Cause(err, "read certificate") + } + certificate = content + } + if len(certificate) > 0 { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certificate) { + return nil, E.New("failed to parse certificate:\n\n", certificate) + } + tlsConfig.RootCAs = certPool + } + var clientCertificate []byte + if len(options.ClientCertificate) > 0 { + clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n")) + } else if options.ClientCertificatePath != "" { + content, err := os.ReadFile(options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "read client certificate") + } + clientCertificate = content + } + var clientKey []byte + if len(options.ClientKey) > 0 { + clientKey = []byte(strings.Join(options.ClientKey, "\n")) + } else if options.ClientKeyPath != "" { + content, err := os.ReadFile(options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "read client key") + } + clientKey = content + } + if len(clientCertificate) > 0 && len(clientKey) > 0 { + keyPair, err := utls.X509KeyPair(clientCertificate, clientKey) + if err != nil { + return nil, E.Cause(err, "parse client x509 key pair") + } + tlsConfig.Certificates = []utls.Certificate{keyPair} + } else if len(clientCertificate) > 0 || len(clientKey) > 0 { + return nil, E.New("client certificate and client key must be provided together") + } + id, err := uTLSClientHelloID(options.UTLS.Fingerprint) + if err != nil { + return nil, err + } + var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} + if options.ECH != nil && options.ECH.Enabled { + if options.Reality != nil && options.Reality.Enabled { + return nil, E.New("Reality is conflict with ECH") + } + config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) + if err != nil { + return nil, err + } + } + if (options.KernelRx || options.KernelTx) && !common.PtrValueOrDefault(options.Reality).Enabled { + if !C.IsLinux { + return nil, E.New("kTLS is only supported on Linux") + } + config = &KTLSClientConfig{ + Config: config, + logger: logger, + kernelTx: options.KernelTx, + kernelRx: options.KernelRx, + } + } + return config, nil +} + +var ( + randomFingerprint utls.ClientHelloID + randomizedFingerprint utls.ClientHelloID +) + +func init() { + modernFingerprints := []utls.ClientHelloID{ + utls.HelloChrome_Auto, + utls.HelloFirefox_Auto, + utls.HelloEdge_Auto, + utls.HelloSafari_Auto, + utls.HelloIOS_Auto, + } + randomFingerprint = modernFingerprints[rand.Intn(len(modernFingerprints))] + + weights := utls.DefaultWeights + weights.TLSVersMax_Set_VersionTLS13 = 1 + weights.FirstKeyShare_Set_CurveP256 = 0 + randomizedFingerprint = utls.HelloRandomized + randomizedFingerprint.Seed, _ = utls.NewPRNGSeed() + randomizedFingerprint.Weights = &weights +} + +func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { + switch name { + case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk": + fallthrough + case "chrome", "": + return utls.HelloChrome_Auto, nil + case "firefox": + return utls.HelloFirefox_Auto, nil + case "edge": + return utls.HelloEdge_Auto, nil + case "safari": + return utls.HelloSafari_Auto, nil + case "360": + return utls.Hello360_Auto, nil + case "qq": + return utls.HelloQQ_Auto, nil + case "ios": + return utls.HelloIOS_Auto, nil + case "android": + return utls.HelloAndroid_11_OkHttp, nil + case "random": + return randomFingerprint, nil + case "randomized": + return randomizedFingerprint, nil + default: + return utls.ClientHelloID{}, E.New("unknown uTLS fingerprint: ", name) + } +} diff --git a/common/tls/utls_stub.go b/common/tls/utls_stub.go new file mode 100644 index 00000000..3eddd28e --- /dev/null +++ b/common/tls/utls_stub.go @@ -0,0 +1,24 @@ +//go:build !with_utls + +package tls + +import ( + "context" + + "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" +) + +func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`) +} + +func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`) +} + +func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) { + return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`) +} diff --git a/common/tlsfragment/conn.go b/common/tlsfragment/conn.go new file mode 100644 index 00000000..040663bd --- /dev/null +++ b/common/tlsfragment/conn.go @@ -0,0 +1,146 @@ +package tf + +import ( + "bytes" + "context" + "encoding/binary" + "math/rand" + "net" + "strings" + "time" + + C "github.com/sagernet/sing-box/constant" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/publicsuffix" +) + +type Conn struct { + net.Conn + tcpConn *net.TCPConn + ctx context.Context + firstPacketWritten bool + splitPacket bool + splitRecord bool + fallbackDelay time.Duration +} + +func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord bool, fallbackDelay time.Duration) *Conn { + if fallbackDelay == 0 { + fallbackDelay = C.TLSFragmentFallbackDelay + } + tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn) + return &Conn{ + Conn: conn, + tcpConn: tcpConn, + ctx: ctx, + splitPacket: splitPacket, + splitRecord: splitRecord, + fallbackDelay: fallbackDelay, + } +} + +func (c *Conn) Write(b []byte) (n int, err error) { + if !c.firstPacketWritten { + defer func() { + c.firstPacketWritten = true + }() + serverName := IndexTLSServerName(b) + if serverName != nil { + if c.splitPacket { + if c.tcpConn != nil { + err = c.tcpConn.SetNoDelay(true) + if err != nil { + return + } + } + } + splits := strings.Split(serverName.ServerName, ".") + currentIndex := serverName.Index + if publicSuffix := publicsuffix.List.PublicSuffix(serverName.ServerName); publicSuffix != "" { + splits = splits[:len(splits)-strings.Count(serverName.ServerName, ".")] + } + if len(splits) > 1 && splits[0] == "..." { + currentIndex += len(splits[0]) + 1 + splits = splits[1:] + } + var splitIndexes []int + for i, split := range splits { + splitAt := rand.Intn(len(split)) + splitIndexes = append(splitIndexes, currentIndex+splitAt) + currentIndex += len(split) + if i != len(splits)-1 { + currentIndex++ + } + } + var buffer bytes.Buffer + for i := 0; i <= len(splitIndexes); i++ { + var payload []byte + if i == 0 { + payload = b[:splitIndexes[i]] + if c.splitRecord { + payload = payload[recordLayerHeaderLen:] + } + } else if i == len(splitIndexes) { + payload = b[splitIndexes[i-1]:] + } else { + payload = b[splitIndexes[i-1]:splitIndexes[i]] + } + if c.splitRecord { + if c.splitPacket { + buffer.Reset() + } + payloadLen := uint16(len(payload)) + buffer.Write(b[:3]) + binary.Write(&buffer, binary.BigEndian, payloadLen) + buffer.Write(payload) + if c.splitPacket { + payload = buffer.Bytes() + } + } + if c.splitPacket { + if c.tcpConn != nil && i != len(splitIndexes) { + err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) + if err != nil { + return + } + } else { + _, err = c.Conn.Write(payload) + if err != nil { + return + } + if i != len(splitIndexes) { + time.Sleep(c.fallbackDelay) + } + } + } + } + if c.splitRecord && !c.splitPacket { + _, err = c.Conn.Write(buffer.Bytes()) + if err != nil { + return + } + } + if c.tcpConn != nil { + err = c.tcpConn.SetNoDelay(false) + if err != nil { + return + } + } + return len(b), nil + } + } + return c.Conn.Write(b) +} + +func (c *Conn) ReaderReplaceable() bool { + return true +} + +func (c *Conn) WriterReplaceable() bool { + return c.firstPacketWritten +} + +func (c *Conn) Upstream() any { + return c.Conn +} diff --git a/common/tlsfragment/conn_test.go b/common/tlsfragment/conn_test.go new file mode 100644 index 00000000..b7ade873 --- /dev/null +++ b/common/tlsfragment/conn_test.go @@ -0,0 +1,42 @@ +package tf_test + +import ( + "context" + "crypto/tls" + "net" + "testing" + + tf "github.com/sagernet/sing-box/common/tlsfragment" + + "github.com/stretchr/testify/require" +) + +func TestTLSFragment(t *testing.T) { + t.Parallel() + tcpConn, err := net.Dial("tcp", "1.1.1.1:443") + require.NoError(t, err) + tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{ + ServerName: "www.cloudflare.com", + }) + require.NoError(t, tlsConn.Handshake()) +} + +func TestTLSRecordFragment(t *testing.T) { + t.Parallel() + tcpConn, err := net.Dial("tcp", "1.1.1.1:443") + require.NoError(t, err) + tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, true, 0), &tls.Config{ + ServerName: "www.cloudflare.com", + }) + require.NoError(t, tlsConn.Handshake()) +} + +func TestTLS2Fragment(t *testing.T) { + t.Parallel() + tcpConn, err := net.Dial("tcp", "1.1.1.1:443") + require.NoError(t, err) + tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, true, 0), &tls.Config{ + ServerName: "www.cloudflare.com", + }) + require.NoError(t, tlsConn.Handshake()) +} diff --git a/common/tlsfragment/index.go b/common/tlsfragment/index.go new file mode 100644 index 00000000..0d58c445 --- /dev/null +++ b/common/tlsfragment/index.go @@ -0,0 +1,133 @@ +package tf + +import ( + "encoding/binary" +) + +const ( + recordLayerHeaderLen int = 5 + handshakeHeaderLen int = 6 + randomDataLen int = 32 + sessionIDHeaderLen int = 1 + cipherSuiteHeaderLen int = 2 + compressMethodHeaderLen int = 1 + extensionsHeaderLen int = 2 + extensionHeaderLen int = 4 + sniExtensionHeaderLen int = 5 + contentType uint8 = 22 + handshakeType uint8 = 1 + sniExtensionType uint16 = 0 + sniNameDNSHostnameType uint8 = 0 + tlsVersionBitmask uint16 = 0xFFFC + tls13 uint16 = 0x0304 +) + +type MyServerName struct { + Index int + Length int + ServerName string +} + +func IndexTLSServerName(payload []byte) *MyServerName { + if len(payload) < recordLayerHeaderLen || payload[0] != contentType { + return nil + } + segmentLen := binary.BigEndian.Uint16(payload[3:5]) + if len(payload) < recordLayerHeaderLen+int(segmentLen) { + return nil + } + serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen:]) + if serverName == nil { + return nil + } + serverName.Index += recordLayerHeaderLen + return serverName +} + +func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName { + if len(handshake) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { + return nil + } + if handshake[0] != handshakeType { + return nil + } + handshakeLen := uint32(handshake[1])<<16 | uint32(handshake[2])<<8 | uint32(handshake[3]) + if len(handshake[4:]) != int(handshakeLen) { + return nil + } + tlsVersion := uint16(handshake[4])<<8 | uint16(handshake[5]) + if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { + return nil + } + sessionIDLen := handshake[38] + currentIndex := handshakeHeaderLen + randomDataLen + sessionIDHeaderLen + int(sessionIDLen) + if len(handshake) < currentIndex { + return nil + } + cipherSuites := handshake[currentIndex:] + if len(cipherSuites) < cipherSuiteHeaderLen { + return nil + } + csLen := uint16(cipherSuites[0])<<8 | uint16(cipherSuites[1]) + if len(cipherSuites) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { + return nil + } + compressMethodLen := uint16(cipherSuites[cipherSuiteHeaderLen+int(csLen)]) + currentIndex += cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen) + if len(handshake) < currentIndex { + return nil + } + serverName := indexTLSServerNameFromExtensions(handshake[currentIndex:]) + if serverName == nil { + return nil + } + serverName.Index += currentIndex + return serverName +} + +func indexTLSServerNameFromExtensions(exs []byte) *MyServerName { + if len(exs) == 0 { + return nil + } + if len(exs) < extensionsHeaderLen { + return nil + } + exsLen := uint16(exs[0])<<8 | uint16(exs[1]) + exs = exs[extensionsHeaderLen:] + if len(exs) < int(exsLen) { + return nil + } + for currentIndex := extensionsHeaderLen; len(exs) > 0; { + if len(exs) < extensionHeaderLen { + return nil + } + exType := uint16(exs[0])<<8 | uint16(exs[1]) + exLen := uint16(exs[2])<<8 | uint16(exs[3]) + if len(exs) < extensionHeaderLen+int(exLen) { + return nil + } + sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)] + + switch exType { + case sniExtensionType: + if len(sex) < sniExtensionHeaderLen { + return nil + } + sniType := sex[2] + if sniType != sniNameDNSHostnameType { + return nil + } + sniLen := uint16(sex[3])<<8 | uint16(sex[4]) + sex = sex[sniExtensionHeaderLen:] + + return &MyServerName{ + Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen, + Length: int(sniLen), + ServerName: string(sex), + } + } + exs = exs[4+exLen:] + currentIndex += 4 + int(exLen) + } + return nil +} diff --git a/common/tlsfragment/index_test.go b/common/tlsfragment/index_test.go new file mode 100644 index 00000000..5086d6c7 --- /dev/null +++ b/common/tlsfragment/index_test.go @@ -0,0 +1,20 @@ +package tf_test + +import ( + "encoding/hex" + "testing" + + "github.com/sagernet/sing-box/common/tlsfragment" + + "github.com/stretchr/testify/require" +) + +func TestIndexTLSServerName(t *testing.T) { + t.Parallel() + payload, err := hex.DecodeString("16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100") + require.NoError(t, err) + serverName := tf.IndexTLSServerName(payload) + require.NotNil(t, serverName) + require.Equal(t, serverName.ServerName, string(payload[serverName.Index:serverName.Index+serverName.Length])) + require.Equal(t, "github.com", serverName.ServerName) +} diff --git a/common/tlsfragment/wait_darwin.go b/common/tlsfragment/wait_darwin.go new file mode 100644 index 00000000..90c65ba2 --- /dev/null +++ b/common/tlsfragment/wait_darwin.go @@ -0,0 +1,93 @@ +package tf + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing/common/control" + + "golang.org/x/sys/unix" +) + +/* +const tcpMaxNotifyAck = 10 + +type tcpNotifyAckID uint32 + + type tcpNotifyAckComplete struct { + NotifyPending uint32 + NotifyCompleteCount uint32 + NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID + } + +var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{})) + + func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) { + var value tcpNotifyAckComplete + vallen := uint32(sizeOfTCPNotifyAckComplete) + err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen) + return &value, err + } + +//go:linkname getsockopt golang.org/x/sys/unix.getsockopt +func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error + + func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error { + const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212 + return control.Conn(conn, func(fd uintptr) error { + err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1) + if err != nil { + if errors.Is(err, unix.EINVAL) { + return waitAckFallback(ctx, conn, 0) + } + return err + } + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + var ackComplete *tcpNotifyAckComplete + ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT) + if err != nil { + return err + } + if ackComplete.NotifyPending == 0 { + return nil + } + time.Sleep(10 * time.Millisecond) + } + }) + } +*/ + +func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { + _, err := conn.Write(payload) + if err != nil { + return err + } + return control.Conn(conn, func(fd uintptr) error { + start := time.Now() + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE) + if err != nil { + return err + } + if unacked == 0 { + if time.Since(start) <= 20*time.Millisecond { + // under transparent proxy + time.Sleep(fallbackDelay) + } + return nil + } + time.Sleep(10 * time.Millisecond) + } + }) +} diff --git a/common/tlsfragment/wait_linux.go b/common/tlsfragment/wait_linux.go new file mode 100644 index 00000000..517d6ea5 --- /dev/null +++ b/common/tlsfragment/wait_linux.go @@ -0,0 +1,40 @@ +package tf + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing/common/control" + + "golang.org/x/sys/unix" +) + +func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { + _, err := conn.Write(payload) + if err != nil { + return err + } + return control.Conn(conn, func(fd uintptr) error { + start := time.Now() + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO) + if err != nil { + return err + } + if tcpInfo.Unacked == 0 { + if time.Since(start) <= 20*time.Millisecond { + // under transparent proxy + time.Sleep(fallbackDelay) + } + return nil + } + time.Sleep(10 * time.Millisecond) + } + }) +} diff --git a/common/tlsfragment/wait_stub.go b/common/tlsfragment/wait_stub.go new file mode 100644 index 00000000..6a1cd889 --- /dev/null +++ b/common/tlsfragment/wait_stub.go @@ -0,0 +1,18 @@ +//go:build !(linux || darwin || windows) + +package tf + +import ( + "context" + "net" + "time" +) + +func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { + _, err := conn.Write(payload) + if err != nil { + return err + } + time.Sleep(fallbackDelay) + return nil +} diff --git a/common/tlsfragment/wait_windows.go b/common/tlsfragment/wait_windows.go new file mode 100644 index 00000000..49706ca5 --- /dev/null +++ b/common/tlsfragment/wait_windows.go @@ -0,0 +1,31 @@ +package tf + +import ( + "context" + "errors" + "net" + "time" + + "github.com/sagernet/sing/common/winiphlpapi" + + "golang.org/x/sys/windows" +) + +func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { + start := time.Now() + err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload) + if err != nil { + if errors.Is(err, windows.ERROR_ACCESS_DENIED) { + if _, err := conn.Write(payload); err != nil { + return err + } + time.Sleep(fallbackDelay) + return nil + } + return err + } + if time.Since(start) <= 20*time.Millisecond { + time.Sleep(fallbackDelay) + } + return nil +} diff --git a/common/uot/router.go b/common/uot/router.go new file mode 100644 index 00000000..98c6d608 --- /dev/null +++ b/common/uot/router.go @@ -0,0 +1,86 @@ +package uot + +import ( + "context" + "net" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" +) + +var _ adapter.ConnectionRouterEx = (*Router)(nil) + +type Router struct { + router adapter.ConnectionRouterEx + logger logger.ContextLogger +} + +func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router { + return &Router{router, logger} +} + +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + switch metadata.Destination.Fqdn { + case uot.MagicAddress: + request, err := uot.ReadRequest(conn) + if err != nil { + return E.Cause(err, "read UoT request") + } + if request.IsConnect { + r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) + } else { + r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) + } + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = request.Destination + return r.router.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata) + case uot.LegacyMagicAddress: + r.logger.InfoContext(ctx, "inbound legacy UoT connection") + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} + return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) + } + return r.router.RouteConnection(ctx, conn, metadata) +} + +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return r.router.RoutePacketConnection(ctx, conn, metadata) +} + +func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + switch metadata.Destination.Fqdn { + case uot.MagicAddress: + request, err := uot.ReadRequest(conn) + if err != nil { + err = E.Cause(err, "UoT read request") + r.logger.ErrorContext(ctx, "process connection from ", metadata.Source, ": ", err) + N.CloseOnHandshakeFailure(conn, onClose, err) + return + } + if request.IsConnect { + r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) + } else { + r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) + } + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = request.Destination + r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose) + return + case uot.LegacyMagicAddress: + r.logger.InfoContext(ctx, "inbound legacy UoT connection") + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} + r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose) + return + } + r.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/common/urltest/urltest.go b/common/urltest/urltest.go new file mode 100644 index 00000000..29d790e4 --- /dev/null +++ b/common/urltest/urltest.go @@ -0,0 +1,130 @@ +package urltest + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/observable" +) + +var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil) + +type HistoryStorage struct { + access sync.RWMutex + delayHistory map[string]*adapter.URLTestHistory + updateHook *observable.Subscriber[struct{}] +} + +func NewHistoryStorage() *HistoryStorage { + return &HistoryStorage{ + delayHistory: make(map[string]*adapter.URLTestHistory), + } +} + +func (s *HistoryStorage) SetHook(hook *observable.Subscriber[struct{}]) { + s.updateHook = hook +} + +func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory { + if s == nil { + return nil + } + s.access.RLock() + defer s.access.RUnlock() + return s.delayHistory[tag] +} + +func (s *HistoryStorage) DeleteURLTestHistory(tag string) { + s.access.Lock() + delete(s.delayHistory, tag) + s.notifyUpdated() + s.access.Unlock() +} + +func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) { + s.access.Lock() + s.delayHistory[tag] = history + s.notifyUpdated() + s.access.Unlock() +} + +func (s *HistoryStorage) notifyUpdated() { + updateHook := s.updateHook + if updateHook != nil { + updateHook.Emit(struct{}{}) + } +} + +func (s *HistoryStorage) Close() error { + s.access.Lock() + defer s.access.Unlock() + s.updateHook = nil + return nil +} + +func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { + if link == "" { + link = "https://www.gstatic.com/generate_204" + } + linkURL, err := url.Parse(link) + if err != nil { + return + } + hostname := linkURL.Hostname() + port := linkURL.Port() + if port == "" { + switch linkURL.Scheme { + case "http": + port = "80" + case "https": + port = "443" + } + } + + start := time.Now() + instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port)) + if err != nil { + return + } + defer instance.Close() + if N.NeedHandshakeForWrite(instance) { + start = time.Now() + } + req, err := http.NewRequest(http.MethodHead, link, nil) + if err != nil { + return + } + client := http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return instance, nil + }, + TLSClientConfig: &tls.Config{ + Time: ntp.TimeFuncFromContext(ctx), + RootCAs: adapter.RootPoolFromContext(ctx), + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: C.TCPTimeout, + } + defer client.CloseIdleConnections() + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return + } + resp.Body.Close() + t = uint16(time.Since(start) / time.Millisecond) + return +} diff --git a/constant/certificate.go b/constant/certificate.go new file mode 100644 index 00000000..05ab1616 --- /dev/null +++ b/constant/certificate.go @@ -0,0 +1,8 @@ +package constant + +const ( + CertificateStoreSystem = "system" + CertificateStoreMozilla = "mozilla" + CertificateStoreChrome = "chrome" + CertificateStoreNone = "none" +) diff --git a/constant/cgo.go b/constant/cgo.go new file mode 100644 index 00000000..6ec4dee7 --- /dev/null +++ b/constant/cgo.go @@ -0,0 +1,5 @@ +//go:build cgo + +package constant + +const CGO_ENABLED = true diff --git a/constant/cgo_disabled.go b/constant/cgo_disabled.go new file mode 100644 index 00000000..51cacad5 --- /dev/null +++ b/constant/cgo_disabled.go @@ -0,0 +1,5 @@ +//go:build !cgo + +package constant + +const CGO_ENABLED = false diff --git a/constant/dhcp.go b/constant/dhcp.go new file mode 100644 index 00000000..bdabd06e --- /dev/null +++ b/constant/dhcp.go @@ -0,0 +1,8 @@ +package constant + +import "time" + +const ( + DHCPTTL = time.Hour + DHCPTimeout = 5 * time.Second +) diff --git a/constant/dns.go b/constant/dns.go new file mode 100644 index 00000000..c7cd0d03 --- /dev/null +++ b/constant/dns.go @@ -0,0 +1,36 @@ +package constant + +const ( + DefaultDNSTTL = 600 +) + +type DomainStrategy = uint8 + +const ( + DomainStrategyAsIS DomainStrategy = iota + DomainStrategyPreferIPv4 + DomainStrategyPreferIPv6 + DomainStrategyIPv4Only + DomainStrategyIPv6Only +) + +const ( + DNSTypeLegacy = "legacy" + DNSTypeUDP = "udp" + DNSTypeTCP = "tcp" + DNSTypeTLS = "tls" + DNSTypeHTTPS = "https" + DNSTypeQUIC = "quic" + DNSTypeHTTP3 = "h3" + DNSTypeLocal = "local" + DNSTypeHosts = "hosts" + DNSTypeFakeIP = "fakeip" + DNSTypeDHCP = "dhcp" + DNSTypeTailscale = "tailscale" +) + +const ( + DNSProviderAliDNS = "alidns" + DNSProviderCloudflare = "cloudflare" + DNSProviderACMEDNS = "acmedns" +) diff --git a/constant/err.go b/constant/err.go new file mode 100644 index 00000000..30845123 --- /dev/null +++ b/constant/err.go @@ -0,0 +1,7 @@ +package constant + +import E "github.com/sagernet/sing/common/exceptions" + +var ErrTLSRequired = E.New("TLS required") + +var ErrQUICNotIncluded = E.New(`QUIC is not included in this build, rebuild with -tags with_quic`) diff --git a/constant/goos/gengoos.go b/constant/goos/gengoos.go new file mode 100644 index 00000000..4b7c6662 --- /dev/null +++ b/constant/goos/gengoos.go @@ -0,0 +1,68 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "strconv" + "strings" +) + +var gooses []string + +func main() { + data, err := os.ReadFile("../../go/build/syslist.go") + if err != nil { + log.Fatal(err) + } + const goosPrefix = `const goosList = ` + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, goosPrefix) { + text, err := strconv.Unquote(strings.TrimPrefix(line, goosPrefix)) + if err != nil { + log.Fatalf("parsing goosList: %v", err) + } + gooses = strings.Fields(text) + } + } + + for _, target := range gooses { + if target == "nacl" { + continue + } + var tags []string + if target == "linux" { + tags = append(tags, "!android") // must explicitly exclude android for linux + } + if target == "solaris" { + tags = append(tags, "!illumos") // must explicitly exclude illumos for solaris + } + if target == "darwin" { + tags = append(tags, "!ios") // must explicitly exclude ios for darwin + } + tags = append(tags, target) // must explicitly include target for bootstrapping purposes + var buf bytes.Buffer + fmt.Fprintf(&buf, "// Code generated by gengoos.go using 'go generate'. DO NOT EDIT.\n\n") + fmt.Fprintf(&buf, "//go:build %s\n", strings.Join(tags, " && ")) + fmt.Fprintf(&buf, "package goos\n\n") + fmt.Fprintf(&buf, "const GOOS = `%s`\n\n", target) + for _, goos := range gooses { + value := 0 + if goos == target { + value = 1 + } + fmt.Fprintf(&buf, "const Is%s = %d\n", strings.Title(goos), value) + } + err := os.WriteFile("zgoos_"+target+".go", buf.Bytes(), 0o666) + if err != nil { + log.Fatal(err) + } + } +} diff --git a/constant/goos/goos.go b/constant/goos/goos.go new file mode 100644 index 00000000..ebb521fe --- /dev/null +++ b/constant/goos/goos.go @@ -0,0 +1,12 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// package goos contains GOOS-specific constants. +package goos + +// The next line makes 'go generate' write the zgoos*.go files with +// per-OS information, including constants named Is$GOOS for every +// known GOOS. The constant is 1 on the current system, 0 otherwise; +// multiplying by them is useful for defining GOOS-specific constants. +//go:generate go run gengoos.go diff --git a/constant/goos/zgoos_aix.go b/constant/goos/zgoos_aix.go new file mode 100644 index 00000000..ff861550 --- /dev/null +++ b/constant/goos/zgoos_aix.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build aix + +package goos + +const GOOS = `aix` + +const IsAix = 1 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_android.go b/constant/goos/zgoos_android.go new file mode 100644 index 00000000..e8aaa124 --- /dev/null +++ b/constant/goos/zgoos_android.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build android + +package goos + +const GOOS = `android` + +const IsAix = 0 +const IsAndroid = 1 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_darwin.go b/constant/goos/zgoos_darwin.go new file mode 100644 index 00000000..decdd496 --- /dev/null +++ b/constant/goos/zgoos_darwin.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build !ios && darwin + +package goos + +const GOOS = `darwin` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 1 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_dragonfly.go b/constant/goos/zgoos_dragonfly.go new file mode 100644 index 00000000..2224baa2 --- /dev/null +++ b/constant/goos/zgoos_dragonfly.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build dragonfly + +package goos + +const GOOS = `dragonfly` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 1 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_freebsd.go b/constant/goos/zgoos_freebsd.go new file mode 100644 index 00000000..3ee5bf99 --- /dev/null +++ b/constant/goos/zgoos_freebsd.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build freebsd + +package goos + +const GOOS = `freebsd` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 1 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_hurd.go b/constant/goos/zgoos_hurd.go new file mode 100644 index 00000000..8a3d3430 --- /dev/null +++ b/constant/goos/zgoos_hurd.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build hurd + +package goos + +const GOOS = `hurd` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 1 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_illumos.go b/constant/goos/zgoos_illumos.go new file mode 100644 index 00000000..fc1b9a9e --- /dev/null +++ b/constant/goos/zgoos_illumos.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build illumos + +package goos + +const GOOS = `illumos` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 1 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_ios.go b/constant/goos/zgoos_ios.go new file mode 100644 index 00000000..746e769e --- /dev/null +++ b/constant/goos/zgoos_ios.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build ios + +package goos + +const GOOS = `ios` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 1 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_js.go b/constant/goos/zgoos_js.go new file mode 100644 index 00000000..6cf2a5d9 --- /dev/null +++ b/constant/goos/zgoos_js.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build js + +package goos + +const GOOS = `js` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 1 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_linux.go b/constant/goos/zgoos_linux.go new file mode 100644 index 00000000..cb9d6e8a --- /dev/null +++ b/constant/goos/zgoos_linux.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build !android && linux + +package goos + +const GOOS = `linux` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 1 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_netbsd.go b/constant/goos/zgoos_netbsd.go new file mode 100644 index 00000000..8285928d --- /dev/null +++ b/constant/goos/zgoos_netbsd.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build netbsd + +package goos + +const GOOS = `netbsd` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 1 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_openbsd.go b/constant/goos/zgoos_openbsd.go new file mode 100644 index 00000000..3f739a4a --- /dev/null +++ b/constant/goos/zgoos_openbsd.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build openbsd + +package goos + +const GOOS = `openbsd` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 1 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_plan9.go b/constant/goos/zgoos_plan9.go new file mode 100644 index 00000000..d4c1c651 --- /dev/null +++ b/constant/goos/zgoos_plan9.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build plan9 + +package goos + +const GOOS = `plan9` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 1 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_solaris.go b/constant/goos/zgoos_solaris.go new file mode 100644 index 00000000..69e3285a --- /dev/null +++ b/constant/goos/zgoos_solaris.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build !illumos && solaris + +package goos + +const GOOS = `solaris` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 1 +const IsWindows = 0 +const IsZos = 0 diff --git a/constant/goos/zgoos_windows.go b/constant/goos/zgoos_windows.go new file mode 100644 index 00000000..16158be7 --- /dev/null +++ b/constant/goos/zgoos_windows.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build windows + +package goos + +const GOOS = `windows` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 1 +const IsZos = 0 diff --git a/constant/goos/zgoos_zos.go b/constant/goos/zgoos_zos.go new file mode 100644 index 00000000..fb6165c7 --- /dev/null +++ b/constant/goos/zgoos_zos.go @@ -0,0 +1,25 @@ +// Code generated by gengoos.go using 'go generate'. DO NOT EDIT. + +//go:build zos + +package goos + +const GOOS = `zos` + +const IsAix = 0 +const IsAndroid = 0 +const IsDarwin = 0 +const IsDragonfly = 0 +const IsFreebsd = 0 +const IsHurd = 0 +const IsIllumos = 0 +const IsIos = 0 +const IsJs = 0 +const IsLinux = 0 +const IsNacl = 0 +const IsNetbsd = 0 +const IsOpenbsd = 0 +const IsPlan9 = 0 +const IsSolaris = 0 +const IsWindows = 0 +const IsZos = 1 diff --git a/constant/hysteria2.go b/constant/hysteria2.go new file mode 100644 index 00000000..35c0b14f --- /dev/null +++ b/constant/hysteria2.go @@ -0,0 +1,7 @@ +package constant + +const ( + Hysterai2MasqueradeTypeFile = "file" + Hysterai2MasqueradeTypeProxy = "proxy" + Hysterai2MasqueradeTypeString = "string" +) diff --git a/constant/network.go b/constant/network.go new file mode 100644 index 00000000..88a1dd81 --- /dev/null +++ b/constant/network.go @@ -0,0 +1,58 @@ +package constant + +import ( + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" +) + +type InterfaceType uint8 + +const ( + InterfaceTypeWIFI InterfaceType = iota + InterfaceTypeCellular + InterfaceTypeEthernet + InterfaceTypeOther +) + +var ( + interfaceTypeToString = map[InterfaceType]string{ + InterfaceTypeWIFI: "wifi", + InterfaceTypeCellular: "cellular", + InterfaceTypeEthernet: "ethernet", + InterfaceTypeOther: "other", + } + StringToInterfaceType = common.ReverseMap(interfaceTypeToString) +) + +func (t InterfaceType) String() string { + name, loaded := interfaceTypeToString[t] + if !loaded { + return F.ToString(int(t)) + } + return name +} + +type NetworkStrategy uint8 + +const ( + NetworkStrategyDefault NetworkStrategy = iota + NetworkStrategyFallback + NetworkStrategyHybrid +) + +var ( + networkStrategyToString = map[NetworkStrategy]string{ + NetworkStrategyDefault: "default", + NetworkStrategyFallback: "fallback", + NetworkStrategyHybrid: "hybrid", + } + StringToNetworkStrategy = common.ReverseMap(networkStrategyToString) +) + +func (s NetworkStrategy) String() string { + name, loaded := networkStrategyToString[s] + if !loaded { + return F.ToString(int(s)) + } + return name +} diff --git a/constant/os.go b/constant/os.go new file mode 100644 index 00000000..6142767c --- /dev/null +++ b/constant/os.go @@ -0,0 +1,37 @@ +package constant + +import ( + "github.com/sagernet/sing-box/constant/goos" +) + +const IsAndroid = goos.IsAndroid == 1 + +const IsDarwin = goos.IsDarwin == 1 || goos.IsIos == 1 + +const IsDragonfly = goos.IsDragonfly == 1 + +const IsFreebsd = goos.IsFreebsd == 1 + +const IsHurd = goos.IsHurd == 1 + +const IsIllumos = goos.IsIllumos == 1 + +const IsIos = goos.IsIos == 1 + +const IsJs = goos.IsJs == 1 + +const IsLinux = goos.IsLinux == 1 || goos.IsAndroid == 1 + +const IsNacl = goos.IsNacl == 1 + +const IsNetbsd = goos.IsNetbsd == 1 + +const IsOpenbsd = goos.IsOpenbsd == 1 + +const IsPlan9 = goos.IsPlan9 == 1 + +const IsSolaris = goos.IsSolaris == 1 + +const IsWindows = goos.IsWindows == 1 + +const IsZos = goos.IsZos == 1 diff --git a/constant/path.go b/constant/path.go new file mode 100644 index 00000000..ea2aad3e --- /dev/null +++ b/constant/path.go @@ -0,0 +1,41 @@ +package constant + +import ( + "os" + "path/filepath" + + "github.com/sagernet/sing/common/rw" +) + +const dirName = "sing-box" + +var resourcePaths []string + +func FindPath(name string) (string, bool) { + name = os.ExpandEnv(name) + if rw.IsFile(name) { + return name, true + } + for _, dir := range resourcePaths { + if path := filepath.Join(dir, dirName, name); rw.IsFile(path) { + return path, true + } + if path := filepath.Join(dir, name); rw.IsFile(path) { + return path, true + } + } + return name, false +} + +func init() { + resourcePaths = append(resourcePaths, ".") + if home := os.Getenv("HOME"); home != "" { + resourcePaths = append(resourcePaths, home) + } + if userConfigDir, err := os.UserConfigDir(); err == nil { + resourcePaths = append(resourcePaths, userConfigDir) + } + if userCacheDir, err := os.UserCacheDir(); err == nil { + resourcePaths = append(resourcePaths, userCacheDir) + } +} diff --git a/constant/path_unix.go b/constant/path_unix.go new file mode 100644 index 00000000..5d9a201d --- /dev/null +++ b/constant/path_unix.go @@ -0,0 +1,17 @@ +//go:build unix || linux + +package constant + +import ( + "os" +) + +func init() { + resourcePaths = append(resourcePaths, "/etc") + resourcePaths = append(resourcePaths, "/usr/share") + resourcePaths = append(resourcePaths, "/usr/local/etc") + resourcePaths = append(resourcePaths, "/usr/local/share") + if homeDir := os.Getenv("HOME"); homeDir != "" { + resourcePaths = append(resourcePaths, homeDir+"/.local/share") + } +} diff --git a/constant/protocol.go b/constant/protocol.go new file mode 100644 index 00000000..dbe16e51 --- /dev/null +++ b/constant/protocol.go @@ -0,0 +1,22 @@ +package constant + +const ( + ProtocolTLS = "tls" + ProtocolHTTP = "http" + ProtocolQUIC = "quic" + ProtocolDNS = "dns" + ProtocolSTUN = "stun" + ProtocolBitTorrent = "bittorrent" + ProtocolDTLS = "dtls" + ProtocolSSH = "ssh" + ProtocolRDP = "rdp" + ProtocolNTP = "ntp" +) + +const ( + ClientChromium = "chromium" + ClientSafari = "safari" + ClientFirefox = "firefox" + ClientQUICGo = "quic-go" + ClientUnknown = "unknown" +) diff --git a/constant/proxy.go b/constant/proxy.go new file mode 100644 index 00000000..ffec8025 --- /dev/null +++ b/constant/proxy.go @@ -0,0 +1,103 @@ +package constant + +const ( + TypeTun = "tun" + TypeRedirect = "redirect" + TypeTProxy = "tproxy" + TypeDirect = "direct" + TypeBlock = "block" + TypeDNS = "dns" + TypeSOCKS = "socks" + TypeHTTP = "http" + TypeMixed = "mixed" + TypeShadowsocks = "shadowsocks" + TypeVMess = "vmess" + TypeTrojan = "trojan" + TypeNaive = "naive" + TypeWireGuard = "wireguard" + TypeHysteria = "hysteria" + TypeTor = "tor" + TypeSSH = "ssh" + TypeShadowTLS = "shadowtls" + TypeAnyTLS = "anytls" + TypeShadowsocksR = "shadowsocksr" + TypeVLESS = "vless" + TypeTUIC = "tuic" + TypeHysteria2 = "hysteria2" + TypeTailscale = "tailscale" + TypeCloudflared = "cloudflared" + TypeDERP = "derp" + TypeResolved = "resolved" + TypeSSMAPI = "ssm-api" + TypeCCM = "ccm" + TypeOCM = "ocm" + TypeOOMKiller = "oom-killer" + TypeACME = "acme" + TypeCloudflareOriginCA = "cloudflare-origin-ca" +) + +const ( + TypeSelector = "selector" + TypeURLTest = "urltest" +) + +func ProxyDisplayName(proxyType string) string { + switch proxyType { + case TypeTun: + return "TUN" + case TypeRedirect: + return "Redirect" + case TypeTProxy: + return "TProxy" + case TypeDirect: + return "Direct" + case TypeBlock: + return "Block" + case TypeDNS: + return "DNS" + case TypeSOCKS: + return "SOCKS" + case TypeHTTP: + return "HTTP" + case TypeMixed: + return "Mixed" + case TypeShadowsocks: + return "Shadowsocks" + case TypeVMess: + return "VMess" + case TypeTrojan: + return "Trojan" + case TypeNaive: + return "Naive" + case TypeWireGuard: + return "WireGuard" + case TypeHysteria: + return "Hysteria" + case TypeTor: + return "Tor" + case TypeSSH: + return "SSH" + case TypeShadowTLS: + return "ShadowTLS" + case TypeShadowsocksR: + return "ShadowsocksR" + case TypeVLESS: + return "VLESS" + case TypeTUIC: + return "TUIC" + case TypeHysteria2: + return "Hysteria2" + case TypeAnyTLS: + return "AnyTLS" + case TypeTailscale: + return "Tailscale" + case TypeCloudflared: + return "Cloudflared" + case TypeSelector: + return "Selector" + case TypeURLTest: + return "URLTest" + default: + return "Unknown" + } +} diff --git a/constant/quic.go b/constant/quic.go new file mode 100644 index 00000000..50bddf88 --- /dev/null +++ b/constant/quic.go @@ -0,0 +1,5 @@ +//go:build with_quic + +package constant + +const WithQUIC = true diff --git a/constant/quic_stub.go b/constant/quic_stub.go new file mode 100644 index 00000000..95b47fef --- /dev/null +++ b/constant/quic_stub.go @@ -0,0 +1,5 @@ +//go:build !with_quic + +package constant + +const WithQUIC = false diff --git a/constant/rule.go b/constant/rule.go new file mode 100644 index 00000000..efd4a2d3 --- /dev/null +++ b/constant/rule.go @@ -0,0 +1,48 @@ +package constant + +const ( + RuleTypeDefault = "default" + RuleTypeLogical = "logical" +) + +const ( + LogicalTypeAnd = "and" + LogicalTypeOr = "or" +) + +const ( + RuleSetTypeInline = "inline" + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) + +const ( + RuleSetVersion1 = 1 + iota + RuleSetVersion2 + RuleSetVersion3 + RuleSetVersion4 + RuleSetVersion5 + RuleSetVersionCurrent = RuleSetVersion5 +) + +const ( + RuleActionTypeRoute = "route" + RuleActionTypeRouteOptions = "route-options" + RuleActionTypeEvaluate = "evaluate" + RuleActionTypeRespond = "respond" + RuleActionTypeDirect = "direct" + RuleActionTypeBypass = "bypass" + RuleActionTypeReject = "reject" + RuleActionTypeHijackDNS = "hijack-dns" + RuleActionTypeSniff = "sniff" + RuleActionTypeResolve = "resolve" + RuleActionTypePredefined = "predefined" +) + +const ( + RuleActionRejectMethodDefault = "default" + RuleActionRejectMethodDrop = "drop" + RuleActionRejectMethodReply = "reply" +) diff --git a/constant/speed.go b/constant/speed.go new file mode 100644 index 00000000..7a2ec130 --- /dev/null +++ b/constant/speed.go @@ -0,0 +1,3 @@ +package constant + +const MbpsToBps = 125000 diff --git a/constant/time.go b/constant/time.go new file mode 100644 index 00000000..4249a72d --- /dev/null +++ b/constant/time.go @@ -0,0 +1,3 @@ +package constant + +const TimeLayout = "2006-01-02 15:04:05 -0700" diff --git a/constant/timeout.go b/constant/timeout.go new file mode 100644 index 00000000..e1bc7ccd --- /dev/null +++ b/constant/timeout.go @@ -0,0 +1,35 @@ +package constant + +import "time" + +const ( + TCPKeepAliveInitial = 5 * time.Minute + TCPKeepAliveInterval = 75 * time.Second + TCPConnectTimeout = 5 * time.Second + TCPTimeout = 15 * time.Second + ReadPayloadTimeout = 300 * time.Millisecond + DNSTimeout = 10 * time.Second + UDPTimeout = 5 * time.Minute + DefaultURLTestInterval = 3 * time.Minute + DefaultURLTestIdleTimeout = 30 * time.Minute + StartTimeout = 10 * time.Second + StopTimeout = 5 * time.Second + FatalStopTimeout = 10 * time.Second + FakeIPMetadataSaveInterval = 10 * time.Second + TLSFragmentFallbackDelay = 500 * time.Millisecond +) + +var PortProtocols = map[uint16]string{ + 53: ProtocolDNS, + 123: ProtocolNTP, + 3478: ProtocolSTUN, + 443: ProtocolQUIC, +} + +var ProtocolTimeouts = map[string]time.Duration{ + ProtocolDNS: 10 * time.Second, + ProtocolNTP: 10 * time.Second, + ProtocolSTUN: 10 * time.Second, + ProtocolQUIC: 30 * time.Second, + ProtocolDTLS: 30 * time.Second, +} diff --git a/constant/tls.go b/constant/tls.go new file mode 100644 index 00000000..2d4f64bc --- /dev/null +++ b/constant/tls.go @@ -0,0 +1,3 @@ +package constant + +const ACMETLS1Protocol = "acme-tls/1" diff --git a/constant/v2ray.go b/constant/v2ray.go new file mode 100644 index 00000000..c3089a6c --- /dev/null +++ b/constant/v2ray.go @@ -0,0 +1,9 @@ +package constant + +const ( + V2RayTransportTypeHTTP = "http" + V2RayTransportTypeWebsocket = "ws" + V2RayTransportTypeQUIC = "quic" + V2RayTransportTypeGRPC = "grpc" + V2RayTransportTypeHTTPUpgrade = "httpupgrade" +) diff --git a/constant/version.go b/constant/version.go new file mode 100644 index 00000000..5a816a73 --- /dev/null +++ b/constant/version.go @@ -0,0 +1,3 @@ +package constant + +var Version = "unknown" diff --git a/daemon/deprecated.go b/daemon/deprecated.go new file mode 100644 index 00000000..6f23db99 --- /dev/null +++ b/daemon/deprecated.go @@ -0,0 +1,29 @@ +package daemon + +import ( + "sync" + + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing/common" +) + +var _ deprecated.Manager = (*deprecatedManager)(nil) + +type deprecatedManager struct { + access sync.Mutex + notes []deprecated.Note +} + +func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) { + m.access.Lock() + defer m.access.Unlock() + m.notes = common.Uniq(append(m.notes, feature)) +} + +func (m *deprecatedManager) Get() []deprecated.Note { + m.access.Lock() + defer m.access.Unlock() + notes := m.notes + m.notes = nil + return notes +} diff --git a/daemon/instance.go b/daemon/instance.go new file mode 100644 index 00000000..9f950c64 --- /dev/null +++ b/daemon/instance.go @@ -0,0 +1,153 @@ +package daemon + +import ( + "bytes" + "context" + + "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +type Instance struct { + ctx context.Context + cancel context.CancelFunc + instance *box.Box + connectionManager adapter.ConnectionManager + clashServer adapter.ClashServer + cacheFile adapter.CacheFile + pauseManager pause.Manager + urlTestHistoryStorage *urltest.HistoryStorage +} + +func (s *StartedService) CheckConfig(configContent string) error { + options, err := parseConfig(s.ctx, configContent) + if err != nil { + return err + } + ctx, cancel := context.WithCancel(s.ctx) + defer cancel() + instance, err := box.New(box.Options{ + Context: ctx, + Options: options, + }) + if err == nil { + instance.Close() + } + return err +} + +func (s *StartedService) FormatConfig(configContent string) (string, error) { + options, err := parseConfig(s.ctx, configContent) + if err != nil { + return "", err + } + var buffer bytes.Buffer + encoder := json.NewEncoder(&buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(options) + if err != nil { + return "", err + } + return buffer.String(), nil +} + +type OverrideOptions struct { + AutoRedirect bool + IncludePackage []string + ExcludePackage []string +} + +func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { + ctx := s.ctx + service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) + ctx, cancel := context.WithCancel(include.Context(ctx)) + options, err := parseConfig(ctx, profileContent) + if err != nil { + cancel() + return nil, err + } + if overrideOptions != nil { + for _, inbound := range options.Inbounds { + if tunInboundOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN { + tunInboundOptions.AutoRedirect = overrideOptions.AutoRedirect + tunInboundOptions.IncludePackage = append(tunInboundOptions.IncludePackage, overrideOptions.IncludePackage...) + tunInboundOptions.ExcludePackage = append(tunInboundOptions.ExcludePackage, overrideOptions.ExcludePackage...) + break + } + } + } + if s.oomKillerEnabled { + if !common.Any(options.Services, func(it option.Service) bool { + return it.Type == C.TypeOOMKiller + }) { + oomOptions := &option.OOMKillerServiceOptions{ + KillerDisabled: s.oomKillerDisabled, + MemoryLimitOverride: s.oomMemoryLimit, + } + options.Services = append(options.Services, option.Service{ + Type: C.TypeOOMKiller, + Options: oomOptions, + }) + } + } + urlTestHistoryStorage := urltest.NewHistoryStorage() + ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) + i := &Instance{ + ctx: ctx, + cancel: cancel, + urlTestHistoryStorage: urlTestHistoryStorage, + } + boxInstance, err := box.New(box.Options{ + Context: ctx, + Options: options, + PlatformLogWriter: s, + }) + if err != nil { + cancel() + return nil, err + } + i.instance = boxInstance + i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx) + i.clashServer = service.FromContext[adapter.ClashServer](ctx) + i.pauseManager = service.FromContext[pause.Manager](ctx) + i.cacheFile = service.FromContext[adapter.CacheFile](ctx) + log.SetStdLogger(boxInstance.LogFactory().Logger()) + return i, nil +} + +func (i *Instance) Start() error { + return i.instance.Start() +} + +func (i *Instance) Close() error { + i.cancel() + i.urlTestHistoryStorage.Close() + return i.instance.Close() +} + +func (i *Instance) Box() *box.Box { + return i.instance +} + +func (i *Instance) PauseManager() pause.Manager { + return i.pauseManager +} + +func parseConfig(ctx context.Context, configContent string) (option.Options, error) { + options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent)) + if err != nil { + return option.Options{}, E.Cause(err, "decode config") + } + return options, nil +} diff --git a/daemon/platform.go b/daemon/platform.go new file mode 100644 index 00000000..ae954c57 --- /dev/null +++ b/daemon/platform.go @@ -0,0 +1,10 @@ +package daemon + +type PlatformHandler interface { + ServiceStop() error + ServiceReload() error + SystemProxyStatus() (*SystemProxyStatus, error) + SetSystemProxyEnabled(enabled bool) error + TriggerNativeCrash() error + WriteDebugMessage(message string) +} diff --git a/daemon/started_service.go b/daemon/started_service.go new file mode 100644 index 00000000..aa15c7be --- /dev/null +++ b/daemon/started_service.go @@ -0,0 +1,1489 @@ +package daemon + +import ( + "context" + "os" + "runtime" + "sync" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/common/stun" + "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/clashapi" + "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing-box/service/oomkiller" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/batch" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/memory" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + "github.com/gofrs/uuid/v5" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +var _ StartedServiceServer = (*StartedService)(nil) + +type StartedService struct { + ctx context.Context + // platform adapter.PlatformInterface + handler PlatformHandler + debug bool + logMaxLines int + oomKillerEnabled bool + oomKillerDisabled bool + oomMemoryLimit uint64 + // workingDirectory string + // tempDirectory string + // userID int + // groupID int + // systemProxyEnabled bool + serviceAccess sync.RWMutex + serviceStatus *ServiceStatus + serviceStatusSubscriber *observable.Subscriber[*ServiceStatus] + serviceStatusObserver *observable.Observer[*ServiceStatus] + logAccess sync.RWMutex + logLines list.List[*log.Entry] + logSubscriber *observable.Subscriber[*log.Entry] + logObserver *observable.Observer[*log.Entry] + instance *Instance + startedAt time.Time + urlTestSubscriber *observable.Subscriber[struct{}] + urlTestObserver *observable.Observer[struct{}] + urlTestHistoryStorage *urltest.HistoryStorage + clashModeSubscriber *observable.Subscriber[struct{}] + clashModeObserver *observable.Observer[struct{}] + + connectionEventSubscriber *observable.Subscriber[trafficontrol.ConnectionEvent] + connectionEventObserver *observable.Observer[trafficontrol.ConnectionEvent] +} + +type ServiceOptions struct { + Context context.Context + // Platform adapter.PlatformInterface + Handler PlatformHandler + Debug bool + LogMaxLines int + OOMKillerEnabled bool + OOMKillerDisabled bool + OOMMemoryLimit uint64 + // WorkingDirectory string + // TempDirectory string + // UserID int + // GroupID int + // SystemProxyEnabled bool +} + +func NewStartedService(options ServiceOptions) *StartedService { + s := &StartedService{ + ctx: options.Context, + // platform: options.Platform, + handler: options.Handler, + debug: options.Debug, + logMaxLines: options.LogMaxLines, + oomKillerEnabled: options.OOMKillerEnabled, + oomKillerDisabled: options.OOMKillerDisabled, + oomMemoryLimit: options.OOMMemoryLimit, + // workingDirectory: options.WorkingDirectory, + // tempDirectory: options.TempDirectory, + // userID: options.UserID, + // groupID: options.GroupID, + // systemProxyEnabled: options.SystemProxyEnabled, + serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE}, + serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4), + logSubscriber: observable.NewSubscriber[*log.Entry](128), + urlTestSubscriber: observable.NewSubscriber[struct{}](1), + urlTestHistoryStorage: urltest.NewHistoryStorage(), + clashModeSubscriber: observable.NewSubscriber[struct{}](1), + connectionEventSubscriber: observable.NewSubscriber[trafficontrol.ConnectionEvent](256), + } + s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2) + s.logObserver = observable.NewObserver(s.logSubscriber, 64) + s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1) + s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1) + s.connectionEventObserver = observable.NewObserver(s.connectionEventSubscriber, 64) + return s +} + +func (s *StartedService) resetLogs() { + s.logAccess.Lock() + s.logLines = list.List[*log.Entry]{} + s.logAccess.Unlock() + s.logSubscriber.Emit(nil) +} + +func (s *StartedService) updateStatus(newStatus ServiceStatus_Type) { + statusObject := &ServiceStatus{Status: newStatus} + s.serviceStatusSubscriber.Emit(statusObject) + s.serviceStatus = statusObject +} + +func (s *StartedService) updateStatusError(err error) error { + statusObject := &ServiceStatus{Status: ServiceStatus_FATAL, ErrorMessage: err.Error()} + s.serviceStatusSubscriber.Emit(statusObject) + s.serviceStatus = statusObject + s.serviceAccess.Unlock() + return err +} + +func (s *StartedService) waitForStarted(ctx context.Context) error { + s.serviceAccess.RLock() + currentStatus := s.serviceStatus.Status + s.serviceAccess.RUnlock() + + switch currentStatus { + case ServiceStatus_STARTED: + return nil + case ServiceStatus_STARTING: + default: + return os.ErrInvalid + } + + subscription, done, err := s.serviceStatusObserver.Subscribe() + if err != nil { + return err + } + defer s.serviceStatusObserver.UnSubscribe(subscription) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-s.ctx.Done(): + return s.ctx.Err() + case status := <-subscription: + switch status.Status { + case ServiceStatus_STARTED: + return nil + case ServiceStatus_FATAL: + return E.New(status.ErrorMessage) + case ServiceStatus_IDLE, ServiceStatus_STOPPING: + return os.ErrInvalid + } + case <-done: + return os.ErrClosed + } + } +} + +func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error { + s.serviceAccess.Lock() + switch s.serviceStatus.Status { + case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL: + default: + s.serviceAccess.Unlock() + return os.ErrInvalid + } + oldInstance := s.instance + if oldInstance != nil { + s.updateStatus(ServiceStatus_STOPPING) + s.serviceAccess.Unlock() + _ = oldInstance.Close() + s.serviceAccess.Lock() + } + s.updateStatus(ServiceStatus_STARTING) + s.resetLogs() + instance, err := s.newInstance(profileContent, options) + if err != nil { + return s.updateStatusError(err) + } + s.instance = instance + instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber) + if instance.clashServer != nil { + instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber) + instance.clashServer.(*clashapi.Server).TrafficManager().SetEventHook(s.connectionEventSubscriber) + } + s.serviceAccess.Unlock() + err = instance.Start() + s.serviceAccess.Lock() + if s.serviceStatus.Status != ServiceStatus_STARTING { + s.serviceAccess.Unlock() + return nil + } + if err != nil { + return s.updateStatusError(err) + } + s.startedAt = time.Now() + s.updateStatus(ServiceStatus_STARTED) + s.serviceAccess.Unlock() + runtime.GC() + return nil +} + +func (s *StartedService) Close() { + s.serviceStatusSubscriber.Close() + s.logSubscriber.Close() + s.urlTestSubscriber.Close() + s.clashModeSubscriber.Close() + s.connectionEventSubscriber.Close() +} + +func (s *StartedService) CloseService() error { + s.serviceAccess.Lock() + switch s.serviceStatus.Status { + case ServiceStatus_STARTING, ServiceStatus_STARTED: + default: + s.serviceAccess.Unlock() + return os.ErrInvalid + } + s.updateStatus(ServiceStatus_STOPPING) + instance := s.instance + s.instance = nil + if instance != nil { + err := instance.Close() + if err != nil { + return s.updateStatusError(err) + } + } + s.startedAt = time.Time{} + s.updateStatus(ServiceStatus_IDLE) + s.serviceAccess.Unlock() + runtime.GC() + return nil +} + +func (s *StartedService) SetError(err error) { + s.serviceAccess.Lock() + s.updateStatusError(err) + s.WriteMessage(log.LevelError, err.Error()) +} + +func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + err := s.handler.ServiceStop() + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + err := s.handler.ServiceReload() + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) SubscribeServiceStatus(empty *emptypb.Empty, server grpc.ServerStreamingServer[ServiceStatus]) error { + subscription, done, err := s.serviceStatusObserver.Subscribe() + if err != nil { + return err + } + defer s.serviceStatusObserver.UnSubscribe(subscription) + err = server.Send(s.serviceStatus) + if err != nil { + return err + } + for { + select { + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case newStatus := <-subscription: + err = server.Send(newStatus) + if err != nil { + return err + } + case <-done: + return nil + } + } +} + +func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerStreamingServer[Log]) error { + var savedLines []*log.Entry + s.logAccess.Lock() + savedLines = make([]*log.Entry, 0, s.logLines.Len()) + for element := s.logLines.Front(); element != nil; element = element.Next() { + savedLines = append(savedLines, element.Value) + } + subscription, done, err := s.logObserver.Subscribe() + s.logAccess.Unlock() + if err != nil { + return err + } + defer s.logObserver.UnSubscribe(subscription) + err = server.Send(&Log{ + Messages: common.Map(savedLines, func(it *log.Entry) *Log_Message { + return &Log_Message{ + Level: LogLevel(it.Level), + Message: it.Message, + } + }), + Reset_: true, + }) + if err != nil { + return err + } + for { + select { + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case message := <-subscription: + var rawMessage Log + if message == nil { + rawMessage.Reset_ = true + } else { + rawMessage.Messages = append(rawMessage.Messages, &Log_Message{ + Level: LogLevel(message.Level), + Message: message.Message, + }) + } + fetch: + for { + select { + case message = <-subscription: + if message == nil { + rawMessage.Messages = nil + rawMessage.Reset_ = true + } else { + rawMessage.Messages = append(rawMessage.Messages, &Log_Message{ + Level: LogLevel(message.Level), + Message: message.Message, + }) + } + default: + break fetch + } + } + err = server.Send(&rawMessage) + if err != nil { + return err + } + case <-done: + return nil + } + } +} + +func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.Empty) (*DefaultLogLevel, error) { + s.serviceAccess.RLock() + switch s.serviceStatus.Status { + case ServiceStatus_STARTING, ServiceStatus_STARTED: + default: + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + logLevel := s.instance.instance.LogFactory().Level() + s.serviceAccess.RUnlock() + return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil +} + +func (s *StartedService) ClearLogs(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + s.resetLogs() + return &emptypb.Empty{}, nil +} + +func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error { + interval := time.Duration(request.Interval) + if interval <= 0 { + interval = time.Second // Default to 1 second + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + status := s.readStatus() + uploadTotal := status.UplinkTotal + downloadTotal := status.DownlinkTotal + for { + err := server.Send(status) + if err != nil { + return err + } + select { + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case <-ticker.C: + } + status = s.readStatus() + upload := status.UplinkTotal - uploadTotal + download := status.DownlinkTotal - downloadTotal + uploadTotal = status.UplinkTotal + downloadTotal = status.DownlinkTotal + status.Uplink = upload + status.Downlink = download + } +} + +func (s *StartedService) readStatus() *Status { + var status Status + status.Memory = memory.Total() + status.Goroutines = int32(runtime.NumGoroutine()) + s.serviceAccess.RLock() + nowService := s.instance + s.serviceAccess.RUnlock() + if nowService != nil && nowService.connectionManager != nil { + status.ConnectionsOut = int32(nowService.connectionManager.Count()) + } + if nowService != nil { + if clashServer := nowService.clashServer; clashServer != nil { + status.TrafficAvailable = true + trafficManager := clashServer.(*clashapi.Server).TrafficManager() + status.UplinkTotal, status.DownlinkTotal = trafficManager.Total() + status.ConnectionsIn = int32(trafficManager.ConnectionsLen()) + } + } + return &status +} + +func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + subscription, done, err := s.urlTestObserver.Subscribe() + if err != nil { + return err + } + defer s.urlTestObserver.UnSubscribe(subscription) + for { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return os.ErrInvalid + } + groups := s.readGroups() + s.serviceAccess.RUnlock() + err = server.Send(groups) + if err != nil { + return err + } + select { + case <-subscription: + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case <-done: + return nil + } + } +} + +func (s *StartedService) readGroups() *Groups { + historyStorage := s.instance.urlTestHistoryStorage + boxService := s.instance + outbounds := boxService.instance.Outbound().Outbounds() + var iGroups []adapter.OutboundGroup + for _, it := range outbounds { + if group, isGroup := it.(adapter.OutboundGroup); isGroup { + iGroups = append(iGroups, group) + } + } + var gs Groups + for _, iGroup := range iGroups { + var g Group + g.Tag = iGroup.Tag() + g.Type = iGroup.Type() + _, g.Selectable = iGroup.(*group.Selector) + g.Selected = iGroup.Now() + if boxService.cacheFile != nil { + if isExpand, loaded := boxService.cacheFile.LoadGroupExpand(g.Tag); loaded { + g.IsExpand = isExpand + } + } + + for _, itemTag := range iGroup.All() { + itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag) + if !isLoaded { + continue + } + + var item GroupItem + item.Tag = itemTag + item.Type = itemOutbound.Type() + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil { + item.UrlTestTime = history.Time.Unix() + item.UrlTestDelay = int32(history.Delay) + } + g.Items = append(g.Items, &item) + } + if len(g.Items) < 2 { + continue + } + gs.Group = append(gs.Group, &g) + } + return &gs +} + +func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.Empty) (*ClashModeStatus, error) { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + clashServer := s.instance.clashServer + s.serviceAccess.RUnlock() + if clashServer == nil { + return nil, os.ErrInvalid + } + return &ClashModeStatus{ + ModeList: clashServer.ModeList(), + CurrentMode: clashServer.Mode(), + }, nil +} + +func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + subscription, done, err := s.clashModeObserver.Subscribe() + if err != nil { + return err + } + defer s.clashModeObserver.UnSubscribe(subscription) + for { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return os.ErrInvalid + } + message := &ClashMode{Mode: s.instance.clashServer.Mode()} + s.serviceAccess.RUnlock() + err = server.Send(message) + if err != nil { + return err + } + select { + case <-subscription: + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case <-done: + return nil + } + } +} + +func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (*emptypb.Empty, error) { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + clashServer := s.instance.clashServer + s.serviceAccess.RUnlock() + clashServer.(*clashapi.Server).SetMode(request.Mode) + return &emptypb.Empty{}, nil +} + +func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (*emptypb.Empty, error) { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + groupTag := request.OutboundTag + abstractOutboundGroup, isLoaded := boxService.instance.Outbound().Outbound(groupTag) + if !isLoaded { + return nil, E.New("outbound group not found: ", groupTag) + } + outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup) + if !isOutboundGroup { + return nil, E.New("outbound is not a group: ", groupTag) + } + urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest) + if isURLTest { + go urlTest.CheckOutbounds() + } else { + historyStorage := boxService.urlTestHistoryStorage + + outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { + itOutbound, _ := boxService.instance.Outbound().Outbound(it) + return itOutbound + }), func(it adapter.Outbound) bool { + if it == nil { + return false + } + _, isGroup := it.(adapter.OutboundGroup) + if isGroup { + return false + } + return true + }) + b, _ := batch.New(boxService.ctx, batch.WithConcurrencyNum[any](10)) + for _, detour := range outbounds { + outboundToTest := detour + outboundTag := outboundToTest.Tag() + b.Go(outboundTag, func() (any, error) { + t, err := urltest.URLTest(boxService.ctx, "", outboundToTest) + if err != nil { + historyStorage.DeleteURLTestHistory(outboundTag) + } else { + historyStorage.StoreURLTestHistory(outboundTag, &adapter.URLTestHistory{ + Time: time.Now(), + Delay: t, + }) + } + return nil, nil + }) + } + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutboundRequest) (*emptypb.Empty, error) { + s.serviceAccess.RLock() + switch s.serviceStatus.Status { + case ServiceStatus_STARTING, ServiceStatus_STARTED: + default: + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + boxService := s.instance.instance + s.serviceAccess.RUnlock() + outboundGroup, isLoaded := boxService.Outbound().Outbound(request.GroupTag) + if !isLoaded { + return nil, E.New("selector not found: ", request.GroupTag) + } + selector, isSelector := outboundGroup.(*group.Selector) + if !isSelector { + return nil, E.New("outbound is not a selector: ", request.GroupTag) + } + if !selector.SelectOutbound(request.OutboundTag) { + return nil, E.New("outbound not found in selector: ", request.OutboundTag) + } + s.urlTestObserver.Emit(struct{}{}) + return &emptypb.Empty{}, nil +} + +func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupExpandRequest) (*emptypb.Empty, error) { + s.serviceAccess.RLock() + switch s.serviceStatus.Status { + case ServiceStatus_STARTING, ServiceStatus_STARTED: + default: + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + if boxService.cacheFile != nil { + err := boxService.cacheFile.StoreGroupExpand(request.GroupTag, request.IsExpand) + if err != nil { + return nil, err + } + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) { + return s.handler.SystemProxyStatus() +} + +func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { + err := s.handler.SetSystemProxyEnabled(request.Enabled) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) { + if !s.debug { + return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable") + } + if request == nil { + return nil, status.Error(codes.InvalidArgument, "missing debug crash request") + } + switch request.Type { + case DebugCrashRequest_GO: + time.AfterFunc(200*time.Millisecond, func() { + *(*int)(unsafe.Pointer(uintptr(0))) = 0 + }) + case DebugCrashRequest_NATIVE: + err := s.handler.TriggerNativeCrash() + if err != nil { + return nil, err + } + default: + return nil, status.Error(codes.InvalidArgument, "unknown debug crash type") + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { + instance := s.Instance() + if instance == nil { + return nil, status.Error(codes.FailedPrecondition, "service not started") + } + reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx) + if reporter == nil { + return nil, status.Error(codes.Unavailable, "OOM reporter not available") + } + return &emptypb.Empty{}, reporter.WriteReport(memory.Total()) +} + +func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + if boxService.clashServer == nil { + return E.New("clash server not available") + } + + trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager() + + subscription, done, err := s.connectionEventObserver.Subscribe() + if err != nil { + return err + } + defer s.connectionEventObserver.UnSubscribe(subscription) + + connectionSnapshots := make(map[uuid.UUID]connectionSnapshot) + initialEvents := s.buildInitialConnectionState(trafficManager, connectionSnapshots) + err = server.Send(&ConnectionEvents{ + Events: initialEvents, + Reset_: true, + }) + if err != nil { + return err + } + + interval := time.Duration(request.Interval) + if interval <= 0 { + interval = time.Second + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case <-done: + return nil + + case event := <-subscription: + var pendingEvents []*ConnectionEvent + if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil { + pendingEvents = append(pendingEvents, protoEvent) + } + drain: + for { + select { + case event = <-subscription: + if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil { + pendingEvents = append(pendingEvents, protoEvent) + } + default: + break drain + } + } + if len(pendingEvents) > 0 { + err = server.Send(&ConnectionEvents{Events: pendingEvents}) + if err != nil { + return err + } + } + + case <-ticker.C: + protoEvents := s.buildTrafficUpdates(trafficManager, connectionSnapshots) + if len(protoEvents) == 0 { + continue + } + err = server.Send(&ConnectionEvents{Events: protoEvents}) + if err != nil { + return err + } + } + } +} + +type connectionSnapshot struct { + uplink int64 + downlink int64 + hadTraffic bool +} + +func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent { + var events []*ConnectionEvent + + for _, metadata := range manager.Connections() { + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_NEW, + Id: metadata.ID.String(), + Connection: buildConnectionProto(metadata), + }) + snapshots[metadata.ID] = connectionSnapshot{ + uplink: metadata.Upload.Load(), + downlink: metadata.Download.Load(), + } + } + + for _, metadata := range manager.ClosedConnections() { + conn := buildConnectionProto(metadata) + conn.ClosedAt = metadata.ClosedAt.UnixMilli() + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_NEW, + Id: metadata.ID.String(), + Connection: conn, + }) + } + + return events +} + +func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent { + switch event.Type { + case trafficontrol.ConnectionEventNew: + if _, exists := snapshots[event.ID]; exists { + return nil + } + snapshots[event.ID] = connectionSnapshot{ + uplink: event.Metadata.Upload.Load(), + downlink: event.Metadata.Download.Load(), + } + return &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_NEW, + Id: event.ID.String(), + Connection: buildConnectionProto(event.Metadata), + } + case trafficontrol.ConnectionEventClosed: + delete(snapshots, event.ID) + protoEvent := &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_CLOSED, + Id: event.ID.String(), + } + closedAt := event.ClosedAt + if closedAt.IsZero() && !event.Metadata.ClosedAt.IsZero() { + closedAt = event.Metadata.ClosedAt + } + if closedAt.IsZero() { + closedAt = time.Now() + } + protoEvent.ClosedAt = closedAt.UnixMilli() + if event.Metadata.ID != uuid.Nil { + conn := buildConnectionProto(event.Metadata) + conn.ClosedAt = protoEvent.ClosedAt + protoEvent.Connection = conn + } + return protoEvent + default: + return nil + } +} + +func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent { + activeConnections := manager.Connections() + activeIndex := make(map[uuid.UUID]*trafficontrol.TrackerMetadata, len(activeConnections)) + var events []*ConnectionEvent + + for _, metadata := range activeConnections { + activeIndex[metadata.ID] = metadata + currentUpload := metadata.Upload.Load() + currentDownload := metadata.Download.Load() + snapshot, exists := snapshots[metadata.ID] + if !exists { + snapshots[metadata.ID] = connectionSnapshot{ + uplink: currentUpload, + downlink: currentDownload, + } + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_NEW, + Id: metadata.ID.String(), + Connection: buildConnectionProto(metadata), + }) + continue + } + uplinkDelta := currentUpload - snapshot.uplink + downlinkDelta := currentDownload - snapshot.downlink + if uplinkDelta < 0 || downlinkDelta < 0 { + if snapshot.hadTraffic { + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_UPDATE, + Id: metadata.ID.String(), + UplinkDelta: 0, + DownlinkDelta: 0, + }) + } + snapshot.uplink = currentUpload + snapshot.downlink = currentDownload + snapshot.hadTraffic = false + snapshots[metadata.ID] = snapshot + continue + } + if uplinkDelta > 0 || downlinkDelta > 0 { + snapshot.uplink = currentUpload + snapshot.downlink = currentDownload + snapshot.hadTraffic = true + snapshots[metadata.ID] = snapshot + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_UPDATE, + Id: metadata.ID.String(), + UplinkDelta: uplinkDelta, + DownlinkDelta: downlinkDelta, + }) + continue + } + if snapshot.hadTraffic { + snapshot.uplink = currentUpload + snapshot.downlink = currentDownload + snapshot.hadTraffic = false + snapshots[metadata.ID] = snapshot + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_UPDATE, + Id: metadata.ID.String(), + UplinkDelta: 0, + DownlinkDelta: 0, + }) + } + } + + var closedIndex map[uuid.UUID]*trafficontrol.TrackerMetadata + for id := range snapshots { + if _, exists := activeIndex[id]; exists { + continue + } + if closedIndex == nil { + closedIndex = make(map[uuid.UUID]*trafficontrol.TrackerMetadata) + for _, metadata := range manager.ClosedConnections() { + closedIndex[metadata.ID] = metadata + } + } + closedAt := time.Now() + var conn *Connection + if metadata, ok := closedIndex[id]; ok { + if !metadata.ClosedAt.IsZero() { + closedAt = metadata.ClosedAt + } + conn = buildConnectionProto(metadata) + conn.ClosedAt = closedAt.UnixMilli() + } + events = append(events, &ConnectionEvent{ + Type: ConnectionEventType_CONNECTION_EVENT_CLOSED, + Id: id.String(), + ClosedAt: closedAt.UnixMilli(), + Connection: conn, + }) + delete(snapshots, id) + } + + return events +} + +func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection { + var rule string + if metadata.Rule != nil { + rule = metadata.Rule.String() + } + uplinkTotal := metadata.Upload.Load() + downlinkTotal := metadata.Download.Load() + var processInfo *ProcessInfo + if metadata.Metadata.ProcessInfo != nil { + processInfo = &ProcessInfo{ + ProcessId: metadata.Metadata.ProcessInfo.ProcessID, + UserId: metadata.Metadata.ProcessInfo.UserId, + UserName: metadata.Metadata.ProcessInfo.UserName, + ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath, + PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames, + } + } + return &Connection{ + Id: metadata.ID.String(), + Inbound: metadata.Metadata.Inbound, + InboundType: metadata.Metadata.InboundType, + IpVersion: int32(metadata.Metadata.IPVersion), + Network: metadata.Metadata.Network, + Source: metadata.Metadata.Source.String(), + Destination: metadata.Metadata.Destination.String(), + Domain: metadata.Metadata.Domain, + Protocol: metadata.Metadata.Protocol, + User: metadata.Metadata.User, + FromOutbound: metadata.Metadata.Outbound, + CreatedAt: metadata.CreatedAt.UnixMilli(), + UplinkTotal: uplinkTotal, + DownlinkTotal: downlinkTotal, + Rule: rule, + Outbound: metadata.Outbound, + OutboundType: metadata.OutboundType, + ChainList: metadata.Chain, + ProcessInfo: processInfo, + } +} + +func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) { + s.serviceAccess.RLock() + switch s.serviceStatus.Status { + case ServiceStatus_STARTING, ServiceStatus_STARTED: + default: + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + targetConn := boxService.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(request.Id)) + if targetConn != nil { + targetConn.Close() + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + s.serviceAccess.RLock() + nowService := s.instance + s.serviceAccess.RUnlock() + if nowService != nil && nowService.connectionManager != nil { + nowService.connectionManager.CloseAll() + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *emptypb.Empty) (*DeprecatedWarnings, error) { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + notes := service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get() + return &DeprecatedWarnings{ + Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning { + return &DeprecatedWarning{ + Message: it.Message(), + Impending: it.Impending(), + MigrationLink: it.MigrationLink, + Description: it.Description, + DeprecatedVersion: it.DeprecatedVersion, + ScheduledVersion: it.ScheduledVersion, + } + }), + }, nil +} + +func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty) (*StartedAt, error) { + s.serviceAccess.RLock() + defer s.serviceAccess.RUnlock() + return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil +} + +func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + subscription, done, err := s.urlTestObserver.Subscribe() + if err != nil { + return err + } + defer s.urlTestObserver.UnSubscribe(subscription) + for { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + historyStorage := boxService.urlTestHistoryStorage + var list OutboundList + for _, ob := range boxService.instance.Outbound().Outbounds() { + item := &GroupItem{ + Tag: ob.Tag(), + Type: ob.Type(), + } + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil { + item.UrlTestTime = history.Time.Unix() + item.UrlTestDelay = int32(history.Delay) + } + list.Outbounds = append(list.Outbounds, item) + } + for _, ep := range boxService.instance.Endpoint().Endpoints() { + item := &GroupItem{ + Tag: ep.Tag(), + Type: ep.Type(), + } + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil { + item.UrlTestTime = history.Time.Unix() + item.UrlTestDelay = int32(history.Delay) + } + list.Outbounds = append(list.Outbounds, item) + } + err = server.Send(&list) + if err != nil { + return err + } + select { + case <-subscription: + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case <-done: + return nil + } + } +} + +func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) { + if tag == "" { + return instance.instance.Outbound().Default(), nil + } + outbound, loaded := instance.instance.Outbound().Outbound(tag) + if !loaded { + return nil, E.New("outbound not found: ", tag) + } + return outbound, nil +} + +func (s *StartedService) StartNetworkQualityTest( + request *NetworkQualityTestRequest, + server grpc.ServerStreamingServer[NetworkQualityTestProgress], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + outbound, err := resolveOutbound(boxService, request.OutboundTag) + if err != nil { + return err + } + + resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0) + httpClient := networkquality.NewHTTPClient(resolvedDialer) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3) + if err != nil { + return err + } + + result, nqErr := networkquality.Run(networkquality.Options{ + ConfigURL: request.ConfigURL, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: request.Serial, + MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second, + Context: server.Context(), + OnProgress: func(p networkquality.Progress) { + _ = server.Send(&NetworkQualityTestProgress{ + Phase: int32(p.Phase), + DownloadCapacity: p.DownloadCapacity, + UploadCapacity: p.UploadCapacity, + DownloadRPM: p.DownloadRPM, + UploadRPM: p.UploadRPM, + IdleLatencyMs: p.IdleLatencyMs, + ElapsedMs: p.ElapsedMs, + DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(p.UploadRPMAccuracy), + }) + }, + }) + if nqErr != nil { + return server.Send(&NetworkQualityTestProgress{ + IsFinal: true, + Error: nqErr.Error(), + }) + } + return server.Send(&NetworkQualityTestProgress{ + Phase: int32(networkquality.PhaseDone), + DownloadCapacity: result.DownloadCapacity, + UploadCapacity: result.UploadCapacity, + DownloadRPM: result.DownloadRPM, + UploadRPM: result.UploadRPM, + IdleLatencyMs: result.IdleLatencyMs, + IsFinal: true, + DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(result.UploadRPMAccuracy), + }) +} + +func (s *StartedService) StartSTUNTest( + request *STUNTestRequest, + server grpc.ServerStreamingServer[STUNTestProgress], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + outbound, err := resolveOutbound(boxService, request.OutboundTag) + if err != nil { + return err + } + + resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0) + + result, stunErr := stun.Run(stun.Options{ + Server: request.Server, + Dialer: resolvedDialer, + Context: server.Context(), + OnProgress: func(p stun.Progress) { + _ = server.Send(&STUNTestProgress{ + Phase: int32(p.Phase), + ExternalAddr: p.ExternalAddr, + LatencyMs: p.LatencyMs, + NatMapping: int32(p.NATMapping), + NatFiltering: int32(p.NATFiltering), + }) + }, + }) + if stunErr != nil { + return server.Send(&STUNTestProgress{ + IsFinal: true, + Error: stunErr.Error(), + }) + } + return server.Send(&STUNTestProgress{ + Phase: int32(stun.PhaseDone), + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NatMapping: int32(result.NATMapping), + NatFiltering: int32(result.NATFiltering), + IsFinal: true, + NatTypeSupported: result.NATTypeSupported, + }) +} + +func (s *StartedService) SubscribeTailscaleStatus( + _ *emptypb.Empty, + server grpc.ServerStreamingServer[TailscaleStatusUpdate], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + if endpointManager == nil { + return status.Error(codes.FailedPrecondition, "endpoint manager not available") + } + + type tailscaleEndpoint struct { + tag string + provider adapter.TailscaleEndpoint + } + var endpoints []tailscaleEndpoint + for _, endpoint := range endpointManager.Endpoints() { + if endpoint.Type() != C.TypeTailscale { + continue + } + provider, loaded := endpoint.(adapter.TailscaleEndpoint) + if !loaded { + continue + } + endpoints = append(endpoints, tailscaleEndpoint{ + tag: endpoint.Tag(), + provider: provider, + }) + } + if len(endpoints) == 0 { + return status.Error(codes.NotFound, "no Tailscale endpoint found") + } + + type taggedStatus struct { + tag string + status *adapter.TailscaleEndpointStatus + } + updates := make(chan taggedStatus, len(endpoints)) + ctx, cancel := context.WithCancel(server.Context()) + defer cancel() + + var waitGroup sync.WaitGroup + for _, endpoint := range endpoints { + waitGroup.Add(1) + go func(tag string, provider adapter.TailscaleEndpoint) { + defer waitGroup.Done() + _ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) { + select { + case updates <- taggedStatus{tag: tag, status: endpointStatus}: + case <-ctx.Done(): + } + }) + }(endpoint.tag, endpoint.provider) + } + + go func() { + waitGroup.Wait() + close(updates) + }() + + var tags []string + statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints)) + for update := range updates { + if _, exists := statuses[update.tag]; !exists { + tags = append(tags, update.tag) + } + statuses[update.tag] = update.status + protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses)) + for _, tag := range tags { + protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag])) + } + sendErr := server.Send(&TailscaleStatusUpdate{ + Endpoints: protoEndpoints, + }) + if sendErr != nil { + return sendErr + } + } + return nil +} + +func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus { + userGroups := make([]*TailscaleUserGroup, len(s.UserGroups)) + for i, group := range s.UserGroups { + peers := make([]*TailscalePeer, len(group.Peers)) + for j, peer := range group.Peers { + peers[j] = tailscalePeerToProto(peer) + } + userGroups[i] = &TailscaleUserGroup{ + UserID: group.UserID, + LoginName: group.LoginName, + DisplayName: group.DisplayName, + ProfilePicURL: group.ProfilePicURL, + Peers: peers, + } + } + result := &TailscaleEndpointStatus{ + EndpointTag: tag, + BackendState: s.BackendState, + AuthURL: s.AuthURL, + NetworkName: s.NetworkName, + MagicDNSSuffix: s.MagicDNSSuffix, + UserGroups: userGroups, + } + if s.Self != nil { + result.Self = tailscalePeerToProto(s.Self) + } + return result +} + +func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer { + return &TailscalePeer{ + HostName: peer.HostName, + DnsName: peer.DNSName, + Os: peer.OS, + TailscaleIPs: peer.TailscaleIPs, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + KeyExpiry: peer.KeyExpiry, + } +} + +func (s *StartedService) StartTailscalePing( + request *TailscalePingRequest, + server grpc.ServerStreamingServer[TailscalePingResponse], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + if endpointManager == nil { + return status.Error(codes.FailedPrecondition, "endpoint manager not available") + } + + var provider adapter.TailscaleEndpoint + if request.EndpointTag != "" { + endpoint, loaded := endpointManager.Get(request.EndpointTag) + if !loaded { + return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag) + } + if endpoint.Type() != C.TypeTailscale { + return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag) + } + pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint) + if !loaded { + return status.Error(codes.FailedPrecondition, "endpoint does not support ping") + } + provider = pingProvider + } else { + for _, endpoint := range endpointManager.Endpoints() { + if endpoint.Type() != C.TypeTailscale { + continue + } + pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint) + if loaded { + provider = pingProvider + break + } + } + if provider == nil { + return status.Error(codes.NotFound, "no Tailscale endpoint found") + } + } + + return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) { + _ = server.Send(&TailscalePingResponse{ + LatencyMs: result.LatencyMs, + IsDirect: result.IsDirect, + Endpoint: result.Endpoint, + DerpRegionID: result.DERPRegionID, + DerpRegionCode: result.DERPRegionCode, + Error: result.Error, + }) + }) +} + +func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { +} + +func (s *StartedService) WriteMessage(level log.Level, message string) { + item := &log.Entry{Level: level, Message: message} + s.logAccess.Lock() + s.logLines.PushBack(item) + if s.logLines.Len() > s.logMaxLines { + s.logLines.Remove(s.logLines.Front()) + } + s.logAccess.Unlock() + s.logSubscriber.Emit(item) + if s.debug { + s.handler.WriteDebugMessage(message) + } +} + +func (s *StartedService) Instance() *Instance { + s.serviceAccess.RLock() + defer s.serviceAccess.RUnlock() + return s.instance +} diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go new file mode 100644 index 00000000..28906960 --- /dev/null +++ b/daemon/started_service.pb.go @@ -0,0 +1,3197 @@ +package daemon + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type LogLevel int32 + +const ( + LogLevel_PANIC LogLevel = 0 + LogLevel_FATAL LogLevel = 1 + LogLevel_ERROR LogLevel = 2 + LogLevel_WARN LogLevel = 3 + LogLevel_INFO LogLevel = 4 + LogLevel_DEBUG LogLevel = 5 + LogLevel_TRACE LogLevel = 6 +) + +// Enum value maps for LogLevel. +var ( + LogLevel_name = map[int32]string{ + 0: "PANIC", + 1: "FATAL", + 2: "ERROR", + 3: "WARN", + 4: "INFO", + 5: "DEBUG", + 6: "TRACE", + } + LogLevel_value = map[string]int32{ + "PANIC": 0, + "FATAL": 1, + "ERROR": 2, + "WARN": 3, + "INFO": 4, + "DEBUG": 5, + "TRACE": 6, + } +) + +func (x LogLevel) Enum() *LogLevel { + p := new(LogLevel) + *p = x + return p +} + +func (x LogLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LogLevel) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_started_service_proto_enumTypes[0].Descriptor() +} + +func (LogLevel) Type() protoreflect.EnumType { + return &file_daemon_started_service_proto_enumTypes[0] +} + +func (x LogLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LogLevel.Descriptor instead. +func (LogLevel) EnumDescriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{0} +} + +type ConnectionEventType int32 + +const ( + ConnectionEventType_CONNECTION_EVENT_NEW ConnectionEventType = 0 + ConnectionEventType_CONNECTION_EVENT_UPDATE ConnectionEventType = 1 + ConnectionEventType_CONNECTION_EVENT_CLOSED ConnectionEventType = 2 +) + +// Enum value maps for ConnectionEventType. +var ( + ConnectionEventType_name = map[int32]string{ + 0: "CONNECTION_EVENT_NEW", + 1: "CONNECTION_EVENT_UPDATE", + 2: "CONNECTION_EVENT_CLOSED", + } + ConnectionEventType_value = map[string]int32{ + "CONNECTION_EVENT_NEW": 0, + "CONNECTION_EVENT_UPDATE": 1, + "CONNECTION_EVENT_CLOSED": 2, + } +) + +func (x ConnectionEventType) Enum() *ConnectionEventType { + p := new(ConnectionEventType) + *p = x + return p +} + +func (x ConnectionEventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConnectionEventType) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_started_service_proto_enumTypes[1].Descriptor() +} + +func (ConnectionEventType) Type() protoreflect.EnumType { + return &file_daemon_started_service_proto_enumTypes[1] +} + +func (x ConnectionEventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConnectionEventType.Descriptor instead. +func (ConnectionEventType) EnumDescriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{1} +} + +type ServiceStatus_Type int32 + +const ( + ServiceStatus_IDLE ServiceStatus_Type = 0 + ServiceStatus_STARTING ServiceStatus_Type = 1 + ServiceStatus_STARTED ServiceStatus_Type = 2 + ServiceStatus_STOPPING ServiceStatus_Type = 3 + ServiceStatus_FATAL ServiceStatus_Type = 4 +) + +// Enum value maps for ServiceStatus_Type. +var ( + ServiceStatus_Type_name = map[int32]string{ + 0: "IDLE", + 1: "STARTING", + 2: "STARTED", + 3: "STOPPING", + 4: "FATAL", + } + ServiceStatus_Type_value = map[string]int32{ + "IDLE": 0, + "STARTING": 1, + "STARTED": 2, + "STOPPING": 3, + "FATAL": 4, + } +) + +func (x ServiceStatus_Type) Enum() *ServiceStatus_Type { + p := new(ServiceStatus_Type) + *p = x + return p +} + +func (x ServiceStatus_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_started_service_proto_enumTypes[2].Descriptor() +} + +func (ServiceStatus_Type) Type() protoreflect.EnumType { + return &file_daemon_started_service_proto_enumTypes[2] +} + +func (x ServiceStatus_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ServiceStatus_Type.Descriptor instead. +func (ServiceStatus_Type) EnumDescriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{0, 0} +} + +type DebugCrashRequest_Type int32 + +const ( + DebugCrashRequest_GO DebugCrashRequest_Type = 0 + DebugCrashRequest_NATIVE DebugCrashRequest_Type = 1 +) + +// Enum value maps for DebugCrashRequest_Type. +var ( + DebugCrashRequest_Type_name = map[int32]string{ + 0: "GO", + 1: "NATIVE", + } + DebugCrashRequest_Type_value = map[string]int32{ + "GO": 0, + "NATIVE": 1, + } +) + +func (x DebugCrashRequest_Type) Enum() *DebugCrashRequest_Type { + p := new(DebugCrashRequest_Type) + *p = x + return p +} + +func (x DebugCrashRequest_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DebugCrashRequest_Type) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_started_service_proto_enumTypes[3].Descriptor() +} + +func (DebugCrashRequest_Type) Type() protoreflect.EnumType { + return &file_daemon_started_service_proto_enumTypes[3] +} + +func (x DebugCrashRequest_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DebugCrashRequest_Type.Descriptor instead. +func (DebugCrashRequest_Type) EnumDescriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{16, 0} +} + +type ServiceStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status ServiceStatus_Type `protobuf:"varint,1,opt,name=status,proto3,enum=daemon.ServiceStatus_Type" json:"status,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceStatus) Reset() { + *x = ServiceStatus{} + mi := &file_daemon_started_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceStatus) ProtoMessage() {} + +func (x *ServiceStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServiceStatus.ProtoReflect.Descriptor instead. +func (*ServiceStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ServiceStatus) GetStatus() ServiceStatus_Type { + if x != nil { + return x.Status + } + return ServiceStatus_IDLE +} + +func (x *ServiceStatus) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type ReloadServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewProfileContent string `protobuf:"bytes,1,opt,name=newProfileContent,proto3" json:"newProfileContent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReloadServiceRequest) Reset() { + *x = ReloadServiceRequest{} + mi := &file_daemon_started_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReloadServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReloadServiceRequest) ProtoMessage() {} + +func (x *ReloadServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReloadServiceRequest.ProtoReflect.Descriptor instead. +func (*ReloadServiceRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ReloadServiceRequest) GetNewProfileContent() string { + if x != nil { + return x.NewProfileContent + } + return "" +} + +type SubscribeStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeStatusRequest) Reset() { + *x = SubscribeStatusRequest{} + mi := &file_daemon_started_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeStatusRequest) ProtoMessage() {} + +func (x *SubscribeStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeStatusRequest.ProtoReflect.Descriptor instead. +func (*SubscribeStatusRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{2} +} + +func (x *SubscribeStatusRequest) GetInterval() int64 { + if x != nil { + return x.Interval + } + return 0 +} + +type Log struct { + state protoimpl.MessageState `protogen:"open.v1"` + Messages []*Log_Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Log) Reset() { + *x = Log{} + mi := &file_daemon_started_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Log) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Log) ProtoMessage() {} + +func (x *Log) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Log.ProtoReflect.Descriptor instead. +func (*Log) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{3} +} + +func (x *Log) GetMessages() []*Log_Message { + if x != nil { + return x.Messages + } + return nil +} + +func (x *Log) GetReset_() bool { + if x != nil { + return x.Reset_ + } + return false +} + +type DefaultLogLevel struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DefaultLogLevel) Reset() { + *x = DefaultLogLevel{} + mi := &file_daemon_started_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DefaultLogLevel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DefaultLogLevel) ProtoMessage() {} + +func (x *DefaultLogLevel) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DefaultLogLevel.ProtoReflect.Descriptor instead. +func (*DefaultLogLevel) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{4} +} + +func (x *DefaultLogLevel) GetLevel() LogLevel { + if x != nil { + return x.Level + } + return LogLevel_PANIC +} + +type Status struct { + state protoimpl.MessageState `protogen:"open.v1"` + Memory uint64 `protobuf:"varint,1,opt,name=memory,proto3" json:"memory,omitempty"` + Goroutines int32 `protobuf:"varint,2,opt,name=goroutines,proto3" json:"goroutines,omitempty"` + ConnectionsIn int32 `protobuf:"varint,3,opt,name=connectionsIn,proto3" json:"connectionsIn,omitempty"` + ConnectionsOut int32 `protobuf:"varint,4,opt,name=connectionsOut,proto3" json:"connectionsOut,omitempty"` + TrafficAvailable bool `protobuf:"varint,5,opt,name=trafficAvailable,proto3" json:"trafficAvailable,omitempty"` + Uplink int64 `protobuf:"varint,6,opt,name=uplink,proto3" json:"uplink,omitempty"` + Downlink int64 `protobuf:"varint,7,opt,name=downlink,proto3" json:"downlink,omitempty"` + UplinkTotal int64 `protobuf:"varint,8,opt,name=uplinkTotal,proto3" json:"uplinkTotal,omitempty"` + DownlinkTotal int64 `protobuf:"varint,9,opt,name=downlinkTotal,proto3" json:"downlinkTotal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Status) Reset() { + *x = Status{} + mi := &file_daemon_started_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{5} +} + +func (x *Status) GetMemory() uint64 { + if x != nil { + return x.Memory + } + return 0 +} + +func (x *Status) GetGoroutines() int32 { + if x != nil { + return x.Goroutines + } + return 0 +} + +func (x *Status) GetConnectionsIn() int32 { + if x != nil { + return x.ConnectionsIn + } + return 0 +} + +func (x *Status) GetConnectionsOut() int32 { + if x != nil { + return x.ConnectionsOut + } + return 0 +} + +func (x *Status) GetTrafficAvailable() bool { + if x != nil { + return x.TrafficAvailable + } + return false +} + +func (x *Status) GetUplink() int64 { + if x != nil { + return x.Uplink + } + return 0 +} + +func (x *Status) GetDownlink() int64 { + if x != nil { + return x.Downlink + } + return 0 +} + +func (x *Status) GetUplinkTotal() int64 { + if x != nil { + return x.UplinkTotal + } + return 0 +} + +func (x *Status) GetDownlinkTotal() int64 { + if x != nil { + return x.DownlinkTotal + } + return 0 +} + +type Groups struct { + state protoimpl.MessageState `protogen:"open.v1"` + Group []*Group `protobuf:"bytes,1,rep,name=group,proto3" json:"group,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Groups) Reset() { + *x = Groups{} + mi := &file_daemon_started_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Groups) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Groups) ProtoMessage() {} + +func (x *Groups) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Groups.ProtoReflect.Descriptor instead. +func (*Groups) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{6} +} + +func (x *Groups) GetGroup() []*Group { + if x != nil { + return x.Group + } + return nil +} + +type Group struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Selectable bool `protobuf:"varint,3,opt,name=selectable,proto3" json:"selectable,omitempty"` + Selected string `protobuf:"bytes,4,opt,name=selected,proto3" json:"selected,omitempty"` + IsExpand bool `protobuf:"varint,5,opt,name=isExpand,proto3" json:"isExpand,omitempty"` + Items []*GroupItem `protobuf:"bytes,6,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Group) Reset() { + *x = Group{} + mi := &file_daemon_started_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Group) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Group) ProtoMessage() {} + +func (x *Group) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Group.ProtoReflect.Descriptor instead. +func (*Group) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{7} +} + +func (x *Group) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *Group) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Group) GetSelectable() bool { + if x != nil { + return x.Selectable + } + return false +} + +func (x *Group) GetSelected() string { + if x != nil { + return x.Selected + } + return "" +} + +func (x *Group) GetIsExpand() bool { + if x != nil { + return x.IsExpand + } + return false +} + +func (x *Group) GetItems() []*GroupItem { + if x != nil { + return x.Items + } + return nil +} + +type GroupItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + UrlTestTime int64 `protobuf:"varint,3,opt,name=urlTestTime,proto3" json:"urlTestTime,omitempty"` + UrlTestDelay int32 `protobuf:"varint,4,opt,name=urlTestDelay,proto3" json:"urlTestDelay,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GroupItem) Reset() { + *x = GroupItem{} + mi := &file_daemon_started_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GroupItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupItem) ProtoMessage() {} + +func (x *GroupItem) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupItem.ProtoReflect.Descriptor instead. +func (*GroupItem) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{8} +} + +func (x *GroupItem) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *GroupItem) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *GroupItem) GetUrlTestTime() int64 { + if x != nil { + return x.UrlTestTime + } + return 0 +} + +func (x *GroupItem) GetUrlTestDelay() int32 { + if x != nil { + return x.UrlTestDelay + } + return 0 +} + +type URLTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OutboundTag string `protobuf:"bytes,1,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *URLTestRequest) Reset() { + *x = URLTestRequest{} + mi := &file_daemon_started_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *URLTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*URLTestRequest) ProtoMessage() {} + +func (x *URLTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use URLTestRequest.ProtoReflect.Descriptor instead. +func (*URLTestRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{9} +} + +func (x *URLTestRequest) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +type SelectOutboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupTag string `protobuf:"bytes,1,opt,name=groupTag,proto3" json:"groupTag,omitempty"` + OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SelectOutboundRequest) Reset() { + *x = SelectOutboundRequest{} + mi := &file_daemon_started_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SelectOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectOutboundRequest) ProtoMessage() {} + +func (x *SelectOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectOutboundRequest.ProtoReflect.Descriptor instead. +func (*SelectOutboundRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{10} +} + +func (x *SelectOutboundRequest) GetGroupTag() string { + if x != nil { + return x.GroupTag + } + return "" +} + +func (x *SelectOutboundRequest) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +type SetGroupExpandRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupTag string `protobuf:"bytes,1,opt,name=groupTag,proto3" json:"groupTag,omitempty"` + IsExpand bool `protobuf:"varint,2,opt,name=isExpand,proto3" json:"isExpand,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetGroupExpandRequest) Reset() { + *x = SetGroupExpandRequest{} + mi := &file_daemon_started_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetGroupExpandRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetGroupExpandRequest) ProtoMessage() {} + +func (x *SetGroupExpandRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetGroupExpandRequest.ProtoReflect.Descriptor instead. +func (*SetGroupExpandRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{11} +} + +func (x *SetGroupExpandRequest) GetGroupTag() string { + if x != nil { + return x.GroupTag + } + return "" +} + +func (x *SetGroupExpandRequest) GetIsExpand() bool { + if x != nil { + return x.IsExpand + } + return false +} + +type ClashMode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClashMode) Reset() { + *x = ClashMode{} + mi := &file_daemon_started_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClashMode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClashMode) ProtoMessage() {} + +func (x *ClashMode) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClashMode.ProtoReflect.Descriptor instead. +func (*ClashMode) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{12} +} + +func (x *ClashMode) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +type ClashModeStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + ModeList []string `protobuf:"bytes,1,rep,name=modeList,proto3" json:"modeList,omitempty"` + CurrentMode string `protobuf:"bytes,2,opt,name=currentMode,proto3" json:"currentMode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClashModeStatus) Reset() { + *x = ClashModeStatus{} + mi := &file_daemon_started_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClashModeStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClashModeStatus) ProtoMessage() {} + +func (x *ClashModeStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClashModeStatus.ProtoReflect.Descriptor instead. +func (*ClashModeStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{13} +} + +func (x *ClashModeStatus) GetModeList() []string { + if x != nil { + return x.ModeList + } + return nil +} + +func (x *ClashModeStatus) GetCurrentMode() string { + if x != nil { + return x.CurrentMode + } + return "" +} + +type SystemProxyStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Available bool `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SystemProxyStatus) Reset() { + *x = SystemProxyStatus{} + mi := &file_daemon_started_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemProxyStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemProxyStatus) ProtoMessage() {} + +func (x *SystemProxyStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SystemProxyStatus.ProtoReflect.Descriptor instead. +func (*SystemProxyStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{14} +} + +func (x *SystemProxyStatus) GetAvailable() bool { + if x != nil { + return x.Available + } + return false +} + +func (x *SystemProxyStatus) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type SetSystemProxyEnabledRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetSystemProxyEnabledRequest) Reset() { + *x = SetSystemProxyEnabledRequest{} + mi := &file_daemon_started_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetSystemProxyEnabledRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetSystemProxyEnabledRequest) ProtoMessage() {} + +func (x *SetSystemProxyEnabledRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetSystemProxyEnabledRequest.ProtoReflect.Descriptor instead. +func (*SetSystemProxyEnabledRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{15} +} + +func (x *SetSystemProxyEnabledRequest) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type DebugCrashRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type DebugCrashRequest_Type `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.DebugCrashRequest_Type" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DebugCrashRequest) Reset() { + *x = DebugCrashRequest{} + mi := &file_daemon_started_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DebugCrashRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DebugCrashRequest) ProtoMessage() {} + +func (x *DebugCrashRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DebugCrashRequest.ProtoReflect.Descriptor instead. +func (*DebugCrashRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{16} +} + +func (x *DebugCrashRequest) GetType() DebugCrashRequest_Type { + if x != nil { + return x.Type + } + return DebugCrashRequest_GO +} + +type SubscribeConnectionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeConnectionsRequest) Reset() { + *x = SubscribeConnectionsRequest{} + mi := &file_daemon_started_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeConnectionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeConnectionsRequest) ProtoMessage() {} + +func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeConnectionsRequest.ProtoReflect.Descriptor instead. +func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{17} +} + +func (x *SubscribeConnectionsRequest) GetInterval() int64 { + if x != nil { + return x.Interval + } + return 0 +} + +type ConnectionEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type ConnectionEventType `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.ConnectionEventType" json:"type,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + Connection *Connection `protobuf:"bytes,3,opt,name=connection,proto3" json:"connection,omitempty"` + UplinkDelta int64 `protobuf:"varint,4,opt,name=uplinkDelta,proto3" json:"uplinkDelta,omitempty"` + DownlinkDelta int64 `protobuf:"varint,5,opt,name=downlinkDelta,proto3" json:"downlinkDelta,omitempty"` + ClosedAt int64 `protobuf:"varint,6,opt,name=closedAt,proto3" json:"closedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectionEvent) Reset() { + *x = ConnectionEvent{} + mi := &file_daemon_started_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectionEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectionEvent) ProtoMessage() {} + +func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead. +func (*ConnectionEvent) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{18} +} + +func (x *ConnectionEvent) GetType() ConnectionEventType { + if x != nil { + return x.Type + } + return ConnectionEventType_CONNECTION_EVENT_NEW +} + +func (x *ConnectionEvent) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ConnectionEvent) GetConnection() *Connection { + if x != nil { + return x.Connection + } + return nil +} + +func (x *ConnectionEvent) GetUplinkDelta() int64 { + if x != nil { + return x.UplinkDelta + } + return 0 +} + +func (x *ConnectionEvent) GetDownlinkDelta() int64 { + if x != nil { + return x.DownlinkDelta + } + return 0 +} + +func (x *ConnectionEvent) GetClosedAt() int64 { + if x != nil { + return x.ClosedAt + } + return 0 +} + +type ConnectionEvents struct { + state protoimpl.MessageState `protogen:"open.v1"` + Events []*ConnectionEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectionEvents) Reset() { + *x = ConnectionEvents{} + mi := &file_daemon_started_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectionEvents) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectionEvents) ProtoMessage() {} + +func (x *ConnectionEvents) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead. +func (*ConnectionEvents) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{19} +} + +func (x *ConnectionEvents) GetEvents() []*ConnectionEvent { + if x != nil { + return x.Events + } + return nil +} + +func (x *ConnectionEvents) GetReset_() bool { + if x != nil { + return x.Reset_ + } + return false +} + +type Connection struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Inbound string `protobuf:"bytes,2,opt,name=inbound,proto3" json:"inbound,omitempty"` + InboundType string `protobuf:"bytes,3,opt,name=inboundType,proto3" json:"inboundType,omitempty"` + IpVersion int32 `protobuf:"varint,4,opt,name=ipVersion,proto3" json:"ipVersion,omitempty"` + Network string `protobuf:"bytes,5,opt,name=network,proto3" json:"network,omitempty"` + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + Destination string `protobuf:"bytes,7,opt,name=destination,proto3" json:"destination,omitempty"` + Domain string `protobuf:"bytes,8,opt,name=domain,proto3" json:"domain,omitempty"` + Protocol string `protobuf:"bytes,9,opt,name=protocol,proto3" json:"protocol,omitempty"` + User string `protobuf:"bytes,10,opt,name=user,proto3" json:"user,omitempty"` + FromOutbound string `protobuf:"bytes,11,opt,name=fromOutbound,proto3" json:"fromOutbound,omitempty"` + CreatedAt int64 `protobuf:"varint,12,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + ClosedAt int64 `protobuf:"varint,13,opt,name=closedAt,proto3" json:"closedAt,omitempty"` + Uplink int64 `protobuf:"varint,14,opt,name=uplink,proto3" json:"uplink,omitempty"` + Downlink int64 `protobuf:"varint,15,opt,name=downlink,proto3" json:"downlink,omitempty"` + UplinkTotal int64 `protobuf:"varint,16,opt,name=uplinkTotal,proto3" json:"uplinkTotal,omitempty"` + DownlinkTotal int64 `protobuf:"varint,17,opt,name=downlinkTotal,proto3" json:"downlinkTotal,omitempty"` + Rule string `protobuf:"bytes,18,opt,name=rule,proto3" json:"rule,omitempty"` + Outbound string `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"` + OutboundType string `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"` + ChainList []string `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"` + ProcessInfo *ProcessInfo `protobuf:"bytes,22,opt,name=processInfo,proto3" json:"processInfo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Connection) Reset() { + *x = Connection{} + mi := &file_daemon_started_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Connection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connection) ProtoMessage() {} + +func (x *Connection) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Connection.ProtoReflect.Descriptor instead. +func (*Connection) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{20} +} + +func (x *Connection) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Connection) GetInbound() string { + if x != nil { + return x.Inbound + } + return "" +} + +func (x *Connection) GetInboundType() string { + if x != nil { + return x.InboundType + } + return "" +} + +func (x *Connection) GetIpVersion() int32 { + if x != nil { + return x.IpVersion + } + return 0 +} + +func (x *Connection) GetNetwork() string { + if x != nil { + return x.Network + } + return "" +} + +func (x *Connection) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *Connection) GetDestination() string { + if x != nil { + return x.Destination + } + return "" +} + +func (x *Connection) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *Connection) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *Connection) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *Connection) GetFromOutbound() string { + if x != nil { + return x.FromOutbound + } + return "" +} + +func (x *Connection) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *Connection) GetClosedAt() int64 { + if x != nil { + return x.ClosedAt + } + return 0 +} + +func (x *Connection) GetUplink() int64 { + if x != nil { + return x.Uplink + } + return 0 +} + +func (x *Connection) GetDownlink() int64 { + if x != nil { + return x.Downlink + } + return 0 +} + +func (x *Connection) GetUplinkTotal() int64 { + if x != nil { + return x.UplinkTotal + } + return 0 +} + +func (x *Connection) GetDownlinkTotal() int64 { + if x != nil { + return x.DownlinkTotal + } + return 0 +} + +func (x *Connection) GetRule() string { + if x != nil { + return x.Rule + } + return "" +} + +func (x *Connection) GetOutbound() string { + if x != nil { + return x.Outbound + } + return "" +} + +func (x *Connection) GetOutboundType() string { + if x != nil { + return x.OutboundType + } + return "" +} + +func (x *Connection) GetChainList() []string { + if x != nil { + return x.ChainList + } + return nil +} + +func (x *Connection) GetProcessInfo() *ProcessInfo { + if x != nil { + return x.ProcessInfo + } + return nil +} + +type ProcessInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProcessId uint32 `protobuf:"varint,1,opt,name=processId,proto3" json:"processId,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"` + UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"` + ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"` + PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProcessInfo) Reset() { + *x = ProcessInfo{} + mi := &file_daemon_started_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProcessInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProcessInfo) ProtoMessage() {} + +func (x *ProcessInfo) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead. +func (*ProcessInfo) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{21} +} + +func (x *ProcessInfo) GetProcessId() uint32 { + if x != nil { + return x.ProcessId + } + return 0 +} + +func (x *ProcessInfo) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *ProcessInfo) GetUserName() string { + if x != nil { + return x.UserName + } + return "" +} + +func (x *ProcessInfo) GetProcessPath() string { + if x != nil { + return x.ProcessPath + } + return "" +} + +func (x *ProcessInfo) GetPackageNames() []string { + if x != nil { + return x.PackageNames + } + return nil +} + +type CloseConnectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseConnectionRequest) Reset() { + *x = CloseConnectionRequest{} + mi := &file_daemon_started_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseConnectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseConnectionRequest) ProtoMessage() {} + +func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. +func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{22} +} + +func (x *CloseConnectionRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type DeprecatedWarnings struct { + state protoimpl.MessageState `protogen:"open.v1"` + Warnings []*DeprecatedWarning `protobuf:"bytes,1,rep,name=warnings,proto3" json:"warnings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeprecatedWarnings) Reset() { + *x = DeprecatedWarnings{} + mi := &file_daemon_started_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeprecatedWarnings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeprecatedWarnings) ProtoMessage() {} + +func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead. +func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{23} +} + +func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { + if x != nil { + return x.Warnings + } + return nil +} + +type DeprecatedWarning struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Impending bool `protobuf:"varint,2,opt,name=impending,proto3" json:"impending,omitempty"` + MigrationLink string `protobuf:"bytes,3,opt,name=migrationLink,proto3" json:"migrationLink,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + DeprecatedVersion string `protobuf:"bytes,5,opt,name=deprecatedVersion,proto3" json:"deprecatedVersion,omitempty"` + ScheduledVersion string `protobuf:"bytes,6,opt,name=scheduledVersion,proto3" json:"scheduledVersion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeprecatedWarning) Reset() { + *x = DeprecatedWarning{} + mi := &file_daemon_started_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeprecatedWarning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeprecatedWarning) ProtoMessage() {} + +func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead. +func (*DeprecatedWarning) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{24} +} + +func (x *DeprecatedWarning) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *DeprecatedWarning) GetImpending() bool { + if x != nil { + return x.Impending + } + return false +} + +func (x *DeprecatedWarning) GetMigrationLink() string { + if x != nil { + return x.MigrationLink + } + return "" +} + +func (x *DeprecatedWarning) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *DeprecatedWarning) GetDeprecatedVersion() string { + if x != nil { + return x.DeprecatedVersion + } + return "" +} + +func (x *DeprecatedWarning) GetScheduledVersion() string { + if x != nil { + return x.ScheduledVersion + } + return "" +} + +type StartedAt struct { + state protoimpl.MessageState `protogen:"open.v1"` + StartedAt int64 `protobuf:"varint,1,opt,name=startedAt,proto3" json:"startedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartedAt) Reset() { + *x = StartedAt{} + mi := &file_daemon_started_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartedAt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartedAt) ProtoMessage() {} + +func (x *StartedAt) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartedAt.ProtoReflect.Descriptor instead. +func (*StartedAt) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{25} +} + +func (x *StartedAt) GetStartedAt() int64 { + if x != nil { + return x.StartedAt + } + return 0 +} + +type OutboundList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outbounds []*GroupItem `protobuf:"bytes,1,rep,name=outbounds,proto3" json:"outbounds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutboundList) Reset() { + *x = OutboundList{} + mi := &file_daemon_started_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutboundList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundList) ProtoMessage() {} + +func (x *OutboundList) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutboundList.ProtoReflect.Descriptor instead. +func (*OutboundList) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{26} +} + +func (x *OutboundList) GetOutbounds() []*GroupItem { + if x != nil { + return x.Outbounds + } + return nil +} + +type NetworkQualityTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigURL string `protobuf:"bytes,1,opt,name=configURL,proto3" json:"configURL,omitempty"` + OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` + Serial bool `protobuf:"varint,3,opt,name=serial,proto3" json:"serial,omitempty"` + MaxRuntimeSeconds int32 `protobuf:"varint,4,opt,name=maxRuntimeSeconds,proto3" json:"maxRuntimeSeconds,omitempty"` + Http3 bool `protobuf:"varint,5,opt,name=http3,proto3" json:"http3,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkQualityTestRequest) Reset() { + *x = NetworkQualityTestRequest{} + mi := &file_daemon_started_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkQualityTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkQualityTestRequest) ProtoMessage() {} + +func (x *NetworkQualityTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkQualityTestRequest.ProtoReflect.Descriptor instead. +func (*NetworkQualityTestRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{27} +} + +func (x *NetworkQualityTestRequest) GetConfigURL() string { + if x != nil { + return x.ConfigURL + } + return "" +} + +func (x *NetworkQualityTestRequest) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +func (x *NetworkQualityTestRequest) GetSerial() bool { + if x != nil { + return x.Serial + } + return false +} + +func (x *NetworkQualityTestRequest) GetMaxRuntimeSeconds() int32 { + if x != nil { + return x.MaxRuntimeSeconds + } + return 0 +} + +func (x *NetworkQualityTestRequest) GetHttp3() bool { + if x != nil { + return x.Http3 + } + return false +} + +type NetworkQualityTestProgress struct { + state protoimpl.MessageState `protogen:"open.v1"` + Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"` + DownloadCapacity int64 `protobuf:"varint,2,opt,name=downloadCapacity,proto3" json:"downloadCapacity,omitempty"` + UploadCapacity int64 `protobuf:"varint,3,opt,name=uploadCapacity,proto3" json:"uploadCapacity,omitempty"` + DownloadRPM int32 `protobuf:"varint,4,opt,name=downloadRPM,proto3" json:"downloadRPM,omitempty"` + UploadRPM int32 `protobuf:"varint,5,opt,name=uploadRPM,proto3" json:"uploadRPM,omitempty"` + IdleLatencyMs int32 `protobuf:"varint,6,opt,name=idleLatencyMs,proto3" json:"idleLatencyMs,omitempty"` + ElapsedMs int64 `protobuf:"varint,7,opt,name=elapsedMs,proto3" json:"elapsedMs,omitempty"` + IsFinal bool `protobuf:"varint,8,opt,name=isFinal,proto3" json:"isFinal,omitempty"` + Error string `protobuf:"bytes,9,opt,name=error,proto3" json:"error,omitempty"` + DownloadCapacityAccuracy int32 `protobuf:"varint,10,opt,name=downloadCapacityAccuracy,proto3" json:"downloadCapacityAccuracy,omitempty"` + UploadCapacityAccuracy int32 `protobuf:"varint,11,opt,name=uploadCapacityAccuracy,proto3" json:"uploadCapacityAccuracy,omitempty"` + DownloadRPMAccuracy int32 `protobuf:"varint,12,opt,name=downloadRPMAccuracy,proto3" json:"downloadRPMAccuracy,omitempty"` + UploadRPMAccuracy int32 `protobuf:"varint,13,opt,name=uploadRPMAccuracy,proto3" json:"uploadRPMAccuracy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkQualityTestProgress) Reset() { + *x = NetworkQualityTestProgress{} + mi := &file_daemon_started_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkQualityTestProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkQualityTestProgress) ProtoMessage() {} + +func (x *NetworkQualityTestProgress) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkQualityTestProgress.ProtoReflect.Descriptor instead. +func (*NetworkQualityTestProgress) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{28} +} + +func (x *NetworkQualityTestProgress) GetPhase() int32 { + if x != nil { + return x.Phase + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetDownloadCapacity() int64 { + if x != nil { + return x.DownloadCapacity + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadCapacity() int64 { + if x != nil { + return x.UploadCapacity + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetDownloadRPM() int32 { + if x != nil { + return x.DownloadRPM + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadRPM() int32 { + if x != nil { + return x.UploadRPM + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetIdleLatencyMs() int32 { + if x != nil { + return x.IdleLatencyMs + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetElapsedMs() int64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetIsFinal() bool { + if x != nil { + return x.IsFinal + } + return false +} + +func (x *NetworkQualityTestProgress) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *NetworkQualityTestProgress) GetDownloadCapacityAccuracy() int32 { + if x != nil { + return x.DownloadCapacityAccuracy + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadCapacityAccuracy() int32 { + if x != nil { + return x.UploadCapacityAccuracy + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetDownloadRPMAccuracy() int32 { + if x != nil { + return x.DownloadRPMAccuracy + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadRPMAccuracy() int32 { + if x != nil { + return x.UploadRPMAccuracy + } + return 0 +} + +type STUNTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STUNTestRequest) Reset() { + *x = STUNTestRequest{} + mi := &file_daemon_started_service_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STUNTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STUNTestRequest) ProtoMessage() {} + +func (x *STUNTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STUNTestRequest.ProtoReflect.Descriptor instead. +func (*STUNTestRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{29} +} + +func (x *STUNTestRequest) GetServer() string { + if x != nil { + return x.Server + } + return "" +} + +func (x *STUNTestRequest) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +type STUNTestProgress struct { + state protoimpl.MessageState `protogen:"open.v1"` + Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"` + ExternalAddr string `protobuf:"bytes,2,opt,name=externalAddr,proto3" json:"externalAddr,omitempty"` + LatencyMs int32 `protobuf:"varint,3,opt,name=latencyMs,proto3" json:"latencyMs,omitempty"` + NatMapping int32 `protobuf:"varint,4,opt,name=natMapping,proto3" json:"natMapping,omitempty"` + NatFiltering int32 `protobuf:"varint,5,opt,name=natFiltering,proto3" json:"natFiltering,omitempty"` + IsFinal bool `protobuf:"varint,6,opt,name=isFinal,proto3" json:"isFinal,omitempty"` + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + NatTypeSupported bool `protobuf:"varint,8,opt,name=natTypeSupported,proto3" json:"natTypeSupported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STUNTestProgress) Reset() { + *x = STUNTestProgress{} + mi := &file_daemon_started_service_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STUNTestProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STUNTestProgress) ProtoMessage() {} + +func (x *STUNTestProgress) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STUNTestProgress.ProtoReflect.Descriptor instead. +func (*STUNTestProgress) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{30} +} + +func (x *STUNTestProgress) GetPhase() int32 { + if x != nil { + return x.Phase + } + return 0 +} + +func (x *STUNTestProgress) GetExternalAddr() string { + if x != nil { + return x.ExternalAddr + } + return "" +} + +func (x *STUNTestProgress) GetLatencyMs() int32 { + if x != nil { + return x.LatencyMs + } + return 0 +} + +func (x *STUNTestProgress) GetNatMapping() int32 { + if x != nil { + return x.NatMapping + } + return 0 +} + +func (x *STUNTestProgress) GetNatFiltering() int32 { + if x != nil { + return x.NatFiltering + } + return 0 +} + +func (x *STUNTestProgress) GetIsFinal() bool { + if x != nil { + return x.IsFinal + } + return false +} + +func (x *STUNTestProgress) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *STUNTestProgress) GetNatTypeSupported() bool { + if x != nil { + return x.NatTypeSupported + } + return false +} + +type TailscaleStatusUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints []*TailscaleEndpointStatus `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleStatusUpdate) Reset() { + *x = TailscaleStatusUpdate{} + mi := &file_daemon_started_service_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleStatusUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleStatusUpdate) ProtoMessage() {} + +func (x *TailscaleStatusUpdate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscaleStatusUpdate.ProtoReflect.Descriptor instead. +func (*TailscaleStatusUpdate) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{31} +} + +func (x *TailscaleStatusUpdate) GetEndpoints() []*TailscaleEndpointStatus { + if x != nil { + return x.Endpoints + } + return nil +} + +type TailscaleEndpointStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + BackendState string `protobuf:"bytes,2,opt,name=backendState,proto3" json:"backendState,omitempty"` + AuthURL string `protobuf:"bytes,3,opt,name=authURL,proto3" json:"authURL,omitempty"` + NetworkName string `protobuf:"bytes,4,opt,name=networkName,proto3" json:"networkName,omitempty"` + MagicDNSSuffix string `protobuf:"bytes,5,opt,name=magicDNSSuffix,proto3" json:"magicDNSSuffix,omitempty"` + Self *TailscalePeer `protobuf:"bytes,6,opt,name=self,proto3" json:"self,omitempty"` + UserGroups []*TailscaleUserGroup `protobuf:"bytes,7,rep,name=userGroups,proto3" json:"userGroups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleEndpointStatus) Reset() { + *x = TailscaleEndpointStatus{} + mi := &file_daemon_started_service_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleEndpointStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleEndpointStatus) ProtoMessage() {} + +func (x *TailscaleEndpointStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscaleEndpointStatus.ProtoReflect.Descriptor instead. +func (*TailscaleEndpointStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{32} +} + +func (x *TailscaleEndpointStatus) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *TailscaleEndpointStatus) GetBackendState() string { + if x != nil { + return x.BackendState + } + return "" +} + +func (x *TailscaleEndpointStatus) GetAuthURL() string { + if x != nil { + return x.AuthURL + } + return "" +} + +func (x *TailscaleEndpointStatus) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *TailscaleEndpointStatus) GetMagicDNSSuffix() string { + if x != nil { + return x.MagicDNSSuffix + } + return "" +} + +func (x *TailscaleEndpointStatus) GetSelf() *TailscalePeer { + if x != nil { + return x.Self + } + return nil +} + +func (x *TailscaleEndpointStatus) GetUserGroups() []*TailscaleUserGroup { + if x != nil { + return x.UserGroups + } + return nil +} + +type TailscaleUserGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserID int64 `protobuf:"varint,1,opt,name=userID,proto3" json:"userID,omitempty"` + LoginName string `protobuf:"bytes,2,opt,name=loginName,proto3" json:"loginName,omitempty"` + DisplayName string `protobuf:"bytes,3,opt,name=displayName,proto3" json:"displayName,omitempty"` + ProfilePicURL string `protobuf:"bytes,4,opt,name=profilePicURL,proto3" json:"profilePicURL,omitempty"` + Peers []*TailscalePeer `protobuf:"bytes,5,rep,name=peers,proto3" json:"peers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleUserGroup) Reset() { + *x = TailscaleUserGroup{} + mi := &file_daemon_started_service_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleUserGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleUserGroup) ProtoMessage() {} + +func (x *TailscaleUserGroup) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscaleUserGroup.ProtoReflect.Descriptor instead. +func (*TailscaleUserGroup) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{33} +} + +func (x *TailscaleUserGroup) GetUserID() int64 { + if x != nil { + return x.UserID + } + return 0 +} + +func (x *TailscaleUserGroup) GetLoginName() string { + if x != nil { + return x.LoginName + } + return "" +} + +func (x *TailscaleUserGroup) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *TailscaleUserGroup) GetProfilePicURL() string { + if x != nil { + return x.ProfilePicURL + } + return "" +} + +func (x *TailscaleUserGroup) GetPeers() []*TailscalePeer { + if x != nil { + return x.Peers + } + return nil +} + +type TailscalePeer struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostName string `protobuf:"bytes,1,opt,name=hostName,proto3" json:"hostName,omitempty"` + DnsName string `protobuf:"bytes,2,opt,name=dnsName,proto3" json:"dnsName,omitempty"` + Os string `protobuf:"bytes,3,opt,name=os,proto3" json:"os,omitempty"` + TailscaleIPs []string `protobuf:"bytes,4,rep,name=tailscaleIPs,proto3" json:"tailscaleIPs,omitempty"` + Online bool `protobuf:"varint,5,opt,name=online,proto3" json:"online,omitempty"` + ExitNode bool `protobuf:"varint,6,opt,name=exitNode,proto3" json:"exitNode,omitempty"` + ExitNodeOption bool `protobuf:"varint,7,opt,name=exitNodeOption,proto3" json:"exitNodeOption,omitempty"` + Active bool `protobuf:"varint,8,opt,name=active,proto3" json:"active,omitempty"` + RxBytes int64 `protobuf:"varint,9,opt,name=rxBytes,proto3" json:"rxBytes,omitempty"` + TxBytes int64 `protobuf:"varint,10,opt,name=txBytes,proto3" json:"txBytes,omitempty"` + KeyExpiry int64 `protobuf:"varint,11,opt,name=keyExpiry,proto3" json:"keyExpiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePeer) Reset() { + *x = TailscalePeer{} + mi := &file_daemon_started_service_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePeer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePeer) ProtoMessage() {} + +func (x *TailscalePeer) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscalePeer.ProtoReflect.Descriptor instead. +func (*TailscalePeer) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{34} +} + +func (x *TailscalePeer) GetHostName() string { + if x != nil { + return x.HostName + } + return "" +} + +func (x *TailscalePeer) GetDnsName() string { + if x != nil { + return x.DnsName + } + return "" +} + +func (x *TailscalePeer) GetOs() string { + if x != nil { + return x.Os + } + return "" +} + +func (x *TailscalePeer) GetTailscaleIPs() []string { + if x != nil { + return x.TailscaleIPs + } + return nil +} + +func (x *TailscalePeer) GetOnline() bool { + if x != nil { + return x.Online + } + return false +} + +func (x *TailscalePeer) GetExitNode() bool { + if x != nil { + return x.ExitNode + } + return false +} + +func (x *TailscalePeer) GetExitNodeOption() bool { + if x != nil { + return x.ExitNodeOption + } + return false +} + +func (x *TailscalePeer) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *TailscalePeer) GetRxBytes() int64 { + if x != nil { + return x.RxBytes + } + return 0 +} + +func (x *TailscalePeer) GetTxBytes() int64 { + if x != nil { + return x.TxBytes + } + return 0 +} + +func (x *TailscalePeer) GetKeyExpiry() int64 { + if x != nil { + return x.KeyExpiry + } + return 0 +} + +type TailscalePingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + PeerIP string `protobuf:"bytes,2,opt,name=peerIP,proto3" json:"peerIP,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePingRequest) Reset() { + *x = TailscalePingRequest{} + mi := &file_daemon_started_service_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePingRequest) ProtoMessage() {} + +func (x *TailscalePingRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscalePingRequest.ProtoReflect.Descriptor instead. +func (*TailscalePingRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{35} +} + +func (x *TailscalePingRequest) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *TailscalePingRequest) GetPeerIP() string { + if x != nil { + return x.PeerIP + } + return "" +} + +type TailscalePingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + LatencyMs float64 `protobuf:"fixed64,1,opt,name=latencyMs,proto3" json:"latencyMs,omitempty"` + IsDirect bool `protobuf:"varint,2,opt,name=isDirect,proto3" json:"isDirect,omitempty"` + Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + DerpRegionID int32 `protobuf:"varint,4,opt,name=derpRegionID,proto3" json:"derpRegionID,omitempty"` + DerpRegionCode string `protobuf:"bytes,5,opt,name=derpRegionCode,proto3" json:"derpRegionCode,omitempty"` + Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePingResponse) Reset() { + *x = TailscalePingResponse{} + mi := &file_daemon_started_service_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePingResponse) ProtoMessage() {} + +func (x *TailscalePingResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscalePingResponse.ProtoReflect.Descriptor instead. +func (*TailscalePingResponse) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{36} +} + +func (x *TailscalePingResponse) GetLatencyMs() float64 { + if x != nil { + return x.LatencyMs + } + return 0 +} + +func (x *TailscalePingResponse) GetIsDirect() bool { + if x != nil { + return x.IsDirect + } + return false +} + +func (x *TailscalePingResponse) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *TailscalePingResponse) GetDerpRegionID() int32 { + if x != nil { + return x.DerpRegionID + } + return 0 +} + +func (x *TailscalePingResponse) GetDerpRegionCode() string { + if x != nil { + return x.DerpRegionCode + } + return "" +} + +func (x *TailscalePingResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type Log_Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Log_Message) Reset() { + *x = Log_Message{} + mi := &file_daemon_started_service_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Log_Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Log_Message) ProtoMessage() {} + +func (x *Log_Message) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Log_Message.ProtoReflect.Descriptor instead. +func (*Log_Message) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *Log_Message) GetLevel() LogLevel { + if x != nil { + return x.Level + } + return LogLevel_PANIC +} + +func (x *Log_Message) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_daemon_started_service_proto protoreflect.FileDescriptor + +const file_daemon_started_service_proto_rawDesc = "" + + "\n" + + "\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xad\x01\n" + + "\rServiceStatus\x122\n" + + "\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" + + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" + + "\x04Type\x12\b\n" + + "\x04IDLE\x10\x00\x12\f\n" + + "\bSTARTING\x10\x01\x12\v\n" + + "\aSTARTED\x10\x02\x12\f\n" + + "\bSTOPPING\x10\x03\x12\t\n" + + "\x05FATAL\x10\x04\"D\n" + + "\x14ReloadServiceRequest\x12,\n" + + "\x11newProfileContent\x18\x01 \x01(\tR\x11newProfileContent\"4\n" + + "\x16SubscribeStatusRequest\x12\x1a\n" + + "\binterval\x18\x01 \x01(\x03R\binterval\"\x99\x01\n" + + "\x03Log\x12/\n" + + "\bmessages\x18\x01 \x03(\v2\x13.daemon.Log.MessageR\bmessages\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\x1aK\n" + + "\aMessage\x12&\n" + + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"9\n" + + "\x0fDefaultLogLevel\x12&\n" + + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"\xb6\x02\n" + + "\x06Status\x12\x16\n" + + "\x06memory\x18\x01 \x01(\x04R\x06memory\x12\x1e\n" + + "\n" + + "goroutines\x18\x02 \x01(\x05R\n" + + "goroutines\x12$\n" + + "\rconnectionsIn\x18\x03 \x01(\x05R\rconnectionsIn\x12&\n" + + "\x0econnectionsOut\x18\x04 \x01(\x05R\x0econnectionsOut\x12*\n" + + "\x10trafficAvailable\x18\x05 \x01(\bR\x10trafficAvailable\x12\x16\n" + + "\x06uplink\x18\x06 \x01(\x03R\x06uplink\x12\x1a\n" + + "\bdownlink\x18\a \x01(\x03R\bdownlink\x12 \n" + + "\vuplinkTotal\x18\b \x01(\x03R\vuplinkTotal\x12$\n" + + "\rdownlinkTotal\x18\t \x01(\x03R\rdownlinkTotal\"-\n" + + "\x06Groups\x12#\n" + + "\x05group\x18\x01 \x03(\v2\r.daemon.GroupR\x05group\"\xae\x01\n" + + "\x05Group\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x1e\n" + + "\n" + + "selectable\x18\x03 \x01(\bR\n" + + "selectable\x12\x1a\n" + + "\bselected\x18\x04 \x01(\tR\bselected\x12\x1a\n" + + "\bisExpand\x18\x05 \x01(\bR\bisExpand\x12'\n" + + "\x05items\x18\x06 \x03(\v2\x11.daemon.GroupItemR\x05items\"w\n" + + "\tGroupItem\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12 \n" + + "\vurlTestTime\x18\x03 \x01(\x03R\vurlTestTime\x12\"\n" + + "\furlTestDelay\x18\x04 \x01(\x05R\furlTestDelay\"2\n" + + "\x0eURLTestRequest\x12 \n" + + "\voutboundTag\x18\x01 \x01(\tR\voutboundTag\"U\n" + + "\x15SelectOutboundRequest\x12\x1a\n" + + "\bgroupTag\x18\x01 \x01(\tR\bgroupTag\x12 \n" + + "\voutboundTag\x18\x02 \x01(\tR\voutboundTag\"O\n" + + "\x15SetGroupExpandRequest\x12\x1a\n" + + "\bgroupTag\x18\x01 \x01(\tR\bgroupTag\x12\x1a\n" + + "\bisExpand\x18\x02 \x01(\bR\bisExpand\"\x1f\n" + + "\tClashMode\x12\x12\n" + + "\x04mode\x18\x03 \x01(\tR\x04mode\"O\n" + + "\x0fClashModeStatus\x12\x1a\n" + + "\bmodeList\x18\x01 \x03(\tR\bmodeList\x12 \n" + + "\vcurrentMode\x18\x02 \x01(\tR\vcurrentMode\"K\n" + + "\x11SystemProxyStatus\x12\x1c\n" + + "\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" + + "\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" + + "\x1cSetSystemProxyEnabledRequest\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\"c\n" + + "\x11DebugCrashRequest\x122\n" + + "\x04type\x18\x01 \x01(\x0e2\x1e.daemon.DebugCrashRequest.TypeR\x04type\"\x1a\n" + + "\x04Type\x12\x06\n" + + "\x02GO\x10\x00\x12\n" + + "\n" + + "\x06NATIVE\x10\x01\"9\n" + + "\x1bSubscribeConnectionsRequest\x12\x1a\n" + + "\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" + + "\x0fConnectionEvent\x12/\n" + + "\x04type\x18\x01 \x01(\x0e2\x1b.daemon.ConnectionEventTypeR\x04type\x12\x0e\n" + + "\x02id\x18\x02 \x01(\tR\x02id\x122\n" + + "\n" + + "connection\x18\x03 \x01(\v2\x12.daemon.ConnectionR\n" + + "connection\x12 \n" + + "\vuplinkDelta\x18\x04 \x01(\x03R\vuplinkDelta\x12$\n" + + "\rdownlinkDelta\x18\x05 \x01(\x03R\rdownlinkDelta\x12\x1a\n" + + "\bclosedAt\x18\x06 \x01(\x03R\bclosedAt\"Y\n" + + "\x10ConnectionEvents\x12/\n" + + "\x06events\x18\x01 \x03(\v2\x17.daemon.ConnectionEventR\x06events\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\"\x95\x05\n" + + "\n" + + "Connection\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + + "\ainbound\x18\x02 \x01(\tR\ainbound\x12 \n" + + "\vinboundType\x18\x03 \x01(\tR\vinboundType\x12\x1c\n" + + "\tipVersion\x18\x04 \x01(\x05R\tipVersion\x12\x18\n" + + "\anetwork\x18\x05 \x01(\tR\anetwork\x12\x16\n" + + "\x06source\x18\x06 \x01(\tR\x06source\x12 \n" + + "\vdestination\x18\a \x01(\tR\vdestination\x12\x16\n" + + "\x06domain\x18\b \x01(\tR\x06domain\x12\x1a\n" + + "\bprotocol\x18\t \x01(\tR\bprotocol\x12\x12\n" + + "\x04user\x18\n" + + " \x01(\tR\x04user\x12\"\n" + + "\ffromOutbound\x18\v \x01(\tR\ffromOutbound\x12\x1c\n" + + "\tcreatedAt\x18\f \x01(\x03R\tcreatedAt\x12\x1a\n" + + "\bclosedAt\x18\r \x01(\x03R\bclosedAt\x12\x16\n" + + "\x06uplink\x18\x0e \x01(\x03R\x06uplink\x12\x1a\n" + + "\bdownlink\x18\x0f \x01(\x03R\bdownlink\x12 \n" + + "\vuplinkTotal\x18\x10 \x01(\x03R\vuplinkTotal\x12$\n" + + "\rdownlinkTotal\x18\x11 \x01(\x03R\rdownlinkTotal\x12\x12\n" + + "\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" + + "\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" + + "\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" + + "\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" + + "\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" + + "\vProcessInfo\x12\x1c\n" + + "\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" + + "\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" + + "\buserName\x18\x03 \x01(\tR\buserName\x12 \n" + + "\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" + + "\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" + + "\x16CloseConnectionRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" + + "\x12DeprecatedWarnings\x125\n" + + "\bwarnings\x18\x01 \x03(\v2\x19.daemon.DeprecatedWarningR\bwarnings\"\xed\x01\n" + + "\x11DeprecatedWarning\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + + "\timpending\x18\x02 \x01(\bR\timpending\x12$\n" + + "\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12,\n" + + "\x11deprecatedVersion\x18\x05 \x01(\tR\x11deprecatedVersion\x12*\n" + + "\x10scheduledVersion\x18\x06 \x01(\tR\x10scheduledVersion\")\n" + + "\tStartedAt\x12\x1c\n" + + "\tstartedAt\x18\x01 \x01(\x03R\tstartedAt\"?\n" + + "\fOutboundList\x12/\n" + + "\toutbounds\x18\x01 \x03(\v2\x11.daemon.GroupItemR\toutbounds\"\xb7\x01\n" + + "\x19NetworkQualityTestRequest\x12\x1c\n" + + "\tconfigURL\x18\x01 \x01(\tR\tconfigURL\x12 \n" + + "\voutboundTag\x18\x02 \x01(\tR\voutboundTag\x12\x16\n" + + "\x06serial\x18\x03 \x01(\bR\x06serial\x12,\n" + + "\x11maxRuntimeSeconds\x18\x04 \x01(\x05R\x11maxRuntimeSeconds\x12\x14\n" + + "\x05http3\x18\x05 \x01(\bR\x05http3\"\x8e\x04\n" + + "\x1aNetworkQualityTestProgress\x12\x14\n" + + "\x05phase\x18\x01 \x01(\x05R\x05phase\x12*\n" + + "\x10downloadCapacity\x18\x02 \x01(\x03R\x10downloadCapacity\x12&\n" + + "\x0euploadCapacity\x18\x03 \x01(\x03R\x0euploadCapacity\x12 \n" + + "\vdownloadRPM\x18\x04 \x01(\x05R\vdownloadRPM\x12\x1c\n" + + "\tuploadRPM\x18\x05 \x01(\x05R\tuploadRPM\x12$\n" + + "\ridleLatencyMs\x18\x06 \x01(\x05R\ridleLatencyMs\x12\x1c\n" + + "\telapsedMs\x18\a \x01(\x03R\telapsedMs\x12\x18\n" + + "\aisFinal\x18\b \x01(\bR\aisFinal\x12\x14\n" + + "\x05error\x18\t \x01(\tR\x05error\x12:\n" + + "\x18downloadCapacityAccuracy\x18\n" + + " \x01(\x05R\x18downloadCapacityAccuracy\x126\n" + + "\x16uploadCapacityAccuracy\x18\v \x01(\x05R\x16uploadCapacityAccuracy\x120\n" + + "\x13downloadRPMAccuracy\x18\f \x01(\x05R\x13downloadRPMAccuracy\x12,\n" + + "\x11uploadRPMAccuracy\x18\r \x01(\x05R\x11uploadRPMAccuracy\"K\n" + + "\x0fSTUNTestRequest\x12\x16\n" + + "\x06server\x18\x01 \x01(\tR\x06server\x12 \n" + + "\voutboundTag\x18\x02 \x01(\tR\voutboundTag\"\x8a\x02\n" + + "\x10STUNTestProgress\x12\x14\n" + + "\x05phase\x18\x01 \x01(\x05R\x05phase\x12\"\n" + + "\fexternalAddr\x18\x02 \x01(\tR\fexternalAddr\x12\x1c\n" + + "\tlatencyMs\x18\x03 \x01(\x05R\tlatencyMs\x12\x1e\n" + + "\n" + + "natMapping\x18\x04 \x01(\x05R\n" + + "natMapping\x12\"\n" + + "\fnatFiltering\x18\x05 \x01(\x05R\fnatFiltering\x12\x18\n" + + "\aisFinal\x18\x06 \x01(\bR\aisFinal\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\x12*\n" + + "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported\"V\n" + + "\x15TailscaleStatusUpdate\x12=\n" + + "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xaa\x02\n" + + "\x17TailscaleEndpointStatus\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\"\n" + + "\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x18\n" + + "\aauthURL\x18\x03 \x01(\tR\aauthURL\x12 \n" + + "\vnetworkName\x18\x04 \x01(\tR\vnetworkName\x12&\n" + + "\x0emagicDNSSuffix\x18\x05 \x01(\tR\x0emagicDNSSuffix\x12)\n" + + "\x04self\x18\x06 \x01(\v2\x15.daemon.TailscalePeerR\x04self\x12:\n" + + "\n" + + "userGroups\x18\a \x03(\v2\x1a.daemon.TailscaleUserGroupR\n" + + "userGroups\"\xbf\x01\n" + + "\x12TailscaleUserGroup\x12\x16\n" + + "\x06userID\x18\x01 \x01(\x03R\x06userID\x12\x1c\n" + + "\tloginName\x18\x02 \x01(\tR\tloginName\x12 \n" + + "\vdisplayName\x18\x03 \x01(\tR\vdisplayName\x12$\n" + + "\rprofilePicURL\x18\x04 \x01(\tR\rprofilePicURL\x12+\n" + + "\x05peers\x18\x05 \x03(\v2\x15.daemon.TailscalePeerR\x05peers\"\xbf\x02\n" + + "\rTailscalePeer\x12\x1a\n" + + "\bhostName\x18\x01 \x01(\tR\bhostName\x12\x18\n" + + "\adnsName\x18\x02 \x01(\tR\adnsName\x12\x0e\n" + + "\x02os\x18\x03 \x01(\tR\x02os\x12\"\n" + + "\ftailscaleIPs\x18\x04 \x03(\tR\ftailscaleIPs\x12\x16\n" + + "\x06online\x18\x05 \x01(\bR\x06online\x12\x1a\n" + + "\bexitNode\x18\x06 \x01(\bR\bexitNode\x12&\n" + + "\x0eexitNodeOption\x18\a \x01(\bR\x0eexitNodeOption\x12\x16\n" + + "\x06active\x18\b \x01(\bR\x06active\x12\x18\n" + + "\arxBytes\x18\t \x01(\x03R\arxBytes\x12\x18\n" + + "\atxBytes\x18\n" + + " \x01(\x03R\atxBytes\x12\x1c\n" + + "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry\"P\n" + + "\x14TailscalePingRequest\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" + + "\x06peerIP\x18\x02 \x01(\tR\x06peerIP\"\xcf\x01\n" + + "\x15TailscalePingResponse\x12\x1c\n" + + "\tlatencyMs\x18\x01 \x01(\x01R\tlatencyMs\x12\x1a\n" + + "\bisDirect\x18\x02 \x01(\bR\bisDirect\x12\x1a\n" + + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\"\n" + + "\fderpRegionID\x18\x04 \x01(\x05R\fderpRegionID\x12&\n" + + "\x0ederpRegionCode\x18\x05 \x01(\tR\x0ederpRegionCode\x12\x14\n" + + "\x05error\x18\x06 \x01(\tR\x05error*U\n" + + "\bLogLevel\x12\t\n" + + "\x05PANIC\x10\x00\x12\t\n" + + "\x05FATAL\x10\x01\x12\t\n" + + "\x05ERROR\x10\x02\x12\b\n" + + "\x04WARN\x10\x03\x12\b\n" + + "\x04INFO\x10\x04\x12\t\n" + + "\x05DEBUG\x10\x05\x12\t\n" + + "\x05TRACE\x10\x06*i\n" + + "\x13ConnectionEventType\x12\x18\n" + + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\x99\x10\n" + + "\x0eStartedService\x12=\n" + + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + + "\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" + + "\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" + + "\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12=\n" + + "\tClearLogs\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12E\n" + + "\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" + + "\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" + + "\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" + + "\x12SubscribeClashMode\x12\x16.google.protobuf.Empty\x1a\x11.daemon.ClashMode\"\x000\x01\x12;\n" + + "\fSetClashMode\x12\x11.daemon.ClashMode\x1a\x16.google.protobuf.Empty\"\x00\x12;\n" + + "\aURLTest\x12\x16.daemon.URLTestRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + + "\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + + "\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" + + "\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" + + "\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n" + + "\x11TriggerDebugCrash\x12\x19.daemon.DebugCrashRequest\x1a\x16.google.protobuf.Empty\"\x00\x12D\n" + + "\x10TriggerOOMReport\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" + + "\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\x000\x01\x12K\n" + + "\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" + + "\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" + + "\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" + + "\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12F\n" + + "\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" + + "\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" + + "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01\x12U\n" + + "\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01\x12U\n" + + "\x12StartTailscalePing\x12\x1c.daemon.TailscalePingRequest\x1a\x1d.daemon.TailscalePingResponse\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + +var ( + file_daemon_started_service_proto_rawDescOnce sync.Once + file_daemon_started_service_proto_rawDescData []byte +) + +func file_daemon_started_service_proto_rawDescGZIP() []byte { + file_daemon_started_service_proto_rawDescOnce.Do(func() { + file_daemon_started_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc))) + }) + return file_daemon_started_service_proto_rawDescData +} + +var ( + file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38) + file_daemon_started_service_proto_goTypes = []any{ + (LogLevel)(0), // 0: daemon.LogLevel + (ConnectionEventType)(0), // 1: daemon.ConnectionEventType + (ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type + (DebugCrashRequest_Type)(0), // 3: daemon.DebugCrashRequest.Type + (*ServiceStatus)(nil), // 4: daemon.ServiceStatus + (*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest + (*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest + (*Log)(nil), // 7: daemon.Log + (*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel + (*Status)(nil), // 9: daemon.Status + (*Groups)(nil), // 10: daemon.Groups + (*Group)(nil), // 11: daemon.Group + (*GroupItem)(nil), // 12: daemon.GroupItem + (*URLTestRequest)(nil), // 13: daemon.URLTestRequest + (*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest + (*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest + (*ClashMode)(nil), // 16: daemon.ClashMode + (*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus + (*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus + (*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest + (*DebugCrashRequest)(nil), // 20: daemon.DebugCrashRequest + (*SubscribeConnectionsRequest)(nil), // 21: daemon.SubscribeConnectionsRequest + (*ConnectionEvent)(nil), // 22: daemon.ConnectionEvent + (*ConnectionEvents)(nil), // 23: daemon.ConnectionEvents + (*Connection)(nil), // 24: daemon.Connection + (*ProcessInfo)(nil), // 25: daemon.ProcessInfo + (*CloseConnectionRequest)(nil), // 26: daemon.CloseConnectionRequest + (*DeprecatedWarnings)(nil), // 27: daemon.DeprecatedWarnings + (*DeprecatedWarning)(nil), // 28: daemon.DeprecatedWarning + (*StartedAt)(nil), // 29: daemon.StartedAt + (*OutboundList)(nil), // 30: daemon.OutboundList + (*NetworkQualityTestRequest)(nil), // 31: daemon.NetworkQualityTestRequest + (*NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress + (*STUNTestRequest)(nil), // 33: daemon.STUNTestRequest + (*STUNTestProgress)(nil), // 34: daemon.STUNTestProgress + (*TailscaleStatusUpdate)(nil), // 35: daemon.TailscaleStatusUpdate + (*TailscaleEndpointStatus)(nil), // 36: daemon.TailscaleEndpointStatus + (*TailscaleUserGroup)(nil), // 37: daemon.TailscaleUserGroup + (*TailscalePeer)(nil), // 38: daemon.TailscalePeer + (*TailscalePingRequest)(nil), // 39: daemon.TailscalePingRequest + (*TailscalePingResponse)(nil), // 40: daemon.TailscalePingResponse + (*Log_Message)(nil), // 41: daemon.Log.Message + (*emptypb.Empty)(nil), // 42: google.protobuf.Empty + } +) + +var file_daemon_started_service_proto_depIdxs = []int32{ + 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type + 41, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel + 11, // 3: daemon.Groups.group:type_name -> daemon.Group + 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem + 3, // 5: daemon.DebugCrashRequest.type:type_name -> daemon.DebugCrashRequest.Type + 1, // 6: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType + 24, // 7: daemon.ConnectionEvent.connection:type_name -> daemon.Connection + 22, // 8: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent + 25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo + 28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning + 12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem + 36, // 12: daemon.TailscaleStatusUpdate.endpoints:type_name -> daemon.TailscaleEndpointStatus + 38, // 13: daemon.TailscaleEndpointStatus.self:type_name -> daemon.TailscalePeer + 37, // 14: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup + 38, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer + 0, // 16: daemon.Log.Message.level:type_name -> daemon.LogLevel + 42, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 42, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 42, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 42, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 42, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 42, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 6, // 23: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 42, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 42, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 42, // 26: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 16, // 27: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 13, // 28: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 14, // 29: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 15, // 30: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 42, // 31: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 19, // 32: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest + 20, // 33: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest + 42, // 34: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 21, // 35: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 26, // 36: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 42, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 42, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 42, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 42, // 40: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 31, // 41: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 33, // 42: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 42, // 43: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 39, // 44: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest + 42, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 42, // 46: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 4, // 47: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 7, // 48: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 8, // 49: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 42, // 50: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 9, // 51: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 10, // 52: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 17, // 53: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 16, // 54: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 42, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 42, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 42, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 42, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 18, // 59: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus + 42, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 42, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 42, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 23, // 63: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 42, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 42, // 65: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 27, // 66: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 29, // 67: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 30, // 68: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 32, // 69: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 34, // 70: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 35, // 71: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 40, // 72: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse + 45, // [45:73] is the sub-list for method output_type + 17, // [17:45] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name +} + +func init() { file_daemon_started_service_proto_init() } +func file_daemon_started_service_proto_init() { + if File_daemon_started_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), + NumEnums: 4, + NumMessages: 38, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_daemon_started_service_proto_goTypes, + DependencyIndexes: file_daemon_started_service_proto_depIdxs, + EnumInfos: file_daemon_started_service_proto_enumTypes, + MessageInfos: file_daemon_started_service_proto_msgTypes, + }.Build() + File_daemon_started_service_proto = out.File + file_daemon_started_service_proto_goTypes = nil + file_daemon_started_service_proto_depIdxs = nil +} diff --git a/daemon/started_service.proto b/daemon/started_service.proto new file mode 100644 index 00000000..2c3140a9 --- /dev/null +++ b/daemon/started_service.proto @@ -0,0 +1,331 @@ +syntax = "proto3"; + +package daemon; +option go_package = "github.com/sagernet/sing-box/daemon"; + +import "google/protobuf/empty.proto"; + +service StartedService { + rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ReloadService(google.protobuf.Empty) returns (google.protobuf.Empty); + + rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {} + rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {} + rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {} + rpc ClearLogs(google.protobuf.Empty) returns(google.protobuf.Empty) {} + rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {} + rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {} + + rpc GetClashModeStatus(google.protobuf.Empty) returns(ClashModeStatus) {} + rpc SubscribeClashMode(google.protobuf.Empty) returns(stream ClashMode) {} + rpc SetClashMode(ClashMode) returns(google.protobuf.Empty) {} + + rpc URLTest(URLTestRequest) returns(google.protobuf.Empty) {} + rpc SelectOutbound(SelectOutboundRequest) returns (google.protobuf.Empty) {} + rpc SetGroupExpand(SetGroupExpandRequest) returns (google.protobuf.Empty) {} + + rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {} + rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {} + rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {} + rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {} + + rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {} + rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {} + rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {} + rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {} + rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {} + + rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {} + rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {} + rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {} + rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {} + rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {} +} + +message ServiceStatus { + enum Type { + IDLE = 0; + STARTING = 1; + STARTED = 2; + STOPPING = 3; + FATAL = 4; + } + Type status = 1; + string errorMessage = 2; +} + +message ReloadServiceRequest { + string newProfileContent = 1; +} + +message SubscribeStatusRequest { + int64 interval = 1; +} + +enum LogLevel { + PANIC = 0; + FATAL = 1; + ERROR = 2; + WARN = 3; + INFO = 4; + DEBUG = 5; + TRACE = 6; +} + +message Log { + repeated Message messages = 1; + bool reset = 2; + message Message { + LogLevel level = 1; + string message = 2; + } +} + +message DefaultLogLevel { + LogLevel level = 1; +} + +message Status { + uint64 memory = 1; + int32 goroutines = 2; + int32 connectionsIn = 3; + int32 connectionsOut = 4; + bool trafficAvailable = 5; + int64 uplink = 6; + int64 downlink = 7; + int64 uplinkTotal = 8; + int64 downlinkTotal = 9; +} + +message Groups { + repeated Group group = 1; +} + +message Group { + string tag = 1; + string type = 2; + bool selectable = 3; + string selected = 4; + bool isExpand = 5; + repeated GroupItem items = 6; +} + +message GroupItem { + string tag = 1; + string type = 2; + int64 urlTestTime = 3; + int32 urlTestDelay = 4; +} + +message URLTestRequest { + string outboundTag = 1; +} + +message SelectOutboundRequest { + string groupTag = 1; + string outboundTag = 2; +} + +message SetGroupExpandRequest { + string groupTag = 1; + bool isExpand = 2; +} + +message ClashMode { + string mode = 3; +} + +message ClashModeStatus { + repeated string modeList = 1; + string currentMode = 2; +} + +message SystemProxyStatus { + bool available = 1; + bool enabled = 2; +} + +message SetSystemProxyEnabledRequest { + bool enabled = 1; +} + +message DebugCrashRequest { + enum Type { + GO = 0; + NATIVE = 1; + } + + Type type = 1; +} + +message SubscribeConnectionsRequest { + int64 interval = 1; +} + +enum ConnectionEventType { + CONNECTION_EVENT_NEW = 0; + CONNECTION_EVENT_UPDATE = 1; + CONNECTION_EVENT_CLOSED = 2; +} + +message ConnectionEvent { + ConnectionEventType type = 1; + string id = 2; + Connection connection = 3; + int64 uplinkDelta = 4; + int64 downlinkDelta = 5; + int64 closedAt = 6; +} + +message ConnectionEvents { + repeated ConnectionEvent events = 1; + bool reset = 2; +} + +message Connection { + string id = 1; + string inbound = 2; + string inboundType = 3; + int32 ipVersion = 4; + string network = 5; + string source = 6; + string destination = 7; + string domain = 8; + string protocol = 9; + string user = 10; + string fromOutbound = 11; + int64 createdAt = 12; + int64 closedAt = 13; + int64 uplink = 14; + int64 downlink = 15; + int64 uplinkTotal = 16; + int64 downlinkTotal = 17; + string rule = 18; + string outbound = 19; + string outboundType = 20; + repeated string chainList = 21; + ProcessInfo processInfo = 22; +} + +message ProcessInfo { + uint32 processId = 1; + int32 userId = 2; + string userName = 3; + string processPath = 4; + repeated string packageNames = 5; +} + +message CloseConnectionRequest { + string id = 1; +} + +message DeprecatedWarnings { + repeated DeprecatedWarning warnings = 1; +} + +message DeprecatedWarning { + string message = 1; + bool impending = 2; + string migrationLink = 3; + string description = 4; + string deprecatedVersion = 5; + string scheduledVersion = 6; +} + +message StartedAt { + int64 startedAt = 1; +} + +message OutboundList { + repeated GroupItem outbounds = 1; +} + +message NetworkQualityTestRequest { + string configURL = 1; + string outboundTag = 2; + bool serial = 3; + int32 maxRuntimeSeconds = 4; + bool http3 = 5; +} + +message NetworkQualityTestProgress { + int32 phase = 1; + int64 downloadCapacity = 2; + int64 uploadCapacity = 3; + int32 downloadRPM = 4; + int32 uploadRPM = 5; + int32 idleLatencyMs = 6; + int64 elapsedMs = 7; + bool isFinal = 8; + string error = 9; + int32 downloadCapacityAccuracy = 10; + int32 uploadCapacityAccuracy = 11; + int32 downloadRPMAccuracy = 12; + int32 uploadRPMAccuracy = 13; +} + +message STUNTestRequest { + string server = 1; + string outboundTag = 2; +} + +message STUNTestProgress { + int32 phase = 1; + string externalAddr = 2; + int32 latencyMs = 3; + int32 natMapping = 4; + int32 natFiltering = 5; + bool isFinal = 6; + string error = 7; + bool natTypeSupported = 8; +} + +message TailscaleStatusUpdate { + repeated TailscaleEndpointStatus endpoints = 1; +} + +message TailscaleEndpointStatus { + string endpointTag = 1; + string backendState = 2; + string authURL = 3; + string networkName = 4; + string magicDNSSuffix = 5; + TailscalePeer self = 6; + repeated TailscaleUserGroup userGroups = 7; +} + +message TailscaleUserGroup { + int64 userID = 1; + string loginName = 2; + string displayName = 3; + string profilePicURL = 4; + repeated TailscalePeer peers = 5; +} + +message TailscalePeer { + string hostName = 1; + string dnsName = 2; + string os = 3; + repeated string tailscaleIPs = 4; + bool online = 5; + bool exitNode = 6; + bool exitNodeOption = 7; + bool active = 8; + int64 rxBytes = 9; + int64 txBytes = 10; + int64 keyExpiry = 11; +} + +message TailscalePingRequest { + string endpointTag = 1; + string peerIP = 2; +} + +message TailscalePingResponse { + double latencyMs = 1; + bool isDirect = 2; + string endpoint = 3; + int32 derpRegionID = 4; + string derpRegionCode = 5; + string error = 6; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go new file mode 100644 index 00000000..967757f1 --- /dev/null +++ b/daemon/started_service_grpc.pb.go @@ -0,0 +1,1204 @@ +package daemon + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" + StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" + StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" + StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" + StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" + StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" + StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" + StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" + StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" + StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" + StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" + StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" + StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" + StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" + StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" + StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" + StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" + StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" + StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" + StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" + StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" + StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" + StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" + StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" + StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" + StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" + StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" + StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing" +) + +// StartedServiceClient is the client API for StartedService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StartedServiceClient interface { + StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) + SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) + GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) + ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) + SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) + GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) + SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error) + SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error) + URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) + SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) + CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) + GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) + SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) + StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) + StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) + SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) + StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) +} + +type startedServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStartedServiceClient(cc grpc.ClientConnInterface) StartedServiceClient { + return &startedServiceClient{cc} +} + +func (c *startedServiceClient) StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_StopService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_ReloadService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[0], StartedService_SubscribeServiceStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, ServiceStatus]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeServiceStatusClient = grpc.ServerStreamingClient[ServiceStatus] + +func (c *startedServiceClient) SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[1], StartedService_SubscribeLog_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, Log]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeLogClient = grpc.ServerStreamingClient[Log] + +func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DefaultLogLevel) + err := c.cc.Invoke(ctx, StartedService_GetDefaultLogLevel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_ClearLogs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SubscribeStatusRequest, Status]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeStatusClient = grpc.ServerStreamingClient[Status] + +func (c *startedServiceClient) SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[3], StartedService_SubscribeGroups_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, Groups]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeGroupsClient = grpc.ServerStreamingClient[Groups] + +func (c *startedServiceClient) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClashModeStatus) + err := c.cc.Invoke(ctx, StartedService_GetClashModeStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[4], StartedService_SubscribeClashMode_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, ClashMode]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeClashModeClient = grpc.ServerStreamingClient[ClashMode] + +func (c *startedServiceClient) SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SetClashMode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_URLTest_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SelectOutbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SetGroupExpand_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SystemProxyStatus) + err := c.cc.Invoke(ctx, StartedService_GetSystemProxyStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SetSystemProxyEnabled_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[ConnectionEvents] + +func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_CloseConnection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_CloseAllConnections_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeprecatedWarnings) + err := c.cc.Invoke(ctx, StartedService_GetDeprecatedWarnings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StartedAt) + err := c.cc.Invoke(ctx, StartedService_GetStartedAt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList] + +func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress] + +func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress] + +func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate] + +func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse] + +// StartedServiceServer is the server API for StartedService service. +// All implementations must embed UnimplementedStartedServiceServer +// for forward compatibility. +type StartedServiceServer interface { + StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error + SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error + GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) + ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error + SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error + GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) + SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error + SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) + URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) + SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) + SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) + GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) + SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) + TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) + TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error + CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) + CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) + GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) + SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error + StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error + StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error + SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error + StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error + mustEmbedUnimplementedStartedServiceServer() +} + +// UnimplementedStartedServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedStartedServiceServer struct{} + +func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method StopService not implemented") +} + +func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error { + return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error { + return status.Error(codes.Unimplemented, "method SubscribeLog not implemented") +} + +func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) { + return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented") +} + +func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method ClearLogs not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error { + return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error { + return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented") +} + +func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) { + return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error { + return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented") +} + +func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented") +} + +func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method URLTest not implemented") +} + +func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented") +} + +func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented") +} + +func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) { + return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented") +} + +func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented") +} + +func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented") +} + +func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error { + return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented") +} + +func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented") +} + +func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented") +} + +func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) { + return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented") +} + +func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) { + return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error { + return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented") +} + +func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error { + return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented") +} + +func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error { + return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error { + return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented") +} + +func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error { + return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented") +} +func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} +func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} + +// UnsafeStartedServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StartedServiceServer will +// result in compilation errors. +type UnsafeStartedServiceServer interface { + mustEmbedUnimplementedStartedServiceServer() +} + +func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) { + // If the following call panics, it indicates UnimplementedStartedServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&StartedService_ServiceDesc, srv) +} + +func _StartedService_StopService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).StopService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_StopService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).StopService(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_ReloadService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).ReloadService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_ReloadService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).ReloadService(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeServiceStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeServiceStatus(m, &grpc.GenericServerStream[emptypb.Empty, ServiceStatus]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeServiceStatusServer = grpc.ServerStreamingServer[ServiceStatus] + +func _StartedService_SubscribeLog_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeLog(m, &grpc.GenericServerStream[emptypb.Empty, Log]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeLogServer = grpc.ServerStreamingServer[Log] + +func _StartedService_GetDefaultLogLevel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).GetDefaultLogLevel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_GetDefaultLogLevel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).GetDefaultLogLevel(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_ClearLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).ClearLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_ClearLogs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).ClearLogs(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeStatusRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeStatus(m, &grpc.GenericServerStream[SubscribeStatusRequest, Status]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeStatusServer = grpc.ServerStreamingServer[Status] + +func _StartedService_SubscribeGroups_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeGroups(m, &grpc.GenericServerStream[emptypb.Empty, Groups]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeGroupsServer = grpc.ServerStreamingServer[Groups] + +func _StartedService_GetClashModeStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).GetClashModeStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_GetClashModeStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).GetClashModeStatus(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeClashMode_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeClashMode(m, &grpc.GenericServerStream[emptypb.Empty, ClashMode]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeClashModeServer = grpc.ServerStreamingServer[ClashMode] + +func _StartedService_SetClashMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClashMode) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SetClashMode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SetClashMode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SetClashMode(ctx, req.(*ClashMode)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_URLTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(URLTestRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).URLTest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_URLTest_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).URLTest(ctx, req.(*URLTestRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SelectOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SelectOutboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SelectOutbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SelectOutbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SelectOutbound(ctx, req.(*SelectOutboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SetGroupExpand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetGroupExpandRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SetGroupExpand(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SetGroupExpand_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SetGroupExpand(ctx, req.(*SetGroupExpandRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_GetSystemProxyStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).GetSystemProxyStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_GetSystemProxyStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).GetSystemProxyStatus(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetSystemProxyEnabledRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SetSystemProxyEnabled_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, req.(*SetSystemProxyEnabledRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DebugCrashRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_TriggerDebugCrash_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).TriggerOOMReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_TriggerOOMReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeConnectionsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[ConnectionEvents] + +func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseConnectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).CloseConnection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_CloseConnection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).CloseConnection(ctx, req.(*CloseConnectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_CloseAllConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).CloseAllConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_CloseAllConnections_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).CloseAllConnections(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_GetDeprecatedWarnings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).GetDeprecatedWarnings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_GetDeprecatedWarnings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).GetDeprecatedWarnings(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).GetStartedAt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_GetStartedAt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).GetStartedAt(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList] + +func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(NetworkQualityTestRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress] + +func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(STUNTestRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress] + +func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate] + +func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(TailscalePingRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse] + +// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StartedService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "daemon.StartedService", + HandlerType: (*StartedServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "StopService", + Handler: _StartedService_StopService_Handler, + }, + { + MethodName: "ReloadService", + Handler: _StartedService_ReloadService_Handler, + }, + { + MethodName: "GetDefaultLogLevel", + Handler: _StartedService_GetDefaultLogLevel_Handler, + }, + { + MethodName: "ClearLogs", + Handler: _StartedService_ClearLogs_Handler, + }, + { + MethodName: "GetClashModeStatus", + Handler: _StartedService_GetClashModeStatus_Handler, + }, + { + MethodName: "SetClashMode", + Handler: _StartedService_SetClashMode_Handler, + }, + { + MethodName: "URLTest", + Handler: _StartedService_URLTest_Handler, + }, + { + MethodName: "SelectOutbound", + Handler: _StartedService_SelectOutbound_Handler, + }, + { + MethodName: "SetGroupExpand", + Handler: _StartedService_SetGroupExpand_Handler, + }, + { + MethodName: "GetSystemProxyStatus", + Handler: _StartedService_GetSystemProxyStatus_Handler, + }, + { + MethodName: "SetSystemProxyEnabled", + Handler: _StartedService_SetSystemProxyEnabled_Handler, + }, + { + MethodName: "TriggerDebugCrash", + Handler: _StartedService_TriggerDebugCrash_Handler, + }, + { + MethodName: "TriggerOOMReport", + Handler: _StartedService_TriggerOOMReport_Handler, + }, + { + MethodName: "CloseConnection", + Handler: _StartedService_CloseConnection_Handler, + }, + { + MethodName: "CloseAllConnections", + Handler: _StartedService_CloseAllConnections_Handler, + }, + { + MethodName: "GetDeprecatedWarnings", + Handler: _StartedService_GetDeprecatedWarnings_Handler, + }, + { + MethodName: "GetStartedAt", + Handler: _StartedService_GetStartedAt_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SubscribeServiceStatus", + Handler: _StartedService_SubscribeServiceStatus_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeLog", + Handler: _StartedService_SubscribeLog_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeStatus", + Handler: _StartedService_SubscribeStatus_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeGroups", + Handler: _StartedService_SubscribeGroups_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeClashMode", + Handler: _StartedService_SubscribeClashMode_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeConnections", + Handler: _StartedService_SubscribeConnections_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeOutbounds", + Handler: _StartedService_SubscribeOutbounds_Handler, + ServerStreams: true, + }, + { + StreamName: "StartNetworkQualityTest", + Handler: _StartedService_StartNetworkQualityTest_Handler, + ServerStreams: true, + }, + { + StreamName: "StartSTUNTest", + Handler: _StartedService_StartSTUNTest_Handler, + ServerStreams: true, + }, + { + StreamName: "SubscribeTailscaleStatus", + Handler: _StartedService_SubscribeTailscaleStatus_Handler, + ServerStreams: true, + }, + { + StreamName: "StartTailscalePing", + Handler: _StartedService_StartTailscalePing_Handler, + ServerStreams: true, + }, + }, + Metadata: "daemon/started_service.proto", +} diff --git a/debug.go b/debug.go new file mode 100644 index 00000000..f620172b --- /dev/null +++ b/debug.go @@ -0,0 +1,34 @@ +package box + +import ( + "runtime/debug" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func applyDebugOptions(options option.DebugOptions) error { + applyDebugListenOption(options) + if options.GCPercent != nil { + debug.SetGCPercent(*options.GCPercent) + } + if options.MaxStack != nil { + debug.SetMaxStack(*options.MaxStack) + } + if options.MaxThreads != nil { + debug.SetMaxThreads(*options.MaxThreads) + } + if options.PanicOnFault != nil { + debug.SetPanicOnFault(*options.PanicOnFault) + } + if options.TraceBack != "" { + debug.SetTraceback(options.TraceBack) + } + if options.MemoryLimit.Value() != 0 { + debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5)) + } + if options.OOMKiller != nil { + return E.New("legacy oom_killer in debug options is removed, use oom-killer service instead") + } + return nil +} diff --git a/debug_http.go b/debug_http.go new file mode 100644 index 00000000..e51a0731 --- /dev/null +++ b/debug_http.go @@ -0,0 +1,76 @@ +package box + +import ( + "net/http" + "net/http/pprof" + "runtime" + "runtime/debug" + "strings" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + + "github.com/go-chi/chi/v5" +) + +var debugHTTPServer *http.Server + +func applyDebugListenOption(options option.DebugOptions) { + if debugHTTPServer != nil { + debugHTTPServer.Close() + debugHTTPServer = nil + } + if options.Listen == "" { + return + } + r := chi.NewMux() + r.Route("/debug", func(r chi.Router) { + r.Get("/gc", func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusNoContent) + go debug.FreeOSMemory() + }) + r.Get("/memory", func(writer http.ResponseWriter, request *http.Request) { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + + var memObject badjson.JSONObject + memObject.Put("heap", byteformats.FormatMemoryBytes(memStats.HeapInuse)) + memObject.Put("stack", byteformats.FormatMemoryBytes(memStats.StackInuse)) + memObject.Put("idle", byteformats.FormatMemoryBytes(memStats.HeapIdle-memStats.HeapReleased)) + memObject.Put("goroutines", runtime.NumGoroutine()) + memObject.Put("rss", rusageMaxRSS()) + + encoder := json.NewEncoder(writer) + encoder.SetIndent("", " ") + encoder.Encode(&memObject) + }) + r.Route("/pprof", func(r chi.Router) { + r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + if !strings.HasSuffix(request.URL.Path, "/") { + http.Redirect(writer, request, request.URL.Path+"/", http.StatusMovedPermanently) + } else { + pprof.Index(writer, request) + } + }) + r.HandleFunc("/*", pprof.Index) + r.HandleFunc("/cmdline", pprof.Cmdline) + r.HandleFunc("/profile", pprof.Profile) + r.HandleFunc("/symbol", pprof.Symbol) + r.HandleFunc("/trace", pprof.Trace) + }) + }) + debugHTTPServer = &http.Server{ + Addr: options.Listen, + Handler: r, + } + go func() { + err := debugHTTPServer.ListenAndServe() + if err != nil && !E.IsClosed(err) { + log.Error(E.Cause(err, "serve debug HTTP server")) + } + }() +} diff --git a/debug_stub.go b/debug_stub.go new file mode 100644 index 00000000..a8988c20 --- /dev/null +++ b/debug_stub.go @@ -0,0 +1,7 @@ +//go:build !(linux || darwin) + +package box + +func rusageMaxRSS() float64 { + return -1 +} diff --git a/debug_unix.go b/debug_unix.go new file mode 100644 index 00000000..3be097e9 --- /dev/null +++ b/debug_unix.go @@ -0,0 +1,25 @@ +//go:build linux || darwin + +package box + +import ( + "runtime" + "syscall" +) + +func rusageMaxRSS() float64 { + ru := syscall.Rusage{} + err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru) + if err != nil { + return 0 + } + + rss := float64(ru.Maxrss) + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + rss /= 1 << 20 // ru_maxrss is bytes on darwin + } else { + // ru_maxrss is kilobytes elsewhere (linux, openbsd, etc) + rss /= 1 << 10 + } + return rss +} diff --git a/dns/client.go b/dns/client.go new file mode 100644 index 00000000..37ba98a8 --- /dev/null +++ b/dns/client.go @@ -0,0 +1,709 @@ +package dns + +import ( + "context" + "errors" + "net" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/compatible" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/task" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" + + "github.com/miekg/dns" +) + +var ( + ErrNoRawSupport = E.New("no raw query support by current transport") + ErrNotCached = E.New("not cached") + ErrResponseRejected = E.New("response rejected") + ErrResponseRejectedCached = E.Extend(ErrResponseRejected, "cached") +) + +var _ adapter.DNSClient = (*Client)(nil) + +type Client struct { + ctx context.Context + timeout time.Duration + disableCache bool + disableExpire bool + optimisticTimeout time.Duration + cacheCapacity uint32 + clientSubnet netip.Prefix + rdrc adapter.RDRCStore + initRDRCFunc func() adapter.RDRCStore + dnsCache adapter.DNSCacheStore + initDNSCacheFunc func() adapter.DNSCacheStore + logger logger.ContextLogger + cache freelru.Cache[dnsCacheKey, *dns.Msg] + cacheLock compatible.Map[dnsCacheKey, chan struct{}] + backgroundRefresh compatible.Map[dnsCacheKey, struct{}] +} + +type ClientOptions struct { + Context context.Context + Timeout time.Duration + DisableCache bool + DisableExpire bool + OptimisticTimeout time.Duration + CacheCapacity uint32 + ClientSubnet netip.Prefix + RDRC func() adapter.RDRCStore + DNSCache func() adapter.DNSCacheStore + Logger logger.ContextLogger +} + +func NewClient(options ClientOptions) *Client { + cacheCapacity := options.CacheCapacity + if cacheCapacity < 1024 { + cacheCapacity = 1024 + } + client := &Client{ + ctx: options.Context, + timeout: options.Timeout, + disableCache: options.DisableCache, + disableExpire: options.DisableExpire, + optimisticTimeout: options.OptimisticTimeout, + cacheCapacity: cacheCapacity, + clientSubnet: options.ClientSubnet, + initRDRCFunc: options.RDRC, + initDNSCacheFunc: options.DNSCache, + logger: options.Logger, + } + if client.timeout == 0 { + client.timeout = C.DNSTimeout + } + if !client.disableCache && client.initDNSCacheFunc == nil { + client.initializeMemoryCache() + } + return client +} + +type dnsCacheKey struct { + dns.Question + transportTag string +} + +func (c *Client) Start() { + if c.initRDRCFunc != nil { + c.rdrc = c.initRDRCFunc() + } + if c.initDNSCacheFunc != nil { + c.dnsCache = c.initDNSCacheFunc() + } + if c.dnsCache == nil { + c.initializeMemoryCache() + } +} + +func (c *Client) initializeMemoryCache() { + if c.disableCache || c.cache != nil { + return + } + c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32)) +} + +func extractNegativeTTL(response *dns.Msg) (uint32, bool) { + for _, record := range response.Ns { + if soa, isSOA := record.(*dns.SOA); isSOA { + soaTTL := soa.Header().Ttl + soaMinimum := soa.Minttl + if soaTTL < soaMinimum { + return soaTTL, true + } + return soaMinimum, true + } + } + return 0, false +} + +func computeTimeToLive(response *dns.Msg) uint32 { + var timeToLive uint32 + if len(response.Answer) == 0 { + if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { + return soaTTL + } + } + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } + if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { + timeToLive = record.Header().Ttl + } + } + } + return timeToLive +} + +func normalizeTTL(response *dns.Msg, timeToLive uint32) { + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } + record.Header().Ttl = timeToLive + } + } +} + +func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) { + if len(message.Question) == 0 { + if c.logger != nil { + c.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) + } + return FixedResponseStatus(message, dns.RcodeFormatError), nil + } + question := message.Question[0] + if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only { + if c.logger != nil { + c.logger.DebugContext(ctx, "strategy rejected") + } + return FixedResponseStatus(message, dns.RcodeSuccess), nil + } + message = c.prepareExchangeMessage(message, options) + + isSimpleRequest := len(message.Question) == 1 && + len(message.Ns) == 0 && + (len(message.Extra) == 0 || len(message.Extra) == 1 && + message.Extra[0].Header().Rrtype == dns.TypeOPT && + message.Extra[0].Header().Class > 0 && + message.Extra[0].Header().Ttl == 0 && + len(message.Extra[0].(*dns.OPT).Option) == 0) && + !options.ClientSubnet.IsValid() + disableCache := !isSimpleRequest || c.disableCache || options.DisableCache + if !disableCache { + cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()} + cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{})) + if loaded { + select { + case <-cond: + case <-ctx.Done(): + return nil, ctx.Err() + } + } else { + defer func() { + c.cacheLock.Delete(cacheKey) + close(cond) + }() + } + response, ttl, isStale := c.loadResponse(question, transport) + if response != nil { + if isStale && !options.DisableOptimisticCache { + c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker) + logOptimisticResponse(c.logger, ctx, response) + response.Id = message.Id + return response, nil + } else if !isStale { + logCachedResponse(c.logger, ctx, response, ttl) + response.Id = message.Id + return response, nil + } + } + } + + messageId := message.Id + contextTransport, clientSubnetLoaded := transportTagFromContext(ctx) + if clientSubnetLoaded && transport.Tag() == contextTransport { + return nil, E.New("DNS query loopback in transport[", contextTransport, "]") + } + ctx = contextWithTransportTag(ctx, transport.Tag()) + if !disableCache && responseChecker != nil && c.rdrc != nil { + rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype) + if rejected { + return nil, ErrResponseRejectedCached + } + } + response, err := c.exchangeToTransport(ctx, transport, message) + if err != nil { + return nil, err + } + disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) + if responseChecker != nil { + var rejected bool + if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + rejected = true + } else { + rejected = !responseChecker(response) + } + if rejected { + if !disableCache && c.rdrc != nil { + c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) + } + logRejectedResponse(c.logger, ctx, response) + return response, ErrResponseRejected + } + } + timeToLive := applyResponseOptions(question, response, options) + if !disableCache { + c.storeCache(transport, question, response, timeToLive) + } + response.Id = messageId + requestEDNSOpt := message.IsEdns0() + responseEDNSOpt := response.IsEdns0() + if responseEDNSOpt != nil && (requestEDNSOpt == nil || requestEDNSOpt.Version() < responseEDNSOpt.Version()) { + response.Extra = common.Filter(response.Extra, func(it dns.RR) bool { + return it.Header().Rrtype != dns.TypeOPT + }) + if requestEDNSOpt != nil { + response.SetEdns0(responseEDNSOpt.UDPSize(), responseEDNSOpt.Do()) + } + } + logExchangedResponse(c.logger, ctx, response, timeToLive) + return response, nil +} + +func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { + domain = FqdnToDomain(domain) + dnsName := dns.Fqdn(domain) + var strategy C.DomainStrategy + if options.LookupStrategy != C.DomainStrategyAsIS { + strategy = options.LookupStrategy + } else { + strategy = options.Strategy + } + lookupOptions := options + if options.LookupStrategy != C.DomainStrategyAsIS { + lookupOptions.Strategy = strategy + } + if strategy == C.DomainStrategyIPv4Only { + return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) + } else if strategy == C.DomainStrategyIPv6Only { + return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) + } + var response4 []netip.Addr + var response6 []netip.Addr + var group task.Group + group.Append("exchange4", func(ctx context.Context) error { + response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) + if err != nil { + return err + } + response4 = response + return nil + }) + group.Append("exchange6", func(ctx context.Context) error { + response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) + if err != nil { + return err + } + response6 = response + return nil + }) + err := group.Run(ctx) + if len(response4) == 0 && len(response6) == 0 { + return nil, err + } + return sortAddresses(response4, response6, strategy), nil +} + +func (c *Client) ClearCache() { + if c.cache != nil { + c.cache.Purge() + } + if c.dnsCache != nil { + err := c.dnsCache.ClearDNSCache() + if err != nil && c.logger != nil { + c.logger.Warn("clear DNS cache: ", err) + } + } +} + +func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr { + if strategy == C.DomainStrategyPreferIPv6 { + return append(response6, response4...) + } else { + return append(response4, response6...) + } +} + +func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, timeToLive uint32) { + if timeToLive == 0 { + return + } + if c.dnsCache != nil { + packed, err := message.Pack() + if err == nil { + expireAt := time.Now().Add(time.Second * time.Duration(timeToLive)) + c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger) + } + return + } + if c.cache == nil { + return + } + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + if c.disableExpire { + c.cache.Add(key, message.Copy()) + } else { + c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive)) + } +} + +func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { + question := dns.Question{ + Name: name, + Qtype: qType, + Qclass: dns.ClassINET, + } + message := dns.Msg{ + MsgHdr: dns.MsgHdr{ + RecursionDesired: true, + }, + Question: []dns.Question{question}, + } + disableCache := c.disableCache || options.DisableCache + if !disableCache { + cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker) + if err != ErrNotCached { + return cachedAddresses, err + } + } + response, err := c.Exchange(ctx, transport, &message, options, responseChecker) + if err != nil { + return nil, err + } + if response.Rcode != dns.RcodeSuccess { + return nil, RcodeError(response.Rcode) + } + return MessageToAddresses(response), nil +} + +func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { + question := message.Question[0] + response, _, isStale := c.loadResponse(question, transport) + if response == nil { + return nil, ErrNotCached + } + if isStale { + if options.DisableOptimisticCache { + return nil, ErrNotCached + } + c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker) + logOptimisticResponse(c.logger, ctx, response) + } + if response.Rcode != dns.RcodeSuccess { + return nil, RcodeError(response.Rcode) + } + return MessageToAddresses(response), nil +} + +func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) { + if c.dnsCache != nil { + return c.loadPersistentResponse(question, transport) + } + if c.cache == nil { + return nil, 0, false + } + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + if c.disableExpire { + response, loaded := c.cache.Get(key) + if !loaded { + return nil, 0, false + } + return response.Copy(), 0, false + } + response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key) + if !loaded { + return nil, 0, false + } + timeNow := time.Now() + if timeNow.After(expireAt) { + if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) { + response = response.Copy() + normalizeTTL(response, 1) + return response, 0, true + } + c.cache.Remove(key) + return nil, 0, false + } + nowTTL := int(expireAt.Sub(timeNow).Seconds()) + if nowTTL < 0 { + nowTTL = 0 + } + response = response.Copy() + normalizeTTL(response, uint32(nowTTL)) + return response, nowTTL, false +} + +func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) { + rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype) + if !loaded { + return nil, 0, false + } + response := new(dns.Msg) + err := response.Unpack(rawMessage) + if err != nil { + return nil, 0, false + } + if c.disableExpire { + return response, 0, false + } + timeNow := time.Now() + if timeNow.After(expireAt) { + if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) { + normalizeTTL(response, 1) + return response, 0, true + } + return nil, 0, false + } + nowTTL := int(expireAt.Sub(timeNow).Seconds()) + if nowTTL < 0 { + nowTTL = 0 + } + normalizeTTL(response, uint32(nowTTL)) + return response, nowTTL, false +} + +func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 { + if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) { + for _, rr := range response.Answer { + https, isHTTPS := rr.(*dns.HTTPS) + if !isHTTPS { + continue + } + content := https.SVCB + content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool { + if options.Strategy == C.DomainStrategyIPv4Only { + return it.Key() != dns.SVCB_IPV6HINT + } + return it.Key() != dns.SVCB_IPV4HINT + }) + https.SVCB = content + } + } + timeToLive := computeTimeToLive(response) + if options.RewriteTTL != nil { + timeToLive = *options.RewriteTTL + } + normalizeTTL(response, timeToLive) + return timeToLive +} + +func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) { + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + _, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{}) + if loaded { + return + } + go func() { + defer c.backgroundRefresh.Delete(key) + ctx := contextWithTransportTag(c.ctx, transport.Tag()) + response, err := c.exchangeToTransport(ctx, transport, message) + if err != nil { + if c.logger != nil { + c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) + } + return + } + if responseChecker != nil { + var rejected bool + if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + rejected = true + } else { + rejected = !responseChecker(response) + } + if rejected { + if c.rdrc != nil { + c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) + } + return + } + } else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + return + } + timeToLive := applyResponseOptions(question, response, options) + c.storeCache(transport, question, response, timeToLive) + }() +} + +func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg { + clientSubnet := options.ClientSubnet + if !clientSubnet.IsValid() { + clientSubnet = c.clientSubnet + } + if clientSubnet.IsValid() { + message = SetClientSubnet(message, clientSubnet) + } + return message +} + +func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + response, err := transport.Exchange(ctx, message) + if err == nil { + return response, nil + } + var rcodeError RcodeError + if errors.As(err, &rcodeError) { + return FixedResponseStatus(message, int(rcodeError)), nil + } + return nil, err +} + +func MessageToAddresses(response *dns.Msg) []netip.Addr { + return adapter.DNSResponseAddresses(response) +} + +func wrapError(err error) error { + switch dnsErr := err.(type) { + case *net.DNSError: + if dnsErr.IsNotFound { + return RcodeNameError + } + case *net.AddrError: + return RcodeNameError + } + return err +} + +type transportKey struct{} + +func contextWithTransportTag(ctx context.Context, transportTag string) context.Context { + return context.WithValue(ctx, transportKey{}, transportTag) +} + +func transportTagFromContext(ctx context.Context) (string, bool) { + value, loaded := ctx.Value(transportKey{}).(string) + return value, loaded +} + +func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg { + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: message.Id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: rcode, + }, + 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, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, + }, + Question: []dns.Question{question}, + } + for _, address := range addresses { + if address.Is4() && question.Qtype == dns.TypeA { + response.Answer = append(response.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: question.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: timeToLive, + }, + A: address.AsSlice(), + }) + } else if address.Is6() && question.Qtype == dns.TypeAAAA { + response.Answer = append(response.Answer, &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: question.Name, + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: timeToLive, + }, + AAAA: address.AsSlice(), + }) + } + } + return &response +} + +func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg { + response := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, + }, + Question: []dns.Question{question}, + Answer: []dns.RR{ + &dns.CNAME{ + Hdr: dns.RR_Header{ + Name: question.Name, + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: timeToLive, + }, + Target: record, + }, + }, + } + return &response +} + +func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg { + response := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, + }, + Question: []dns.Question{question}, + Answer: []dns.RR{ + &dns.TXT{ + Hdr: dns.RR_Header{ + Name: question.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: timeToLive, + }, + Txt: records, + }, + }, + } + return &response +} + +func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg { + response := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, + }, + Question: []dns.Question{question}, + } + for _, record := range records { + response.Answer = append(response.Answer, &dns.MX{ + Hdr: dns.RR_Header{ + Name: question.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: timeToLive, + }, + Preference: record.Pref, + Mx: record.Host, + }) + } + return &response +} diff --git a/dns/client_log.go b/dns/client_log.go new file mode 100644 index 00000000..129e273c --- /dev/null +++ b/dns/client_log.go @@ -0,0 +1,82 @@ +package dns + +import ( + "context" + "strings" + + "github.com/sagernet/sing/common/logger" + + "github.com/miekg/dns" +) + +func logCachedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl int) { + if logger == nil || len(response.Question) == 0 { + return + } + domain := FqdnToDomain(response.Question[0].Name) + logger.DebugContext(ctx, "cached ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl) + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "cached ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + +func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) { + if logger == nil || len(response.Question) == 0 { + return + } + domain := FqdnToDomain(response.Question[0].Name) + logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode]) + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + +func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) { + if logger == nil || len(response.Question) == 0 { + return + } + domain := FqdnToDomain(response.Question[0].Name) + logger.DebugContext(ctx, "exchanged ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl) + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "exchanged ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + +func logRejectedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) { + if logger == nil || len(response.Question) == 0 { + return + } + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "rejected ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + +func FqdnToDomain(fqdn string) string { + if dns.IsFqdn(fqdn) { + return fqdn[:len(fqdn)-1] + } + return fqdn +} + +func FormatQuestion(string string) string { + for strings.HasPrefix(string, ";") { + string = string[1:] + } + string = strings.ReplaceAll(string, "\t", " ") + string = strings.ReplaceAll(string, "\n", " ") + string = strings.ReplaceAll(string, ";; ", " ") + string = strings.ReplaceAll(string, "; ", " ") + + for strings.Contains(string, " ") { + string = strings.ReplaceAll(string, " ", " ") + } + return strings.TrimSpace(string) +} diff --git a/dns/client_truncate.go b/dns/client_truncate.go new file mode 100644 index 00000000..19165f99 --- /dev/null +++ b/dns/client_truncate.go @@ -0,0 +1,30 @@ +package dns + +import ( + "github.com/sagernet/sing/common/buf" + + "github.com/miekg/dns" +) + +func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf.Buffer, error) { + maxLen := 512 + if edns0Option := request.IsEdns0(); edns0Option != nil { + if udpSize := int(edns0Option.UDPSize()); udpSize > 512 { + maxLen = udpSize + } + } + responseLen := response.Len() + if responseLen > maxLen { + response = response.Copy() + response.Truncate(maxLen) + } + buffer := buf.NewSize(headroom*2 + 1 + responseLen) + buffer.Resize(headroom, 0) + rawMessage, err := response.PackBuffer(buffer.FreeBytes()) + if err != nil { + buffer.Release() + return nil, err + } + buffer.Truncate(len(rawMessage)) + return buffer, nil +} diff --git a/dns/extension_edns0_subnet.go b/dns/extension_edns0_subnet.go new file mode 100644 index 00000000..e804fb6c --- /dev/null +++ b/dns/extension_edns0_subnet.go @@ -0,0 +1,57 @@ +package dns + +import ( + "net/netip" + + "github.com/miekg/dns" +) + +func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix) *dns.Msg { + return setClientSubnet(message, clientSubnet, true) +} + +func setClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, clone bool) *dns.Msg { + var ( + optRecord *dns.OPT + subnetOption *dns.EDNS0_SUBNET + ) +findExists: + for _, record := range message.Extra { + var isOPTRecord bool + if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord { + for _, option := range optRecord.Option { + var isEDNS0Subnet bool + subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET) + if isEDNS0Subnet { + break findExists + } + } + } + } + if optRecord == nil { + exMessage := *message + message = &exMessage + optRecord = &dns.OPT{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: dns.TypeOPT, + }, + } + message.Extra = append(message.Extra, optRecord) + } else if clone { + return setClientSubnet(message.Copy(), clientSubnet, false) + } + if subnetOption == nil { + subnetOption = new(dns.EDNS0_SUBNET) + subnetOption.Code = dns.EDNS0SUBNET + optRecord.Option = append(optRecord.Option, subnetOption) + } + if clientSubnet.Addr().Is4() { + subnetOption.Family = 1 + } else { + subnetOption.Family = 2 + } + subnetOption.SourceNetmask = uint8(clientSubnet.Bits()) + subnetOption.Address = clientSubnet.Addr().AsSlice() + return message +} diff --git a/dns/rcode.go b/dns/rcode.go new file mode 100644 index 00000000..417d41fa --- /dev/null +++ b/dns/rcode.go @@ -0,0 +1,19 @@ +package dns + +import ( + mDNS "github.com/miekg/dns" +) + +const ( + RcodeSuccess RcodeError = mDNS.RcodeSuccess + RcodeServerFailure RcodeError = mDNS.RcodeServerFailure + RcodeFormatError RcodeError = mDNS.RcodeFormatError + RcodeNameError RcodeError = mDNS.RcodeNameError + RcodeRefused RcodeError = mDNS.RcodeRefused +) + +type RcodeError int + +func (e RcodeError) Error() string { + return mDNS.RcodeToString[int(e)] +} diff --git a/dns/repro_test.go b/dns/repro_test.go new file mode 100644 index 00000000..113f7c49 --- /dev/null +++ b/dns/repro_test.go @@ -0,0 +1,111 @@ +package dns + +import ( + "context" + "net/netip" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + + mDNS "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + var qTypes []uint16 + router := newTestRouter(t, nil, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + qTypes = append(qTypes, message.Question[0].Qtype) + if message.Question[0].Qtype == mDNS.TypeA { + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + } + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil + }, + }) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + Strategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []uint16{mDNS.TypeA}, qTypes) + require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses) +} + +func TestReproLogicalMatchResponseIPCIDR(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} diff --git a/dns/router.go b/dns/router.go new file mode 100644 index 00000000..b9fc8f97 --- /dev/null +++ b/dns/router.go @@ -0,0 +1,1149 @@ +package dns + +import ( + "context" + "errors" + "net/netip" + "strings" + "sync" + "time" + + "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/experimental/deprecated" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + R "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + 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" + "github.com/sagernet/sing/common/task" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +var ( + _ adapter.DNSRouter = (*Router)(nil) + _ adapter.DNSRuleSetUpdateValidator = (*Router)(nil) +) + +type Router struct { + ctx context.Context + logger logger.ContextLogger + transport adapter.DNSTransportManager + outbound adapter.OutboundManager + client adapter.DNSClient + rawRules []option.DNSRule + rules []adapter.DNSRule + defaultDomainStrategy C.DomainStrategy + dnsReverseMapping freelru.Cache[netip.Addr, string] + platformInterface adapter.PlatformInterface + legacyDNSMode bool + rulesAccess sync.RWMutex + started bool + closing bool +} + +func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) { + router := &Router{ + ctx: ctx, + logger: logFactory.NewLogger("dns"), + transport: service.FromContext[adapter.DNSTransportManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + rawRules: make([]option.DNSRule, 0, len(options.Rules)), + rules: make([]adapter.DNSRule, 0, len(options.Rules)), + defaultDomainStrategy: C.DomainStrategy(options.Strategy), + } + if options.DNSClientOptions.IndependentCache { + deprecated.Report(ctx, deprecated.OptionIndependentDNSCache) + } + var optimisticTimeout time.Duration + optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic) + if optimisticOptions.Enabled { + if options.DNSClientOptions.DisableCache { + return nil, E.New("`optimistic` is conflict with `disable_cache`") + } + if options.DNSClientOptions.DisableExpire { + return nil, E.New("`optimistic` is conflict with `disable_expire`") + } + optimisticTimeout = time.Duration(optimisticOptions.Timeout) + if optimisticTimeout == 0 { + optimisticTimeout = 3 * 24 * time.Hour + } + } + router.client = NewClient(ClientOptions{ + Context: ctx, + DisableCache: options.DNSClientOptions.DisableCache, + DisableExpire: options.DNSClientOptions.DisableExpire, + OptimisticTimeout: optimisticTimeout, + CacheCapacity: options.DNSClientOptions.CacheCapacity, + ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), + RDRC: func() adapter.RDRCStore { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + return nil + } + if !cacheFile.StoreRDRC() { + return nil + } + return cacheFile + }, + DNSCache: func() adapter.DNSCacheStore { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + return nil + } + if !cacheFile.StoreDNS() { + return nil + } + cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire) + cacheFile.SetOptimisticTimeout(optimisticTimeout) + return cacheFile + }, + Logger: router.logger, + }) + if options.ReverseMapping { + router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32)) + } + return router, nil +} + +func (r *Router) Initialize(rules []option.DNSRule) error { + r.rawRules = append(r.rawRules[:0], rules...) + newRules, _, _, err := r.buildRules(false) + if err != nil { + return err + } + closeRules(newRules) + return nil +} + +func (r *Router) Start(stage adapter.StartStage) error { + monitor := taskmonitor.New(r.logger, C.StartTimeout) + switch stage { + case adapter.StartStateStart: + monitor.Start("initialize DNS client") + r.client.Start() + monitor.Finish() + + monitor.Start("initialize DNS rules") + newRules, legacyDNSMode, modeFlags, err := r.buildRules(true) + monitor.Finish() + if err != nil { + return err + } + r.rulesAccess.Lock() + if r.closing { + r.rulesAccess.Unlock() + closeRules(newRules) + return nil + } + r.rules = newRules + r.legacyDNSMode = legacyDNSMode + r.started = true + r.rulesAccess.Unlock() + if legacyDNSMode && common.Any(newRules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() }) { + deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter) + } + if legacyDNSMode && modeFlags.neededFromStrategy { + deprecated.Report(r.ctx, deprecated.OptionLegacyDNSRuleStrategy) + } + } + return nil +} + +func (r *Router) Close() error { + r.rulesAccess.Lock() + if r.closing { + r.rulesAccess.Unlock() + return nil + } + r.closing = true + runtimeRules := r.rules + r.rules = nil + r.rulesAccess.Unlock() + closeRules(runtimeRules) + return nil +} + +func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) { + for i, ruleOptions := range r.rawRules { + err := R.ValidateNoNestedDNSRuleActions(ruleOptions) + if err != nil { + return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]") + } + } + router := service.FromContext[adapter.Router](r.ctx) + legacyDNSMode, modeFlags, err := resolveLegacyDNSMode(router, r.rawRules, nil) + if err != nil { + return nil, false, dnsRuleModeFlags{}, err + } + if !legacyDNSMode { + err = validateLegacyDNSModeDisabledRules(r.rawRules) + if err != nil { + return nil, false, dnsRuleModeFlags{}, err + } + } + err = validateEvaluateFakeIPRules(r.rawRules, r.transport) + if err != nil { + return nil, false, dnsRuleModeFlags{}, err + } + newRules := make([]adapter.DNSRule, 0, len(r.rawRules)) + for i, ruleOptions := range r.rawRules { + var dnsRule adapter.DNSRule + dnsRule, err = R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode) + if err != nil { + closeRules(newRules) + return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]") + } + newRules = append(newRules, dnsRule) + } + if startRules { + for i, rule := range newRules { + err = rule.Start() + if err != nil { + closeRules(newRules) + return nil, false, dnsRuleModeFlags{}, E.Cause(err, "initialize DNS rule[", i, "]") + } + } + } + return newRules, legacyDNSMode, modeFlags, nil +} + +func closeRules(rules []adapter.DNSRule) { + for _, rule := range rules { + _ = rule.Close() + } +} + +func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error { + if len(r.rawRules) == 0 { + return nil + } + router := service.FromContext[adapter.Router](r.ctx) + if router == nil { + return E.New("router service not found") + } + overrides := map[string]adapter.RuleSetMetadata{ + tag: metadata, + } + r.rulesAccess.RLock() + started := r.started + legacyDNSMode := r.legacyDNSMode + closing := r.closing + r.rulesAccess.RUnlock() + if closing { + return nil + } + if !started { + candidateLegacyDNSMode, _, err := resolveLegacyDNSMode(router, r.rawRules, overrides) + if err != nil { + return err + } + if !candidateLegacyDNSMode { + return validateLegacyDNSModeDisabledRules(r.rawRules) + } + return nil + } + candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides) + if err != nil { + return err + } + if legacyDNSMode { + if !candidateLegacyDNSMode && flags.disabled { + err := validateLegacyDNSModeDisabledRules(r.rawRules) + if err != nil { + return err + } + return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) + } + return nil + } + if candidateLegacyDNSMode { + return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) + } + return nil +} + +func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) { + metadata := adapter.ContextFrom(ctx) + if metadata == nil { + panic("no context") + } + var currentRuleIndex int + if ruleIndex != -1 { + currentRuleIndex = ruleIndex + 1 + } + for ; currentRuleIndex < len(rules); currentRuleIndex++ { + currentRule := rules[currentRuleIndex] + if currentRule.WithAddressLimit() && !isAddressQuery { + continue + } + metadata.ResetRuleCache() + metadata.DestinationAddressMatchFromResponse = false + if currentRule.LegacyPreMatch(metadata) { + if ruleDescription := currentRule.String(); ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) + } + switch action := currentRule.Action().(type) { + case *R.RuleActionDNSRoute: + transport, loaded := r.transport.Transport(action.Server) + if !loaded { + r.logger.ErrorContext(ctx, "transport not found: ", action.Server) + continue + } + isFakeIP := transport.Type() == C.DNSTypeFakeIP + if isFakeIP && !allowFakeIP { + continue + } + if action.Strategy != C.DomainStrategyAsIS { + options.Strategy = action.Strategy + } + if isFakeIP || action.DisableCache { + options.DisableCache = true + } + if action.RewriteTTL != nil { + options.RewriteTTL = action.RewriteTTL + } + if action.ClientSubnet.IsValid() { + options.ClientSubnet = action.ClientSubnet + } + return transport, currentRule, currentRuleIndex + case *R.RuleActionDNSRouteOptions: + if action.Strategy != C.DomainStrategyAsIS { + options.Strategy = action.Strategy + } + if action.DisableCache { + options.DisableCache = true + } + if action.RewriteTTL != nil { + options.RewriteTTL = action.RewriteTTL + } + if action.ClientSubnet.IsValid() { + options.ClientSubnet = action.ClientSubnet + } + case *R.RuleActionReject: + return nil, currentRule, currentRuleIndex + case *R.RuleActionPredefined: + return nil, currentRule, currentRuleIndex + } + } + } + transport := r.transport.Default() + return transport, nil, -1 +} + +func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) { + // Strategy is intentionally skipped here. A non-default DNS rule action strategy + // forces legacy mode via resolveLegacyDNSMode, so this path is only reachable + // when strategy remains at its default value. + if routeOptions.DisableCache { + options.DisableCache = true + } + if routeOptions.DisableOptimisticCache { + options.DisableOptimisticCache = true + } + if routeOptions.RewriteTTL != nil { + options.RewriteTTL = routeOptions.RewriteTTL + } + if routeOptions.ClientSubnet.IsValid() { + options.ClientSubnet = routeOptions.ClientSubnet + } +} + +type dnsRouteStatus uint8 + +const ( + dnsRouteStatusMissing dnsRouteStatus = iota + dnsRouteStatusSkipped + dnsRouteStatusResolved +) + +func (r *Router) resolveDNSRoute(server string, routeOptions R.RuleActionDNSRouteOptions, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus) { + transport, loaded := r.transport.Transport(server) + if !loaded { + return nil, dnsRouteStatusMissing + } + isFakeIP := transport.Type() == C.DNSTypeFakeIP + if isFakeIP && !allowFakeIP { + return transport, dnsRouteStatusSkipped + } + r.applyDNSRouteOptions(options, routeOptions) + if isFakeIP { + options.DisableCache = true + } + return transport, dnsRouteStatusResolved +} + +func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) { + if ruleDescription := currentRule.String(); ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", ruleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", ruleIndex, "] => ", currentRule.Action()) + } +} + +type exchangeWithRulesResult struct { + response *mDNS.Msg + transport adapter.DNSTransport + rejectAction *R.RuleActionReject + err error +} + +const dnsRespondMissingResponseMessage = "respond action requires an evaluated response from a preceding evaluate action" + +func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult { + metadata := adapter.ContextFrom(ctx) + if metadata == nil { + panic("no context") + } + effectiveOptions := options + var evaluatedResponse *mDNS.Msg + var evaluatedTransport adapter.DNSTransport + for currentRuleIndex, currentRule := range rules { + metadata.ResetRuleCache() + metadata.DNSResponse = evaluatedResponse + metadata.DestinationAddressMatchFromResponse = false + if !currentRule.Match(metadata) { + continue + } + r.logRuleMatch(ctx, currentRuleIndex, currentRule) + switch action := currentRule.Action().(type) { + case *R.RuleActionDNSRouteOptions: + r.applyDNSRouteOptions(&effectiveOptions, *action) + case *R.RuleActionEvaluate: + queryOptions := effectiveOptions + transport, loaded := r.transport.Transport(action.Server) + if !loaded { + r.logger.ErrorContext(ctx, "transport not found: ", action.Server) + evaluatedResponse = nil + evaluatedTransport = nil + continue + } + r.applyDNSRouteOptions(&queryOptions, action.RuleActionDNSRouteOptions) + exchangeOptions := queryOptions + if exchangeOptions.Strategy == C.DomainStrategyAsIS { + exchangeOptions.Strategy = r.defaultDomainStrategy + } + response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) + if err != nil { + r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) + evaluatedResponse = nil + evaluatedTransport = nil + continue + } + evaluatedResponse = response + evaluatedTransport = transport + case *R.RuleActionRespond: + if evaluatedResponse == nil { + return exchangeWithRulesResult{ + err: E.New(dnsRespondMissingResponseMessage), + } + } + return exchangeWithRulesResult{ + response: evaluatedResponse, + transport: evaluatedTransport, + } + case *R.RuleActionDNSRoute: + queryOptions := effectiveOptions + transport, status := r.resolveDNSRoute(action.Server, action.RuleActionDNSRouteOptions, allowFakeIP, &queryOptions) + switch status { + case dnsRouteStatusMissing: + r.logger.ErrorContext(ctx, "transport not found: ", action.Server) + continue + case dnsRouteStatusSkipped: + continue + } + exchangeOptions := queryOptions + if exchangeOptions.Strategy == C.DomainStrategyAsIS { + exchangeOptions.Strategy = r.defaultDomainStrategy + } + response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) + return exchangeWithRulesResult{ + response: response, + transport: transport, + err: err, + } + case *R.RuleActionReject: + switch action.Method { + case C.RuleActionRejectMethodDefault: + return exchangeWithRulesResult{ + response: &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Rcode: mDNS.RcodeRefused, + Response: true, + }, + Question: []mDNS.Question{message.Question[0]}, + }, + rejectAction: action, + } + case C.RuleActionRejectMethodDrop: + return exchangeWithRulesResult{ + rejectAction: action, + err: tun.ErrDrop, + } + } + case *R.RuleActionPredefined: + return exchangeWithRulesResult{ + response: action.Response(message), + } + } + } + transport := r.transport.Default() + exchangeOptions := effectiveOptions + if exchangeOptions.Strategy == C.DomainStrategyAsIS { + exchangeOptions.Strategy = r.defaultDomainStrategy + } + response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) + return exchangeWithRulesResult{ + response: response, + transport: transport, + err: err, + } +} + +func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.DomainStrategy { + if options.LookupStrategy != C.DomainStrategyAsIS { + return options.LookupStrategy + } + if options.Strategy != C.DomainStrategyAsIS { + return options.Strategy + } + return r.defaultDomainStrategy +} + +func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.QueryType = qType + metadata.IPVersion = 0 + switch qType { + case mDNS.TypeA: + metadata.IPVersion = 4 + case mDNS.TypeAAAA: + metadata.IPVersion = 6 + } + return ctx +} + +func filterAddressesByQueryType(addresses []netip.Addr, qType uint16) []netip.Addr { + switch qType { + case mDNS.TypeA: + return common.Filter(addresses, func(address netip.Addr) bool { + return address.Is4() + }) + case mDNS.TypeAAAA: + return common.Filter(addresses, func(address netip.Addr) bool { + return address.Is6() + }) + default: + return addresses + } +} + +func (r *Router) lookupWithRules(ctx context.Context, rules []adapter.DNSRule, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { + strategy := r.resolveLookupStrategy(options) + lookupOptions := options + if strategy != C.DomainStrategyAsIS { + lookupOptions.Strategy = strategy + } + if strategy == C.DomainStrategyIPv4Only { + return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions) + } + if strategy == C.DomainStrategyIPv6Only { + return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions) + } + var ( + response4 []netip.Addr + response6 []netip.Addr + ) + var group task.Group + group.Append("exchange4", func(ctx context.Context) error { + result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions) + response4 = result + return err + }) + group.Append("exchange6", func(ctx context.Context) error { + result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions) + response6 = result + return err + }) + err := group.Run(ctx) + if len(response4) == 0 && len(response6) == 0 { + return nil, err + } + return sortAddresses(response4, response6, strategy), nil +} + +func (r *Router) lookupWithRulesType(ctx context.Context, rules []adapter.DNSRule, domain string, qType uint16, options adapter.DNSQueryOptions) ([]netip.Addr, error) { + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{{ + Name: mDNS.Fqdn(domain), + Qtype: qType, + Qclass: mDNS.ClassINET, + }}, + } + exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), rules, request, options, false) + if exchangeResult.rejectAction != nil { + return nil, exchangeResult.rejectAction.Error(ctx) + } + if exchangeResult.err != nil { + return nil, exchangeResult.err + } + if exchangeResult.response.Rcode != mDNS.RcodeSuccess { + return nil, RcodeError(exchangeResult.response.Rcode) + } + return filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType), nil +} + +func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) { + if len(message.Question) != 1 { + r.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) + responseMessage := mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Response: true, + Rcode: mDNS.RcodeFormatError, + }, + Question: message.Question, + } + return &responseMessage, nil + } + r.rulesAccess.RLock() + if r.closing { + r.rulesAccess.RUnlock() + return nil, E.New("dns router closed") + } + rules := r.rules + legacyDNSMode := r.legacyDNSMode + r.rulesAccess.RUnlock() + r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String())) + var ( + response *mDNS.Msg + transport adapter.DNSTransport + err error + ) + var metadata *adapter.InboundContext + ctx, metadata = adapter.ExtendContext(ctx) + metadata.Destination = M.Socksaddr{} + metadata.QueryType = message.Question[0].Qtype + metadata.DNSResponse = nil + metadata.DestinationAddressMatchFromResponse = false + switch metadata.QueryType { + case mDNS.TypeA: + metadata.IPVersion = 4 + case mDNS.TypeAAAA: + metadata.IPVersion = 6 + } + metadata.Domain = FqdnToDomain(message.Question[0].Name) + if options.Transport != nil { + transport = options.Transport + if options.Strategy == C.DomainStrategyAsIS { + options.Strategy = r.defaultDomainStrategy + } + response, err = r.client.Exchange(ctx, transport, message, options, nil) + } else if !legacyDNSMode { + exchangeResult := r.exchangeWithRules(ctx, rules, message, options, true) + response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err + } else { + var ( + rule adapter.DNSRule + ruleIndex int + ) + ruleIndex = -1 + for { + dnsCtx := adapter.OverrideContext(ctx) + dnsOptions := options + transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions) + if rule != nil { + switch action := rule.Action().(type) { + case *R.RuleActionReject: + switch action.Method { + case C.RuleActionRejectMethodDefault: + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Rcode: mDNS.RcodeRefused, + Response: true, + }, + Question: []mDNS.Question{message.Question[0]}, + }, nil + case C.RuleActionRejectMethodDrop: + return nil, tun.ErrDrop + } + case *R.RuleActionPredefined: + err = nil + response = action.Response(message) + goto done + } + } + responseCheck := addressLimitResponseCheck(rule, metadata) + if dnsOptions.Strategy == C.DomainStrategyAsIS { + dnsOptions.Strategy = r.defaultDomainStrategy + } + response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck) + var rejected bool + if err != nil { + if errors.Is(err, ErrResponseRejectedCached) { + rejected = true + r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)") + } 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 len(message.Question) > 0 { + 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 ")) + } + } + if responseCheck != nil && rejected { + continue + } + break + } + } +done: + if err != nil { + return nil, err + } + if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { + if transport == nil || transport.Type() != C.DNSTypeFakeIP { + for _, answer := range response.Answer { + switch record := answer.(type) { + case *mDNS.A: + r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second) + case *mDNS.AAAA: + r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second) + } + } + } + } + return response, nil +} + +func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { + r.rulesAccess.RLock() + if r.closing { + r.rulesAccess.RUnlock() + return nil, E.New("dns router closed") + } + rules := r.rules + legacyDNSMode := r.legacyDNSMode + r.rulesAccess.RUnlock() + var ( + responseAddrs []netip.Addr + err error + ) + printResult := func() { + if err == nil && len(responseAddrs) == 0 { + err = E.New("empty result") + } + if err != nil { + if errors.Is(err, ErrResponseRejectedCached) { + r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)") + } else if errors.Is(err, ErrResponseRejected) { + r.logger.DebugContext(ctx, "response rejected for ", domain) + } else if R.IsRejected(err) { + r.logger.DebugContext(ctx, "lookup rejected for ", domain) + } else { + r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) + } + } + if err != nil { + err = E.Cause(err, "lookup ", domain) + } + } + r.logger.DebugContext(ctx, "lookup domain ", domain) + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Destination = M.Socksaddr{} + metadata.Domain = FqdnToDomain(domain) + metadata.DNSResponse = nil + metadata.DestinationAddressMatchFromResponse = false + if options.Transport != nil { + transport := options.Transport + if options.Strategy == C.DomainStrategyAsIS { + options.Strategy = r.defaultDomainStrategy + } + responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil) + } else if !legacyDNSMode { + responseAddrs, err = r.lookupWithRules(ctx, rules, domain, options) + } else { + var ( + transport adapter.DNSTransport + rule adapter.DNSRule + ruleIndex int + ) + ruleIndex = -1 + for { + dnsCtx := adapter.OverrideContext(ctx) + dnsOptions := options + transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions) + if rule != nil { + switch action := rule.Action().(type) { + case *R.RuleActionReject: + return nil, &R.RejectedError{Cause: action.Error(ctx)} + case *R.RuleActionPredefined: + responseAddrs = nil + if action.Rcode != mDNS.RcodeSuccess { + err = RcodeError(action.Rcode) + } else { + err = nil + for _, answer := range action.Answer { + switch record := answer.(type) { + case *mDNS.A: + responseAddrs = append(responseAddrs, M.AddrFromIP(record.A)) + case *mDNS.AAAA: + responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA)) + } + } + } + goto response + } + } + responseCheck := addressLimitResponseCheck(rule, metadata) + if dnsOptions.Strategy == C.DomainStrategyAsIS { + dnsOptions.Strategy = r.defaultDomainStrategy + } + responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck) + if responseCheck == nil || err == nil { + break + } + printResult() + } + } +response: + printResult() + if len(responseAddrs) > 0 { + r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) + } + return responseAddrs, err +} + +func isAddressQuery(message *mDNS.Msg) bool { + for _, question := range message.Question { + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS { + return true + } + } + return false +} + +func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool { + if rule == nil || !rule.WithAddressLimit() { + return nil + } + responseMetadata := *metadata + return func(response *mDNS.Msg) bool { + checkMetadata := responseMetadata + return rule.MatchAddressLimit(&checkMetadata, response) + } +} + +func (r *Router) ClearCache() { + r.client.ClearCache() + if r.platformInterface != nil { + r.platformInterface.ClearDNSCache() + } +} + +func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) { + if r.dnsReverseMapping == nil { + return "", false + } + domain, loaded := r.dnsReverseMapping.Get(ip) + return domain, loaded +} + +func (r *Router) ResetNetwork() { + r.ClearCache() + for _, transport := range r.transport.Transports() { + transport.Reset() + } +} + +func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool { + if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + return true + } + return !rule.MatchResponse && (rule.IPAcceptAny || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) +} + +func hasResponseMatchFields(rule option.DefaultDNSRule) bool { + return rule.ResponseRcode != nil || + len(rule.ResponseAnswer) > 0 || + len(rule.ResponseNs) > 0 || + len(rule.ResponseExtra) > 0 +} + +func defaultRuleDisablesLegacyDNSMode(rule option.DefaultDNSRule) bool { + return rule.MatchResponse || + hasResponseMatchFields(rule) || + rule.Action == C.RuleActionTypeEvaluate || + rule.Action == C.RuleActionTypeRespond || + rule.IPVersion > 0 || + len(rule.QueryType) > 0 +} + +type dnsRuleModeFlags struct { + disabled bool + needed bool + neededFromStrategy bool +} + +func (f *dnsRuleModeFlags) merge(other dnsRuleModeFlags) { + f.disabled = f.disabled || other.disabled + f.needed = f.needed || other.needed + f.neededFromStrategy = f.neededFromStrategy || other.neededFromStrategy +} + +func resolveLegacyDNSMode(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, dnsRuleModeFlags, error) { + flags, err := dnsRuleModeRequirements(router, rules, metadataOverrides) + if err != nil { + return false, flags, err + } + if flags.disabled && flags.neededFromStrategy { + return false, flags, E.New(deprecated.OptionLegacyDNSRuleStrategy.MessageWithLink()) + } + if flags.disabled { + return false, flags, nil + } + return flags.needed, flags, nil +} + +func dnsRuleModeRequirements(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { + var flags dnsRuleModeFlags + for i, rule := range rules { + ruleFlags, err := dnsRuleModeRequirementsInRule(router, rule, metadataOverrides) + if err != nil { + return dnsRuleModeFlags{}, E.Cause(err, "dns rule[", i, "]") + } + flags.merge(ruleFlags) + } + return flags, nil +} + +func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { + switch rule.Type { + case "", C.RuleTypeDefault: + return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides) + case C.RuleTypeLogical: + flags := dnsRuleModeFlags{ + disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || + dnsRuleActionType(rule) == C.RuleActionTypeRespond || + dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction), + neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction), + } + flags.needed = flags.neededFromStrategy + for i, subRule := range rule.LogicalOptions.Rules { + subFlags, err := dnsRuleModeRequirementsInRule(router, subRule, metadataOverrides) + if err != nil { + return dnsRuleModeFlags{}, E.Cause(err, "sub rule[", i, "]") + } + flags.merge(subFlags) + } + return flags, nil + default: + return dnsRuleModeFlags{}, nil + } +} + +func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { + flags := dnsRuleModeFlags{ + disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction), + neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction), + } + flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy + if len(rule.RuleSet) == 0 { + return flags, nil + } + if router == nil { + return dnsRuleModeFlags{}, E.New("router service not found") + } + for _, tag := range rule.RuleSet { + metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides) + if err != nil { + return dnsRuleModeFlags{}, err + } + // ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent. + flags.disabled = flags.disabled || metadata.ContainsDNSQueryTypeRule + if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule { + flags.needed = true + } + } + return flags, nil +} + +func lookupDNSRuleSetMetadata(router adapter.Router, tag string, metadataOverrides map[string]adapter.RuleSetMetadata) (adapter.RuleSetMetadata, error) { + if metadataOverrides != nil { + if metadata, loaded := metadataOverrides[tag]; loaded { + return metadata, nil + } + } + ruleSet, loaded := router.RuleSet(tag) + if !loaded { + return adapter.RuleSetMetadata{}, E.New("rule-set not found: ", tag) + } + return ruleSet.Metadata(), nil +} + +func referencedDNSRuleSetTags(rules []option.DNSRule) []string { + tagMap := make(map[string]bool) + var walkRule func(rule option.DNSRule) + walkRule = func(rule option.DNSRule) { + switch rule.Type { + case "", C.RuleTypeDefault: + for _, tag := range rule.DefaultOptions.RuleSet { + tagMap[tag] = true + } + case C.RuleTypeLogical: + for _, subRule := range rule.LogicalOptions.Rules { + walkRule(subRule) + } + } + } + for _, rule := range rules { + walkRule(rule) + } + tags := make([]string, 0, len(tagMap)) + for tag := range tagMap { + if tag != "" { + tags = append(tags, tag) + } + } + return tags +} + +func validateLegacyDNSModeDisabledRules(rules []option.DNSRule) error { + var seenEvaluate bool + for i, rule := range rules { + requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(rule) + if err != nil { + return E.Cause(err, "validate dns rule[", i, "]") + } + if requiresPriorEvaluate && !seenEvaluate { + return E.New("dns rule[", i, "]: response-based matching requires a preceding evaluate action") + } + if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate { + seenEvaluate = true + } + } + return nil +} + +func validateEvaluateFakeIPRules(rules []option.DNSRule, transportManager adapter.DNSTransportManager) error { + if transportManager == nil { + return nil + } + for i, rule := range rules { + if dnsRuleActionType(rule) != C.RuleActionTypeEvaluate { + continue + } + server := dnsRuleActionServer(rule) + if server == "" { + continue + } + transport, loaded := transportManager.Transport(server) + if !loaded || transport.Type() != C.DNSTypeFakeIP { + continue + } + return E.New("dns rule[", i, "]: evaluate action cannot use fakeip server: ", server) + } + return nil +} + +func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) { + switch rule.Type { + case "", C.RuleTypeDefault: + return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions) + case C.RuleTypeLogical: + requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond + for i, subRule := range rule.LogicalOptions.Rules { + subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(subRule) + if err != nil { + return false, E.Cause(err, "sub rule[", i, "]") + } + requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate + } + return requiresPriorEvaluate, nil + default: + return false, nil + } +} + +func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) { + hasResponseRecords := hasResponseMatchFields(rule) + if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate || rule.IPAcceptAny) && !rule.MatchResponse { + return false, E.New("Response Match Fields (ip_cidr, ip_is_private, ip_accept_any, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled") + } + // Intentionally do not reject rule_set here. A referenced rule set may mix + // destination-IP predicates with pre-response predicates such as domain items. + // When match_response is false, those destination-IP branches fail closed during + // pre-response evaluation instead of consuming DNS response state, while sibling + // non-response branches remain matchable. + if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) + } + return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil +} + +func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool { + switch action.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + return action.RouteOptions.DisableOptimisticCache + case C.RuleActionTypeRouteOptions: + return action.RouteOptionsOptions.DisableOptimisticCache + default: + return false + } +} + +func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool { + switch action.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + return C.DomainStrategy(action.RouteOptions.Strategy) != C.DomainStrategyAsIS + case C.RuleActionTypeRouteOptions: + return C.DomainStrategy(action.RouteOptionsOptions.Strategy) != C.DomainStrategyAsIS + default: + return false + } +} + +func dnsRuleActionType(rule option.DNSRule) string { + switch rule.Type { + case "", C.RuleTypeDefault: + if rule.DefaultOptions.Action == "" { + return C.RuleActionTypeRoute + } + return rule.DefaultOptions.Action + case C.RuleTypeLogical: + if rule.LogicalOptions.Action == "" { + return C.RuleActionTypeRoute + } + return rule.LogicalOptions.Action + default: + return "" + } +} + +func dnsRuleActionServer(rule option.DNSRule) string { + switch rule.Type { + case "", C.RuleTypeDefault: + return rule.DefaultOptions.RouteOptions.Server + case C.RuleTypeLogical: + return rule.LogicalOptions.RouteOptions.Server + default: + return "" + } +} diff --git a/dns/router_test.go b/dns/router_test.go new file mode 100644 index 00000000..54213b23 --- /dev/null +++ b/dns/router_test.go @@ -0,0 +1,2547 @@ +package dns + +import ( + "context" + "net" + "net/netip" + "strings" + "sync" + "testing" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + rulepkg "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-tun" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" + "github.com/stretchr/testify/require" + "go4.org/netipx" +) + +type fakeDNSTransport struct { + tag string + transportType string +} + +func (t *fakeDNSTransport) Start(adapter.StartStage) error { return nil } +func (t *fakeDNSTransport) Close() error { return nil } +func (t *fakeDNSTransport) Type() string { return t.transportType } +func (t *fakeDNSTransport) Tag() string { return t.tag } +func (t *fakeDNSTransport) Dependencies() []string { return nil } +func (t *fakeDNSTransport) Reset() {} +func (t *fakeDNSTransport) Exchange(context.Context, *mDNS.Msg) (*mDNS.Msg, error) { + return nil, E.New("unused transport exchange") +} + +type fakeDNSTransportManager struct { + defaultTransport adapter.DNSTransport + transports map[string]adapter.DNSTransport +} + +func (m *fakeDNSTransportManager) Start(adapter.StartStage) error { return nil } +func (m *fakeDNSTransportManager) Close() error { return nil } +func (m *fakeDNSTransportManager) Transports() []adapter.DNSTransport { + transports := make([]adapter.DNSTransport, 0, len(m.transports)) + for _, transport := range m.transports { + transports = append(transports, transport) + } + return transports +} + +func (m *fakeDNSTransportManager) Transport(tag string) (adapter.DNSTransport, bool) { + transport, loaded := m.transports[tag] + return transport, loaded +} +func (m *fakeDNSTransportManager) Default() adapter.DNSTransport { return m.defaultTransport } +func (m *fakeDNSTransportManager) FakeIP() adapter.FakeIPTransport { + return nil +} +func (m *fakeDNSTransportManager) Remove(string) error { return nil } +func (m *fakeDNSTransportManager) Create(context.Context, log.ContextLogger, string, string, any) error { + return E.New("unsupported") +} + +type fakeDNSClient struct { + beforeExchange func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg) + exchange func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) + lookupWithCtx func(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) + lookup func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) +} + +type fakeDeprecatedManager struct { + features []deprecated.Note +} + +type fakeRouter struct { + access sync.RWMutex + ruleSets map[string]adapter.RuleSet +} + +func (r *fakeRouter) Start(adapter.StartStage) error { return nil } +func (r *fakeRouter) Close() error { return nil } +func (r *fakeRouter) PreMatch(metadata adapter.InboundContext, _ tun.DirectRouteContext, _ time.Duration, _ bool) (tun.DirectRouteDestination, error) { + return nil, nil +} + +func (r *fakeRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error { + return nil +} + +func (r *fakeRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error { + return nil +} + +func (r *fakeRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *fakeRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *fakeRouter) RuleSet(tag string) (adapter.RuleSet, bool) { + r.access.RLock() + defer r.access.RUnlock() + ruleSet, loaded := r.ruleSets[tag] + return ruleSet, loaded +} + +func (r *fakeRouter) setRuleSet(tag string, ruleSet adapter.RuleSet) { + r.access.Lock() + defer r.access.Unlock() + if r.ruleSets == nil { + r.ruleSets = make(map[string]adapter.RuleSet) + } + r.ruleSets[tag] = ruleSet +} +func (r *fakeRouter) Rules() []adapter.Rule { return nil } +func (r *fakeRouter) NeedFindProcess() bool { return false } +func (r *fakeRouter) NeedFindNeighbor() bool { return false } +func (r *fakeRouter) NeighborResolver() adapter.NeighborResolver { return nil } +func (r *fakeRouter) AppendTracker(adapter.ConnectionTracker) {} +func (r *fakeRouter) ResetNetwork() {} + +type fakeRuleSet struct { + access sync.Mutex + metadata adapter.RuleSetMetadata + metadataRead func(adapter.RuleSetMetadata) adapter.RuleSetMetadata + match func(*adapter.InboundContext) bool + callbacks list.List[adapter.RuleSetUpdateCallback] + refs int + afterIncrementReference func() + beforeDecrementReference func() +} + +func (s *fakeRuleSet) Name() string { return "fake-rule-set" } +func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil } +func (s *fakeRuleSet) PostStart() error { return nil } +func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata { + s.access.Lock() + metadata := s.metadata + metadataRead := s.metadataRead + s.access.Unlock() + if metadataRead != nil { + return metadataRead(metadata) + } + return metadata +} +func (s *fakeRuleSet) ExtractIPSet() []*netipx.IPSet { return nil } +func (s *fakeRuleSet) IncRef() { + s.access.Lock() + s.refs++ + afterIncrementReference := s.afterIncrementReference + s.access.Unlock() + if afterIncrementReference != nil { + afterIncrementReference() + } +} + +func (s *fakeRuleSet) DecRef() { + s.access.Lock() + beforeDecrementReference := s.beforeDecrementReference + s.access.Unlock() + if beforeDecrementReference != nil { + beforeDecrementReference() + } + s.access.Lock() + defer s.access.Unlock() + s.refs-- + if s.refs < 0 { + panic("rule-set: negative refs") + } +} +func (s *fakeRuleSet) Cleanup() {} +func (s *fakeRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *fakeRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.access.Lock() + defer s.access.Unlock() + s.callbacks.Remove(element) +} +func (s *fakeRuleSet) Close() error { return nil } +func (s *fakeRuleSet) Match(metadata *adapter.InboundContext) bool { + s.access.Lock() + match := s.match + s.access.Unlock() + if match != nil { + return match(metadata) + } + return true +} +func (s *fakeRuleSet) String() string { return "fake-rule-set" } +func (s *fakeRuleSet) updateMetadata(metadata adapter.RuleSetMetadata) { + s.access.Lock() + s.metadata = metadata + callbacks := s.callbacks.Array() + s.access.Unlock() + for _, callback := range callbacks { + callback(s) + } +} + +func (s *fakeRuleSet) snapshotCallbacks() []adapter.RuleSetUpdateCallback { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.Array() +} + +func (s *fakeRuleSet) refCount() int { + s.access.Lock() + defer s.access.Unlock() + return s.refs +} + +func (m *fakeDeprecatedManager) ReportDeprecated(feature deprecated.Note) { + m.features = append(m.features, feature) +} + +func (c *fakeDNSClient) Start() {} + +func (c *fakeDNSClient) Exchange(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, _ adapter.DNSQueryOptions, _ func(*mDNS.Msg) bool) (*mDNS.Msg, error) { + if c.beforeExchange != nil { + c.beforeExchange(ctx, transport, message) + } + if c.exchange == nil { + if len(message.Question) != 1 { + return nil, E.New("unused client exchange") + } + var ( + addresses []netip.Addr + response *mDNS.Msg + err error + ) + if c.lookupWithCtx != nil { + addresses, response, err = c.lookupWithCtx(ctx, transport, FqdnToDomain(message.Question[0].Name), adapter.DNSQueryOptions{}) + } else if c.lookup != nil { + addresses, response, err = c.lookup(transport, FqdnToDomain(message.Question[0].Name), adapter.DNSQueryOptions{}) + } else { + return nil, E.New("unused client exchange") + } + if err != nil { + return nil, err + } + if response != nil { + return response, nil + } + return FixedResponse(0, message.Question[0], addresses, 60), nil + } + return c.exchange(transport, message) +} + +func (c *fakeDNSClient) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(*mDNS.Msg) bool) ([]netip.Addr, error) { + if c.lookup == nil && c.lookupWithCtx == nil { + return nil, E.New("unused client lookup") + } + var ( + addresses []netip.Addr + response *mDNS.Msg + err error + ) + if c.lookupWithCtx != nil { + addresses, response, err = c.lookupWithCtx(ctx, transport, domain, options) + } else { + addresses, response, err = c.lookup(transport, domain, options) + } + if err != nil { + return nil, err + } + if response == nil { + response = FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), addresses, 60) + } + if responseChecker != nil && !responseChecker(response) { + return nil, ErrResponseRejected + } + if addresses != nil { + return addresses, nil + } + return MessageToAddresses(response), nil +} + +func (c *fakeDNSClient) ClearCache() {} + +func newTestRouter(t *testing.T, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router { + router := newTestRouterWithContext(t, context.Background(), rules, transportManager, client) + t.Cleanup(func() { + router.Close() + }) + return router +} + +func newTestRouterWithContext(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router { + return newTestRouterWithContextAndLogger(t, ctx, rules, transportManager, client, log.NewNOPFactory().NewLogger("dns")) +} + +func newTestRouterWithContextAndLogger(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient, dnsLogger log.ContextLogger) *Router { + t.Helper() + router := &Router{ + ctx: ctx, + logger: dnsLogger, + transport: transportManager, + client: client, + rawRules: make([]option.DNSRule, 0, len(rules)), + rules: make([]adapter.DNSRule, 0, len(rules)), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + if rules != nil { + err := router.Initialize(rules) + require.NoError(t, err) + err = router.Start(adapter.StartStateStart) + require.NoError(t, err) + } + return router +} + +func waitForLogMessageContaining(t *testing.T, entries <-chan log.Entry, done <-chan struct{}, substring string) log.Entry { + t.Helper() + timeout := time.After(time.Second) + for { + select { + case entry, ok := <-entries: + if !ok { + t.Fatal("log subscription closed") + } + if strings.Contains(entry.Message, substring) { + return entry + } + case <-done: + t.Fatal("log subscription closed") + case <-timeout: + t.Fatalf("timed out waiting for log message containing %q", substring) + } + } +} + +func fixedQuestion(name string, qType uint16) mDNS.Question { + return mDNS.Question{ + Name: mDNS.Fqdn(name), + Qtype: qType, + Qclass: mDNS.ClassINET, + } +} + +func mustRecord(t *testing.T, record string) option.DNSRecordOptions { + t.Helper() + var value option.DNSRecordOptions + require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`))) + return value +} + +func TestInitializeRejectsDirectLegacyRuleWhenRuleSetForcesNew(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ + Type: C.RuleSetTypeInline, + Tag: "query-set", + InlineOptions: option.PlainRuleSet{ + Rules: []option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)}, + }, + }}, + }, + }) + require.NoError(t, err) + ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "query-set": ruleSet, + }, + }) + + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err = router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"query-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }, + }) + require.ErrorContains(t, err, "Response Match Fields") + require.ErrorContains(t, err, "require match_response") +} + +func TestLookupLegacyDNSModeDefersRuleSetDestinationIPMatch(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ + Type: C.RuleSetTypeInline, + Tag: "legacy-ipcidr-set", + InlineOptions: option.PlainRuleSet{ + Rules: []option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + IPCIDR: badoption.Listable[string]{"10.0.0.0/8"}, + }, + }}, + }, + }) + require.NoError(t, err) + ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "legacy-ipcidr-set": ruleSet, + }, + }) + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, "private", transport.Tag()) + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60) + return MessageToAddresses(response), response, nil + }, + }) + + require.True(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses) +} + +func TestRuleSetUpdateReleasesOldRuleSetRefs(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + ctx := service.ContextWith[adapter.Router](context.Background(), &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + }) + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + + require.Equal(t, 1, fakeSet.refCount()) + + fakeSet.updateMetadata(adapter.RuleSetMetadata{}) + require.Equal(t, 1, fakeSet.refCount()) + + fakeSet.updateMetadata(adapter.RuleSetMetadata{}) + require.Equal(t, 1, fakeSet.refCount()) + + require.NoError(t, router.Close()) + require.Zero(t, fakeSet.refCount()) +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetThatWouldDisableLegacyDNSMode(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil + }, + }) + require.True(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsDNSQueryTypeRule: true, + }) + require.ErrorContains(t, err, "require match_response") +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetOnlyLegacyModeSwitchToNew(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil + }, + }) + require.True(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + ContainsDNSQueryTypeRule: true, + }) + require.ErrorContains(t, err, "Address Filter Fields") +} + +func TestValidateRuleSetMetadataUpdateBeforeStartUsesStartupValidation(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + rules: make([]adapter.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }) + require.NoError(t, err) + require.False(t, router.started) + + err = router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsDNSQueryTypeRule: true, + }) + require.ErrorContains(t, err, "require match_response") +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetThatWouldRequireLegacyDNSMode(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil, nil + }, + }) + require.False(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }) + require.ErrorContains(t, err, "Address Filter Fields") +} + +func TestValidateRuleSetMetadataUpdateAllowsRuleSetThatKeepsNonLegacyDNSMode(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }) + require.NoError(t, err) +} + +func TestValidateRuleSetMetadataUpdateAllowsRelaxingLegacyRequirement(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil + }, + }) + require.True(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{}) + require.NoError(t, err) +} + +func TestCloseWaitsForInFlightLookupUntilContextCancellation(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + selectedTransport := &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP} + lookupStarted := make(chan struct{}) + var lookupStartedOnce sync.Once + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "selected": selectedTransport, + }, + }, &fakeDNSClient{ + lookupWithCtx: func(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "selected", transport.Tag()) + require.Equal(t, "example.com", domain) + lookupStartedOnce.Do(func() { + close(lookupStarted) + }) + <-ctx.Done() + return nil, nil, ctx.Err() + }, + }) + + lookupCtx, cancelLookup := context.WithCancel(context.Background()) + defer cancelLookup() + var ( + lookupErr error + closeErr error + ) + lookupDone := make(chan struct{}) + go func() { + _, lookupErr = router.Lookup(lookupCtx, "example.com", adapter.DNSQueryOptions{}) + close(lookupDone) + }() + + select { + case <-lookupStarted: + case <-time.After(time.Second): + t.Fatal("lookup did not reach DNS client") + } + + closeDone := make(chan struct{}) + go func() { + closeErr = router.Close() + close(closeDone) + }() + + select { + case <-closeDone: + t.Fatal("close finished before lookup context cancellation") + default: + } + + cancelLookup() + + select { + case <-lookupDone: + case <-time.After(time.Second): + t.Fatal("lookup did not finish after cancellation") + } + select { + case <-closeDone: + case <-time.After(time.Second): + t.Fatal("close did not finish after lookup cancellation") + } + + require.ErrorIs(t, lookupErr, context.Canceled) + require.NoError(t, closeErr) +} + +func TestLookupLegacyDNSModeDefersDirectDestinationIPMatch(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + client := &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) + switch transport.Tag() { + case "private": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60) + return MessageToAddresses(response), response, nil + case "default": + t.Fatal("default transport should not be used when legacy rule matches after response") + } + return nil, nil, E.New("unexpected transport") + }, + } + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, client) + + require.True(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses) +} + +func TestLookupLegacyDNSModeFallsBackAfterRejectedAddressLimitResponse(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + var lookupAccess sync.Mutex + var lookupTags []string + recordLookup := func(tag string) { + lookupAccess.Lock() + lookupTags = append(lookupTags, tag) + lookupAccess.Unlock() + } + currentLookupTags := func() []string { + lookupAccess.Lock() + defer lookupAccess.Unlock() + return append([]string(nil), lookupTags...) + } + client := &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) + recordLookup(transport.Tag()) + switch transport.Tag() { + case "private": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60) + return MessageToAddresses(response), response, nil + case "default": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60) + return MessageToAddresses(response), response, nil + } + return nil, nil, E.New("unexpected transport") + }, + } + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, client) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses) + require.Equal(t, []string{"private", "default"}, currentLookupTags()) +} + +func TestLookupLegacyDNSModeRuleSetAcceptEmptyDoesNotTreatMismatchAsEmpty(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ + Type: C.RuleSetTypeInline, + Tag: "legacy-ipcidr-set", + InlineOptions: option.PlainRuleSet{ + Rules: []option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + IPCIDR: badoption.Listable[string]{"10.0.0.0/8"}, + }, + }}, + }, + }) + require.NoError(t, err) + ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "legacy-ipcidr-set": ruleSet, + }, + }) + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + var lookupAccess sync.Mutex + var lookupTags []string + recordLookup := func(tag string) { + lookupAccess.Lock() + lookupTags = append(lookupTags, tag) + lookupAccess.Unlock() + } + currentLookupTags := func() []string { + lookupAccess.Lock() + defer lookupAccess.Unlock() + return append([]string(nil), lookupTags...) + } + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"}, + RuleSetIPCIDRAcceptEmpty: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) + recordLookup(transport.Tag()) + switch transport.Tag() { + case "private": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60) + return MessageToAddresses(response), response, nil + case "default": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60) + return MessageToAddresses(response), response, nil + } + return nil, nil, E.New("unexpected transport") + }, + }) + + require.True(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses) + require.Equal(t, []string{"private", "default"}, currentLookupTags()) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRcodeRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeNameError, + }, + Question: []mDNS.Question{message.Question[0]}, + }, nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rcode := option.DNSRCode(mDNS.RcodeNameError) + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseRcode: &rcode, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseNsRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + nsRecord := mustRecord(t, "example.com. IN NS ns1.example.com.") + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: []mDNS.Question{message.Question[0]}, + Ns: []mDNS.RR{nsRecord.Build()}, + }, nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseNs: badoption.Listable[option.DNSRecordOptions]{nsRecord}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseExtraRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + extraRecord := mustRecord(t, "ns1.example.com. IN A 192.0.2.53") + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: []mDNS.Question{message.Question[0]}, + Extra: []mDNS.RR{extraRecord.Build()}, + }, nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseExtra: badoption.Listable[option.DNSRecordOptions]{extraRecord}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateDoesNotLeakAddressesToNextQuery(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + var inspectedSelected bool + client := &fakeDNSClient{ + beforeExchange: func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg) { + if transport.Tag() != "selected" { + return + } + inspectedSelected = true + metadata := adapter.ContextFrom(ctx) + require.NotNil(t, metadata) + require.Empty(t, metadata.DestinationAddresses) + require.NotNil(t, metadata.DNSResponse) + }, + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.True(t, inspectedSelected) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateRouteResolutionFailureClearsResponse(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "missing"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("4.4.4.4")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledSecondEvaluateOverwritesFirstResponse(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "first-upstream": &fakeDNSTransport{tag: "first-upstream", transportType: C.DNSTypeUDP}, + "second-upstream": &fakeDNSTransport{tag: "second-upstream", transportType: C.DNSTypeUDP}, + "first-match": &fakeDNSTransport{tag: "first-match", transportType: C.DNSTypeUDP}, + "second-match": &fakeDNSTransport{tag: "second-match", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "first-upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "second-upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + case "first-match": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("7.7.7.7")}, 60), nil + case "second-match": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "first-upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "second-upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "first-match"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 2.2.2.2")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "second-match"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateExchangeFailureUsesMatchResponseBooleanSemantics(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + invert bool + expectedAddr netip.Addr + }{ + { + name: "plain match_response rule stays false", + expectedAddr: netip.MustParseAddr("4.4.4.4"), + }, + { + name: "invert match_response rule becomes true", + invert: true, + expectedAddr: netip.MustParseAddr("8.8.8.8"), + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return nil, E.New("upstream exchange failed") + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + Invert: testCase.invert, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{testCase.expectedAddr}, MessageToAddresses(response)) + }) + } +} + +func TestExchangeLegacyDNSModeDisabledRespondReturnsEvaluatedResponse(t *testing.T) { + t.Parallel() + + var exchanges []string + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + exchanges = append(exchanges, transport.Tag()) + require.Equal(t, "upstream", transport.Tag()) + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + }, + }) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []string{"upstream"}, exchanges) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, MessageToAddresses(response)) +} + +func TestLookupLegacyDNSModeDisabledRespondReturnsEvaluatedResponse(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "upstream", transport.Tag()) + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case mDNS.TypeAAAA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil + default: + return nil, E.New("unexpected qtype") + } + }, + }) + require.False(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("2001:db8::1"), + }, addresses) +} + +func TestExchangeLegacyDNSModeDisabledRespondWithoutEvaluatedResponseReturnsError(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, _ *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "upstream", transport.Tag()) + return nil, E.New("upstream exchange failed") + }, + }) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.Nil(t, response) + require.ErrorContains(t, err, dnsRespondMissingResponseMessage) +} + +func TestLookupLegacyDNSModeDisabledAllowsPartialSuccessForExchangeFailure(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, nil, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "default", transport.Tag()) + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case mDNS.TypeAAAA: + return nil, E.New("ipv6 failed") + default: + return nil, E.New("unexpected qtype") + } + }, + }) + router.legacyDNSMode = false + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) +} + +func TestLookupLegacyDNSModeDisabledAllowsPartialSuccessForRcodeError(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, nil, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "default", transport.Tag()) + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case mDNS.TypeAAAA: + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeNameError, + }, + Question: []mDNS.Question{message.Question[0]}, + }, nil + default: + return nil, E.New("unexpected qtype") + } + }, + }) + router.legacyDNSMode = false + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) +} + +func TestLookupLegacyDNSModeDisabledSkipsFakeIPRule(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "default", transport.Tag()) + if message.Question[0].Qtype == mDNS.TypeA { + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + } + return FixedResponse(0, message.Question[0], nil, 60), nil + }, + }) + router.legacyDNSMode = false + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses) +} + +func TestExchangeLegacyDNSModeDisabledAllowsRouteFakeIPRule(t *testing.T) { + t.Parallel() + + fakeTransport := &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP} + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "fake": fakeTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Same(t, fakeTransport, transport) + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("198.18.0.1")}, 60), nil + }, + }) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("198.18.0.1")}, MessageToAddresses(response)) +} + +func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), + }, + }, + }, + }}) + require.ErrorContains(t, err, "strategy") + require.ErrorContains(t, err, "deprecated") +} + +func TestInitializeRejectsEvaluateFakeIPServerInDefaultRule(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{transports: map[string]adapter.DNSTransport{"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}}}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}) + require.ErrorContains(t, err, "evaluate action cannot use fakeip server") + require.ErrorContains(t, err, "fake") +} + +func TestInitializeRejectsEvaluateFakeIPServerInLogicalRule(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{transports: map[string]adapter.DNSTransport{"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}}}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}) + require.ErrorContains(t, err, "evaluate action cannot use fakeip server") + require.ErrorContains(t, err, "fake") +} + +func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByMatchResponse(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRouteOptions, + RouteOptionsOptions: option.DNSRouteOptionsActionOptions{ + Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), + }, + }, + }, + }}) + require.ErrorContains(t, err, "strategy") + require.ErrorContains(t, err, "deprecated") +} + +func TestInitializeRejectsDNSMatchResponseWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeRejectsDNSRespondWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeRejectsLogicalDNSRespondWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeRejectsEvaluateRuleWithResponseMatchWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + }, + }, + }, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeAllowsEvaluateRuleWithResponseMatchAfterPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"bootstrap.example"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "bootstrap"}, + }, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + }, + }, + }, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + }) + require.NoError(t, err) +} + +func TestLookupLegacyDNSModeDisabledReturnsRejectedErrorForRejectAction(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodDefault, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.Nil(t, addresses) + require.Error(t, err) + require.True(t, rulepkg.IsRejected(err)) +} + +func TestExchangeLegacyDNSModeDisabledReturnsRefusedResponseForRejectAction(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodDefault, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, mDNS.RcodeRefused, response.Rcode) + require.Equal(t, []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, response.Question) +} + +func TestExchangeLegacyDNSModeDisabledReturnsDropErrorForRejectDropAction(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodDrop, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.Nil(t, response) + require.ErrorIs(t, err, tun.ErrDrop) +} + +func TestLookupLegacyDNSModeDisabledFiltersPerQueryTypeAddressesBeforeMerging(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypePredefined, + PredefinedOptions: option.DNSRouteActionPredefined{ + Answer: badoption.Listable[option.DNSRecordOptions]{ + mustRecord(t, "example.com. IN A 1.1.1.1"), + mustRecord(t, "example.com. IN AAAA 2001:db8::1"), + }, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("2001:db8::1"), + }, addresses) +} + +func TestExchangeLegacyDNSModeDisabledLogicalMatchResponseIPCIDRFallsThrough(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("4.4.4.4")}, MessageToAddresses(response)) +} + +func TestLegacyDNSModeReportsLegacyAddressFilterDeprecation(t *testing.T) { + t.Parallel() + + manager := &fakeDeprecatedManager{} + ctx := service.ContextWith[deprecated.Manager](context.Background(), manager) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + client: &fakeDNSClient{}, + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}) + require.NoError(t, err) + + err = router.Start(adapter.StartStateStart) + require.NoError(t, err) + require.Len(t, manager.features, 1) + require.Equal(t, deprecated.OptionLegacyDNSAddressFilter.Name, manager.features[0].Name) +} + +func TestLegacyDNSModeReportsDNSRuleStrategyDeprecation(t *testing.T) { + t.Parallel() + + manager := &fakeDeprecatedManager{} + ctx := service.ContextWith[deprecated.Manager](context.Background(), manager) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + client: &fakeDNSClient{}, + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), + }, + }, + }, + }}) + require.NoError(t, err) + + err = router.Start(adapter.StartStateStart) + require.NoError(t, err) + require.Len(t, manager.features, 1) + require.Equal(t, deprecated.OptionLegacyDNSRuleStrategy.Name, manager.features[0].Name) +} diff --git a/dns/transport/base.go b/dns/transport/base.go new file mode 100644 index 00000000..06e41fd0 --- /dev/null +++ b/dns/transport/base.go @@ -0,0 +1,145 @@ +package transport + +import ( + "context" + "os" + "sync" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +type TransportState int + +const ( + StateNew TransportState = iota + StateStarted + StateClosing + StateClosed +) + +var ( + ErrTransportClosed = os.ErrClosed + ErrConnectionReset = E.New("connection reset") +) + +type BaseTransport struct { + dns.TransportAdapter + Logger logger.ContextLogger + + mutex sync.Mutex + state TransportState + inFlight int32 + queriesComplete chan struct{} + closeCtx context.Context + closeCancel context.CancelFunc +} + +func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport { + ctx, cancel := context.WithCancel(context.Background()) + return &BaseTransport{ + TransportAdapter: adapter, + Logger: logger, + state: StateNew, + closeCtx: ctx, + closeCancel: cancel, + } +} + +func (t *BaseTransport) State() TransportState { + t.mutex.Lock() + defer t.mutex.Unlock() + return t.state +} + +func (t *BaseTransport) SetStarted() error { + t.mutex.Lock() + defer t.mutex.Unlock() + switch t.state { + case StateNew: + t.state = StateStarted + return nil + case StateStarted: + return nil + default: + return ErrTransportClosed + } +} + +func (t *BaseTransport) BeginQuery() bool { + t.mutex.Lock() + defer t.mutex.Unlock() + if t.state != StateStarted { + return false + } + t.inFlight++ + return true +} + +func (t *BaseTransport) EndQuery() { + t.mutex.Lock() + if t.inFlight > 0 { + t.inFlight-- + } + if t.inFlight == 0 && t.queriesComplete != nil { + close(t.queriesComplete) + t.queriesComplete = nil + } + t.mutex.Unlock() +} + +func (t *BaseTransport) CloseContext() context.Context { + return t.closeCtx +} + +func (t *BaseTransport) Shutdown(ctx context.Context) error { + t.mutex.Lock() + + if t.state >= StateClosing { + t.mutex.Unlock() + return nil + } + + if t.state == StateNew { + t.state = StateClosed + t.mutex.Unlock() + t.closeCancel() + return nil + } + + t.state = StateClosing + + if t.inFlight == 0 { + t.state = StateClosed + t.mutex.Unlock() + t.closeCancel() + return nil + } + + t.queriesComplete = make(chan struct{}) + queriesComplete := t.queriesComplete + t.mutex.Unlock() + + t.closeCancel() + + select { + case <-queriesComplete: + t.mutex.Lock() + t.state = StateClosed + t.mutex.Unlock() + return nil + case <-ctx.Done(): + t.mutex.Lock() + t.state = StateClosed + t.mutex.Unlock() + return ctx.Err() + } +} + +func (t *BaseTransport) Close() error { + ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout) + defer cancel() + return t.Shutdown(ctx) +} diff --git a/dns/transport/connector.go b/dns/transport/connector.go new file mode 100644 index 00000000..3a87456d --- /dev/null +++ b/dns/transport/connector.go @@ -0,0 +1,321 @@ +package transport + +import ( + "context" + "net" + "sync" + "time" + + E "github.com/sagernet/sing/common/exceptions" +) + +type ConnectorCallbacks[T any] struct { + IsClosed func(connection T) bool + Close func(connection T) + Reset func(connection T) +} + +type Connector[T any] struct { + dial func(ctx context.Context) (T, error) + callbacks ConnectorCallbacks[T] + + access sync.Mutex + connection T + hasConnection bool + connectionCancel context.CancelFunc + connecting chan struct{} + + closeCtx context.Context + closed bool +} + +func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] { + return &Connector[T]{ + dial: dial, + callbacks: callbacks, + closeCtx: closeCtx, + } +} + +func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] { + return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{ + IsClosed: func(connection *Connection) bool { + return connection.IsClosed() + }, + Close: func(connection *Connection) { + connection.CloseWithError(ErrTransportClosed) + }, + Reset: func(connection *Connection) { + connection.CloseWithError(ErrConnectionReset) + }, + }) +} + +type contextKeyConnecting struct{} + +var errRecursiveConnectorDial = E.New("recursive connector dial") + +type connectorDialResult[T any] struct { + connection T + cancel context.CancelFunc + err error +} + +func (c *Connector[T]) Get(ctx context.Context) (T, error) { + var zero T + for { + c.access.Lock() + + if c.closed { + c.access.Unlock() + return zero, ErrTransportClosed + } + + if c.hasConnection && !c.callbacks.IsClosed(c.connection) { + connection := c.connection + c.access.Unlock() + return connection, nil + } + + c.hasConnection = false + if c.connectionCancel != nil { + c.connectionCancel() + c.connectionCancel = nil + } + if isRecursiveConnectorDial(ctx, c) { + c.access.Unlock() + return zero, errRecursiveConnectorDial + } + + if c.connecting != nil { + connecting := c.connecting + c.access.Unlock() + + select { + case <-connecting: + continue + case <-ctx.Done(): + return zero, ctx.Err() + case <-c.closeCtx.Done(): + return zero, ErrTransportClosed + } + } + + if err := ctx.Err(); err != nil { + c.access.Unlock() + return zero, err + } + + connecting := make(chan struct{}) + c.connecting = connecting + dialContext := context.WithValue(ctx, contextKeyConnecting{}, c) + dialResult := make(chan connectorDialResult[T], 1) + c.access.Unlock() + + go func() { + connection, cancel, err := c.dialWithCancellation(dialContext) + dialResult <- connectorDialResult[T]{ + connection: connection, + cancel: cancel, + err: err, + } + }() + + select { + case result := <-dialResult: + return c.completeDial(ctx, connecting, result) + case <-ctx.Done(): + go func() { + result := <-dialResult + _, _ = c.completeDial(ctx, connecting, result) + }() + return zero, ctx.Err() + case <-c.closeCtx.Done(): + go func() { + result := <-dialResult + _, _ = c.completeDial(ctx, connecting, result) + }() + return zero, ErrTransportClosed + } + } +} + +func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool { + dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T]) + return loaded && dialConnector == connector +} + +func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) { + var zero T + + c.access.Lock() + defer c.access.Unlock() + defer func() { + if c.connecting == connecting { + c.connecting = nil + } + close(connecting) + }() + + if result.err != nil { + return zero, result.err + } + if c.closed || c.closeCtx.Err() != nil { + result.cancel() + c.callbacks.Close(result.connection) + return zero, ErrTransportClosed + } + if err := ctx.Err(); err != nil { + result.cancel() + c.callbacks.Close(result.connection) + return zero, err + } + + c.connection = result.connection + c.hasConnection = true + c.connectionCancel = result.cancel + return c.connection, nil +} + +func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + connCtx, cancel := context.WithCancel(c.closeCtx) + + var ( + stateAccess sync.Mutex + dialComplete bool + ) + stopCancel := context.AfterFunc(ctx, func() { + stateAccess.Lock() + if !dialComplete { + cancel() + } + stateAccess.Unlock() + }) + select { + case <-ctx.Done(): + stateAccess.Lock() + dialComplete = true + stateAccess.Unlock() + stopCancel() + cancel() + return zero, nil, ctx.Err() + default: + } + + connection, err := c.dial(valueContext{connCtx, ctx}) + stateAccess.Lock() + dialComplete = true + stateAccess.Unlock() + stopCancel() + if err != nil { + cancel() + return zero, nil, err + } + return connection, cancel, nil +} + +type valueContext struct { + context.Context + parent context.Context +} + +func (v valueContext) Value(key any) any { + return v.parent.Value(key) +} + +func (v valueContext) Deadline() (time.Time, bool) { + return v.parent.Deadline() +} + +func (c *Connector[T]) Close() error { + c.access.Lock() + defer c.access.Unlock() + + if c.closed { + return nil + } + c.closed = true + + if c.connectionCancel != nil { + c.connectionCancel() + c.connectionCancel = nil + } + if c.hasConnection { + c.callbacks.Close(c.connection) + c.hasConnection = false + } + + return nil +} + +func (c *Connector[T]) Reset() { + c.access.Lock() + defer c.access.Unlock() + + if c.connectionCancel != nil { + c.connectionCancel() + c.connectionCancel = nil + } + if c.hasConnection { + c.callbacks.Reset(c.connection) + c.hasConnection = false + } +} + +type Connection struct { + net.Conn + + closeOnce sync.Once + done chan struct{} + closeError error +} + +func WrapConnection(conn net.Conn) *Connection { + return &Connection{ + Conn: conn, + done: make(chan struct{}), + } +} + +func (c *Connection) Done() <-chan struct{} { + return c.done +} + +func (c *Connection) IsClosed() bool { + select { + case <-c.done: + return true + default: + return false + } +} + +func (c *Connection) CloseError() error { + select { + case <-c.done: + if c.closeError != nil { + return c.closeError + } + return ErrTransportClosed + default: + return nil + } +} + +func (c *Connection) Close() error { + return c.CloseWithError(ErrTransportClosed) +} + +func (c *Connection) CloseWithError(err error) error { + var returnError error + c.closeOnce.Do(func() { + c.closeError = err + returnError = c.Conn.Close() + close(c.done) + }) + return returnError +} diff --git a/dns/transport/connector_test.go b/dns/transport/connector_test.go new file mode 100644 index 00000000..309b28c8 --- /dev/null +++ b/dns/transport/connector_test.go @@ -0,0 +1,407 @@ +package transport + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type testConnectorConnection struct{} + +func TestConnectorRecursiveGetFailsFast(t *testing.T) { + t.Parallel() + + var ( + dialCount atomic.Int32 + closeCount atomic.Int32 + connector *Connector[*testConnectorConnection] + ) + + dial := func(ctx context.Context) (*testConnectorConnection, error) { + dialCount.Add(1) + _, err := connector.Get(ctx) + if err != nil { + return nil, err + } + return &testConnectorConnection{}, nil + } + + connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) { + closeCount.Add(1) + }, + Reset: func(connection *testConnectorConnection) { + closeCount.Add(1) + }, + }) + + _, err := connector.Get(context.Background()) + require.ErrorIs(t, err, errRecursiveConnectorDial) + require.EqualValues(t, 1, dialCount.Load()) + require.EqualValues(t, 0, closeCount.Load()) +} + +func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) { + t.Parallel() + + var ( + outerDialCount atomic.Int32 + innerDialCount atomic.Int32 + outerConnector *Connector[*testConnectorConnection] + innerConnector *Connector[*testConnectorConnection] + ) + + innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + innerDialCount.Add(1) + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) {}, + Reset: func(connection *testConnectorConnection) {}, + }) + + outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + outerDialCount.Add(1) + _, err := innerConnector.Get(ctx) + if err != nil { + return nil, err + } + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) {}, + Reset: func(connection *testConnectorConnection) {}, + }) + + _, err := outerConnector.Get(context.Background()) + require.NoError(t, err) + require.EqualValues(t, 1, outerDialCount.Load()) + require.EqualValues(t, 1, innerDialCount.Load()) +} + +func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) { + t.Parallel() + + type contextKey struct{} + + var ( + dialValue any + dialDeadline time.Time + dialHasDeadline bool + ) + + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + dialValue = ctx.Value(contextKey{}) + dialDeadline, dialHasDeadline = ctx.Deadline() + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) {}, + Reset: func(connection *testConnectorConnection) {}, + }) + + deadline := time.Now().Add(time.Minute) + requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline) + defer cancel() + + _, err := connector.Get(requestContext) + require.NoError(t, err) + require.Equal(t, "test-value", dialValue) + require.True(t, dialHasDeadline) + require.WithinDuration(t, deadline, dialDeadline, time.Second) +} + +func TestConnectorDialSkipsCanceledRequest(t *testing.T) { + t.Parallel() + + var dialCount atomic.Int32 + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + dialCount.Add(1) + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) {}, + Reset: func(connection *testConnectorConnection) {}, + }) + + requestContext, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := connector.Get(requestContext) + require.ErrorIs(t, err, context.Canceled) + require.EqualValues(t, 0, dialCount.Load()) +} + +func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) { + t.Parallel() + + var ( + dialCount atomic.Int32 + closeCount atomic.Int32 + ) + dialStarted := make(chan struct{}, 1) + releaseDial := make(chan struct{}) + + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + dialCount.Add(1) + select { + case dialStarted <- struct{}{}: + default: + } + <-releaseDial + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) { + closeCount.Add(1) + }, + Reset: func(connection *testConnectorConnection) {}, + }) + + requestContext, cancel := context.WithCancel(context.Background()) + result := make(chan error, 1) + go func() { + _, err := connector.Get(requestContext) + result <- err + }() + + <-dialStarted + cancel() + close(releaseDial) + + err := <-result + require.ErrorIs(t, err, context.Canceled) + require.EqualValues(t, 1, dialCount.Load()) + require.Eventually(t, func() bool { + return closeCount.Load() == 1 + }, time.Second, 10*time.Millisecond) + + _, err = connector.Get(context.Background()) + require.NoError(t, err) + require.EqualValues(t, 2, dialCount.Load()) +} + +func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) { + t.Parallel() + + var ( + dialCount atomic.Int32 + closeCount atomic.Int32 + ) + dialStarted := make(chan struct{}, 1) + releaseDial := make(chan struct{}) + + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + dialCount.Add(1) + select { + case dialStarted <- struct{}{}: + default: + } + <-releaseDial + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) { + closeCount.Add(1) + }, + Reset: func(connection *testConnectorConnection) {}, + }) + + requestContext, cancel := context.WithCancel(context.Background()) + result := make(chan error, 1) + go func() { + _, err := connector.Get(requestContext) + result <- err + }() + + <-dialStarted + cancel() + + select { + case err := <-result: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + t.Fatal("Get did not return after request cancel") + } + + require.EqualValues(t, 1, dialCount.Load()) + require.EqualValues(t, 0, closeCount.Load()) + + close(releaseDial) + + require.Eventually(t, func() bool { + return closeCount.Load() == 1 + }, time.Second, 10*time.Millisecond) + + _, err := connector.Get(context.Background()) + require.NoError(t, err) + require.EqualValues(t, 2, dialCount.Load()) +} + +func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) { + t.Parallel() + + var ( + dialCount atomic.Int32 + closeCount atomic.Int32 + ) + firstDialStarted := make(chan struct{}, 1) + secondDialStarted := make(chan struct{}, 1) + releaseFirstDial := make(chan struct{}) + + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + attempt := dialCount.Add(1) + switch attempt { + case 1: + select { + case firstDialStarted <- struct{}{}: + default: + } + <-releaseFirstDial + case 2: + select { + case secondDialStarted <- struct{}{}: + default: + } + } + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) { + closeCount.Add(1) + }, + Reset: func(connection *testConnectorConnection) {}, + }) + + requestContext, cancel := context.WithCancel(context.Background()) + firstResult := make(chan error, 1) + go func() { + _, err := connector.Get(requestContext) + firstResult <- err + }() + + <-firstDialStarted + cancel() + + secondResult := make(chan error, 1) + go func() { + _, err := connector.Get(context.Background()) + secondResult <- err + }() + + select { + case <-secondDialStarted: + t.Fatal("second dial started before first dial completed") + case <-time.After(100 * time.Millisecond): + } + + select { + case err := <-firstResult: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + t.Fatal("first Get did not return after request cancel") + } + + close(releaseFirstDial) + + require.Eventually(t, func() bool { + return closeCount.Load() == 1 + }, time.Second, 10*time.Millisecond) + + select { + case <-secondDialStarted: + case <-time.After(time.Second): + t.Fatal("second dial did not start after first dial completed") + } + + err := <-secondResult + require.NoError(t, err) + require.EqualValues(t, 2, dialCount.Load()) +} + +func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) { + t.Parallel() + + var dialContext context.Context + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + dialContext = ctx + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) {}, + Reset: func(connection *testConnectorConnection) {}, + }) + + requestContext, cancel := context.WithCancel(context.Background()) + _, err := connector.Get(requestContext) + require.NoError(t, err) + require.NotNil(t, dialContext) + + cancel() + + select { + case <-dialContext.Done(): + t.Fatal("dial context canceled by request context after successful dial") + case <-time.After(100 * time.Millisecond): + } + + err = connector.Close() + require.NoError(t, err) +} + +func TestConnectorDialContextCanceledOnClose(t *testing.T) { + t.Parallel() + + var dialContext context.Context + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + dialContext = ctx + return &testConnectorConnection{}, nil + }, ConnectorCallbacks[*testConnectorConnection]{ + IsClosed: func(connection *testConnectorConnection) bool { + return false + }, + Close: func(connection *testConnectorConnection) {}, + Reset: func(connection *testConnectorConnection) {}, + }) + + _, err := connector.Get(context.Background()) + require.NoError(t, err) + require.NotNil(t, dialContext) + + select { + case <-dialContext.Done(): + t.Fatal("dial context canceled before connector close") + default: + } + + err = connector.Close() + require.NoError(t, err) + + select { + case <-dialContext.Done(): + case <-time.After(time.Second): + t.Fatal("dial context not canceled after connector close") + } +} diff --git a/dns/transport/dhcp/dhcp.go b/dns/transport/dhcp/dhcp.go new file mode 100644 index 00000000..3f4eb721 --- /dev/null +++ b/dns/transport/dhcp/dhcp.go @@ -0,0 +1,310 @@ +package dhcp + +import ( + "context" + "errors" + "io" + "net" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/task" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + "github.com/insomniacslk/dhcp/dhcpv4" + mDNS "github.com/miekg/dns" + "golang.org/x/exp/slices" +) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.DHCPDNSServerOptions](registry, C.DNSTypeDHCP, NewTransport) +} + +var _ adapter.DNSTransport = (*Transport)(nil) + +type Transport struct { + dns.TransportAdapter + ctx context.Context + dialer N.Dialer + logger logger.ContextLogger + networkManager adapter.NetworkManager + interfaceName string + interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] + transportLock sync.RWMutex + updatedAt time.Time + lastError error + servers []M.Socksaddr + search []string + ndots int + attempts int +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewLocalDialer(ctx, options.LocalDNSServerOptions) + if err != nil { + return nil, err + } + return &Transport{ + TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions), + ctx: ctx, + dialer: transportDialer, + logger: logger, + networkManager: service.FromContext[adapter.NetworkManager](ctx), + interfaceName: options.Interface, + ndots: 1, + attempts: 2, + }, nil +} + +func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) *Transport { + return &Transport{ + TransportAdapter: transportAdapter, + ctx: ctx, + dialer: dialer, + logger: logger, + networkManager: service.FromContext[adapter.NetworkManager](ctx), + ndots: 1, + attempts: 2, + } +} + +func (t *Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if t.interfaceName == "" { + t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) + } + go func() { + _, err := t.fetch() + if err != nil { + t.logger.Error(E.Cause(err, "fetch DNS servers")) + } + }() + return nil +} + +func (t *Transport) Close() error { + if t.interfaceCallback != nil { + t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) + } + return nil +} + +func (t *Transport) Reset() { + t.transportLock.Lock() + t.updatedAt = time.Time{} + t.servers = nil + t.transportLock.Unlock() +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + servers, err := t.fetch() + if err != nil { + return nil, err + } + if len(servers) == 0 { + return nil, E.New("dhcp: empty DNS servers from response") + } + return t.Exchange0(ctx, message, servers) +} + +func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) { + question := message.Question[0] + domain := dns.FqdnToDomain(question.Name) + if len(servers) == 1 || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { + return t.exchangeSingleRequest(ctx, servers, message, domain) + } else { + return t.exchangeParallel(ctx, servers, message, domain) + } +} + +func (t *Transport) Fetch() []M.Socksaddr { + servers, _ := t.fetch() + return servers +} + +func (t *Transport) fetch() ([]M.Socksaddr, error) { + t.transportLock.RLock() + updatedAt := t.updatedAt + lastError := t.lastError + servers := t.servers + t.transportLock.RUnlock() + if lastError != nil { + return nil, lastError + } + if time.Since(updatedAt) < C.DHCPTTL { + return servers, nil + } + t.transportLock.Lock() + defer t.transportLock.Unlock() + if time.Since(t.updatedAt) < C.DHCPTTL { + return t.servers, nil + } + err := t.updateServers() + if err != nil { + return servers, err + } + return t.servers, nil +} + +func (t *Transport) fetchInterface() (*control.Interface, error) { + if t.interfaceName == "" { + if t.networkManager.InterfaceMonitor() == nil { + return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface") + } + defaultInterface := t.networkManager.InterfaceMonitor().DefaultInterface() + if defaultInterface == nil { + return nil, E.New("missing default interface") + } + return defaultInterface, nil + } else { + return t.networkManager.InterfaceFinder().ByName(t.interfaceName) + } +} + +func (t *Transport) updateServers() error { + iface, err := t.fetchInterface() + if err != nil { + return E.Cause(err, "dhcp: prepare interface") + } + + t.logger.Info("dhcp: query DNS servers on ", iface.Name) + fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout) + err = t.fetchServers0(fetchCtx, iface) + cancel() + t.updatedAt = time.Now() + if err != nil { + t.lastError = err + return err + } else if len(t.servers) == 0 { + t.lastError = E.New("dhcp: empty DNS servers response") + return t.lastError + } else { + t.lastError = nil + return nil + } +} + +func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) { + err := t.updateServers() + if err != nil { + t.logger.Error("update servers: ", err) + } +} + +func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface) error { + var listener net.ListenConfig + listener.Control = control.Append(listener.Control, control.BindToInterface(t.networkManager.InterfaceFinder(), iface.Name, iface.Index)) + listener.Control = control.Append(listener.Control, control.ReuseAddr()) + listenAddr := "0.0.0.0:68" + if runtime.GOOS == "linux" || runtime.GOOS == "android" { + listenAddr = "255.255.255.255:68" + } + var ( + packetConn net.PacketConn + err error + ) + for i := 0; i < 5; i++ { + packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr) + if err == nil || !errors.Is(err, syscall.EADDRINUSE) { + break + } + time.Sleep(time.Second) + } + if err != nil { + return err + } + defer packetConn.Close() + + discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions( + dhcpv4.OptionDomainName, + dhcpv4.OptionDomainNameServer, + dhcpv4.OptionDNSDomainSearchList, + )) + if err != nil { + return err + } + + _, err = packetConn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67}) + if err != nil { + return err + } + + var group task.Group + group.Append0(func(ctx context.Context) error { + return t.fetchServersResponse(iface, packetConn, discovery.TransactionID) + }) + group.Cleanup(func() { + packetConn.Close() + }) + return group.Run(ctx) +} + +func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn net.PacketConn, transactionID dhcpv4.TransactionID) error { + buffer := buf.NewSize(dhcpv4.MaxMessageSize) + defer buffer.Release() + + for { + buffer.Reset() + _, _, err := buffer.ReadPacketFrom(packetConn) + if err != nil { + if errors.Is(err, io.ErrShortBuffer) { + continue + } + return err + } + + dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes()) + if err != nil { + t.logger.Trace("dhcp: parse DHCP response: ", err) + return err + } + + if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer { + t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType()) + continue + } + + if dhcpPacket.TransactionID != transactionID { + t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID) + continue + } + + return t.recreateServers(iface, dhcpPacket) + } +} + +func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error { + searchList := dhcpPacket.DomainSearch() + if searchList != nil && len(searchList.Labels) > 0 { + t.search = searchList.Labels + } else if dhcpPacket.DomainName() != "" { + t.search = []string{dhcpPacket.DomainName()} + } + serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr { + return M.SocksaddrFrom(M.AddrFromIP(it), 53) + }) + if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) { + t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]") + } + t.servers = serverAddrs + return nil +} diff --git a/dns/transport/dhcp/dhcp_shared.go b/dns/transport/dhcp/dhcp_shared.go new file mode 100644 index 00000000..20cd50c5 --- /dev/null +++ b/dns/transport/dhcp/dhcp_shared.go @@ -0,0 +1,205 @@ +package dhcp + +import ( + "context" + "errors" + "math/rand" + "strings" + "syscall" + + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + var lastErr error + for _, fqdn := range t.nameList(domain) { + response, err := t.tryOneName(ctx, servers, fqdn, message) + if err != nil { + lastErr = err + continue + } + return response, nil + } + return nil, lastErr +} + +func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + returned := make(chan struct{}) + defer close(returned) + type queryResult struct { + response *mDNS.Msg + err error + } + results := make(chan queryResult) + startRacer := func(ctx context.Context, fqdn string) { + response, err := t.tryOneName(ctx, servers, fqdn, message) + select { + case results <- queryResult{response, err}: + case <-returned: + } + } + queryCtx, queryCancel := context.WithCancel(ctx) + defer queryCancel() + var nameCount int + for _, fqdn := range t.nameList(domain) { + nameCount++ + go startRacer(queryCtx, fqdn) + } + var errors []error + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case result := <-results: + if result.err == nil { + return result.response, nil + } + errors = append(errors, result.err) + if len(errors) == nameCount { + return nil, E.Errors(errors...) + } + } + } +} + +func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) { + sLen := len(servers) + var lastErr error + for i := 0; i < t.attempts; i++ { + for j := 0; j < sLen; j++ { + server := servers[j] + question := message.Question[0] + question.Name = fqdn + response, err := t.exchangeOne(ctx, server, question) + if err != nil { + lastErr = err + continue + } + return response, nil + } + } + return nil, E.Cause(lastErr, fqdn) +} + +func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) { + if server.Port == 0 { + server.Port = 53 + } + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: uint16(rand.Uint32()), + RecursionDesired: true, + AuthenticatedData: true, + }, + Question: []mDNS.Question{question}, + Compress: true, + } + request.SetEdns0(buf.UDPBufferSize, false) + return t.exchangeUDP(ctx, server, request) +} + +func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server) + if err != nil { + return nil, err + } + defer conn.Close() + if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { + conn.SetDeadline(deadline) + } + buffer := buf.Get(buf.UDPBufferSize) + defer buf.Put(buffer) + rawMessage, err := request.PackBuffer(buffer) + if err != nil { + return nil, E.Cause(err, "pack request") + } + _, err = conn.Write(rawMessage) + if err != nil { + if errors.Is(err, syscall.EMSGSIZE) { + return t.exchangeTCP(ctx, server, request) + } + return nil, E.Cause(err, "write request") + } + n, err := conn.Read(buffer) + if err != nil { + if errors.Is(err, syscall.EMSGSIZE) { + return t.exchangeTCP(ctx, server, request) + } + return nil, E.Cause(err, "read response") + } + var response mDNS.Msg + err = response.Unpack(buffer[:n]) + if err != nil { + return nil, E.Cause(err, "unpack response") + } + if response.Truncated { + return t.exchangeTCP(ctx, server, request) + } + return &response, nil +} + +func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server) + if err != nil { + return nil, err + } + defer conn.Close() + if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { + conn.SetDeadline(deadline) + } + err = transport.WriteMessage(conn, 0, request) + if err != nil { + return nil, err + } + return transport.ReadMessage(conn) +} + +func (t *Transport) nameList(name string) []string { + l := len(name) + rooted := l > 0 && name[l-1] == '.' + if l > 254 || l == 254 && !rooted { + return nil + } + + if rooted { + if avoidDNS(name) { + return nil + } + return []string{name} + } + + hasNdots := strings.Count(name, ".") >= t.ndots + name += "." + // l++ + + names := make([]string, 0, 1+len(t.search)) + if hasNdots && !avoidDNS(name) { + names = append(names, name) + } + for _, suffix := range t.search { + fqdn := name + suffix + if !avoidDNS(fqdn) && len(fqdn) <= 254 { + names = append(names, fqdn) + } + } + if !hasNdots && !avoidDNS(name) { + names = append(names, name) + } + return names +} + +func avoidDNS(name string) bool { + if name == "" { + return true + } + if name[len(name)-1] == '.' { + name = name[:len(name)-1] + } + return strings.HasSuffix(name, ".onion") +} diff --git a/dns/transport/fakeip/fakeip.go b/dns/transport/fakeip/fakeip.go new file mode 100644 index 00000000..9aa41e58 --- /dev/null +++ b/dns/transport/fakeip/fakeip.go @@ -0,0 +1,79 @@ +package fakeip + +import ( + "context" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + mDNS "github.com/miekg/dns" +) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.FakeIPDNSServerOptions](registry, C.DNSTypeFakeIP, NewTransport) +} + +var _ adapter.FakeIPTransport = (*Transport)(nil) + +type Transport struct { + dns.TransportAdapter + logger logger.ContextLogger + store adapter.FakeIPStore + inet4Enabled bool + inet6Enabled bool +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) { + inet4Range := options.Inet4Range.Build(netip.Prefix{}) + inet6Range := options.Inet6Range.Build(netip.Prefix{}) + if !inet4Range.IsValid() && !inet6Range.IsValid() { + return nil, E.New("at least one of inet4_range or inet6_range must be set") + } + store := NewStore(ctx, logger, inet4Range, inet6Range) + return &Transport{ + TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil), + logger: logger, + store: store, + inet4Enabled: inet4Range.IsValid(), + inet6Enabled: inet6Range.IsValid(), + }, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return t.store.Start() +} + +func (t *Transport) Close() error { + return t.store.Close() +} + +func (t *Transport) Reset() { +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA { + return nil, E.New("only IP queries are supported by fakeip") + } + if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled { + return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil + } + address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA) + if err != nil { + return nil, err + } + return dns.FixedResponse(message.Id, question, []netip.Addr{address}, C.DefaultDNSTTL), nil +} + +func (t *Transport) Store() adapter.FakeIPStore { + return t.store +} diff --git a/dns/transport/fakeip/memory.go b/dns/transport/fakeip/memory.go new file mode 100644 index 00000000..0cf8ecc7 --- /dev/null +++ b/dns/transport/fakeip/memory.go @@ -0,0 +1,93 @@ +package fakeip + +import ( + "net/netip" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/logger" +) + +var _ adapter.FakeIPStorage = (*MemoryStorage)(nil) + +type MemoryStorage struct { + addressAccess sync.RWMutex + domainAccess sync.RWMutex + addressCache map[netip.Addr]string + domainCache4 map[string]netip.Addr + domainCache6 map[string]netip.Addr +} + +func NewMemoryStorage() *MemoryStorage { + return &MemoryStorage{ + addressCache: make(map[netip.Addr]string), + domainCache4: make(map[string]netip.Addr), + domainCache6: make(map[string]netip.Addr), + } +} + +func (s *MemoryStorage) FakeIPMetadata() *adapter.FakeIPMetadata { + return nil +} + +func (s *MemoryStorage) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { + return nil +} + +func (s *MemoryStorage) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) { +} + +func (s *MemoryStorage) FakeIPStore(address netip.Addr, domain string) error { + s.addressAccess.Lock() + s.domainAccess.Lock() + if oldDomain, loaded := s.addressCache[address]; loaded { + if address.Is4() { + delete(s.domainCache4, oldDomain) + } else { + delete(s.domainCache6, oldDomain) + } + } + s.addressCache[address] = domain + if address.Is4() { + s.domainCache4[domain] = address + } else { + s.domainCache6[domain] = address + } + s.domainAccess.Unlock() + s.addressAccess.Unlock() + return nil +} + +func (s *MemoryStorage) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) { + _ = s.FakeIPStore(address, domain) +} + +func (s *MemoryStorage) FakeIPLoad(address netip.Addr) (string, bool) { + s.addressAccess.RLock() + defer s.addressAccess.RUnlock() + domain, loaded := s.addressCache[address] + return domain, loaded +} + +func (s *MemoryStorage) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) { + s.domainAccess.RLock() + defer s.domainAccess.RUnlock() + if !isIPv6 { + address, loaded := s.domainCache4[domain] + return address, loaded + } else { + address, loaded := s.domainCache6[domain] + return address, loaded + } +} + +func (s *MemoryStorage) FakeIPReset() error { + s.addressAccess.Lock() + s.domainAccess.Lock() + s.addressCache = make(map[netip.Addr]string) + s.domainCache4 = make(map[string]netip.Addr) + s.domainCache6 = make(map[string]netip.Addr) + s.domainAccess.Unlock() + s.addressAccess.Unlock() + return nil +} diff --git a/dns/transport/fakeip/store.go b/dns/transport/fakeip/store.go new file mode 100644 index 00000000..b7c51dfa --- /dev/null +++ b/dns/transport/fakeip/store.go @@ -0,0 +1,161 @@ +package fakeip + +import ( + "context" + "net/netip" + "sync" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" +) + +var _ adapter.FakeIPStore = (*Store)(nil) + +type Store struct { + ctx context.Context + logger logger.Logger + inet4Range netip.Prefix + inet6Range netip.Prefix + inet4Last netip.Addr + inet6Last netip.Addr + storage adapter.FakeIPStorage + + addressAccess sync.Mutex + inet4Current netip.Addr + inet6Current netip.Addr +} + +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { + store := &Store{ + ctx: ctx, + logger: logger, + inet4Range: inet4Range, + inet6Range: inet6Range, + } + if inet4Range.IsValid() { + store.inet4Last = broadcastAddress(inet4Range) + } + if inet6Range.IsValid() { + store.inet6Last = broadcastAddress(inet6Range) + } + return store +} + +func broadcastAddress(prefix netip.Prefix) netip.Addr { + addr := prefix.Addr() + raw := addr.As16() + bits := prefix.Bits() + if addr.Is4() { + bits += 96 + } + for i := bits; i < 128; i++ { + raw[i/8] |= 1 << (7 - i%8) + } + if addr.Is4() { + return netip.AddrFrom4([4]byte(raw[12:])) + } + return netip.AddrFrom16(raw) +} + +func (s *Store) Start() error { + var storage adapter.FakeIPStorage + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile + } + if storage == nil { + storage = NewMemoryStorage() + } + metadata := storage.FakeIPMetadata() + if metadata != nil && metadata.Inet4Range == s.inet4Range && metadata.Inet6Range == s.inet6Range { + s.inet4Current = metadata.Inet4Current + s.inet6Current = metadata.Inet6Current + } else { + if s.inet4Range.IsValid() { + s.inet4Current = s.inet4Range.Addr().Next() + } + if s.inet6Range.IsValid() { + s.inet6Current = s.inet6Range.Addr().Next() + } + _ = storage.FakeIPReset() + } + s.storage = storage + return nil +} + +func (s *Store) Contains(address netip.Addr) bool { + return s.inet4Range.Contains(address) || s.inet6Range.Contains(address) +} + +func (s *Store) Close() error { + if s.storage == nil { + return nil + } + s.addressAccess.Lock() + metadata := &adapter.FakeIPMetadata{ + Inet4Range: s.inet4Range, + Inet6Range: s.inet6Range, + Inet4Current: s.inet4Current, + Inet6Current: s.inet6Current, + } + s.addressAccess.Unlock() + return s.storage.FakeIPSaveMetadata(metadata) +} + +func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) { + if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded { + return address, nil + } + + s.addressAccess.Lock() + defer s.addressAccess.Unlock() + + // Double-check after acquiring lock + if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded { + return address, nil + } + + var address netip.Addr + if !isIPv6 { + if !s.inet4Current.IsValid() { + return netip.Addr{}, E.New("missing IPv4 fakeip address range") + } + nextAddress := s.inet4Current.Next() + if nextAddress == s.inet4Last || !s.inet4Range.Contains(nextAddress) { + nextAddress = s.inet4Range.Addr().Next().Next() + } + s.inet4Current = nextAddress + address = nextAddress + } else { + if !s.inet6Current.IsValid() { + return netip.Addr{}, E.New("missing IPv6 fakeip address range") + } + nextAddress := s.inet6Current.Next() + if nextAddress == s.inet6Last || !s.inet6Range.Contains(nextAddress) { + nextAddress = s.inet6Range.Addr().Next().Next() + } + s.inet6Current = nextAddress + address = nextAddress + } + err := s.storage.FakeIPStore(address, domain) + if err != nil { + s.logger.Warn("save FakeIP cache: ", err) + } + s.storage.FakeIPSaveMetadataAsync(&adapter.FakeIPMetadata{ + Inet4Range: s.inet4Range, + Inet6Range: s.inet6Range, + Inet4Current: s.inet4Current, + Inet6Current: s.inet6Current, + }) + return address, nil +} + +func (s *Store) Lookup(address netip.Addr) (string, bool) { + return s.storage.FakeIPLoad(address) +} + +func (s *Store) Reset() error { + return s.storage.FakeIPReset() +} diff --git a/dns/transport/hosts/hosts.go b/dns/transport/hosts/hosts.go new file mode 100644 index 00000000..f0e70a9a --- /dev/null +++ b/dns/transport/hosts/hosts.go @@ -0,0 +1,87 @@ +package hosts + +import ( + "context" + "net/netip" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/service/filemanager" + + mDNS "github.com/miekg/dns" +) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport) +} + +var _ adapter.DNSTransport = (*Transport)(nil) + +type Transport struct { + dns.TransportAdapter + files []*File + predefined map[string][]netip.Addr +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) { + var ( + files []*File + predefined = make(map[string][]netip.Addr) + ) + if len(options.Path) == 0 { + files = append(files, NewFile(DefaultPath)) + } else { + for _, path := range options.Path { + files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path)))) + } + } + if options.Predefined != nil { + for _, entry := range options.Predefined.Entries() { + predefined[mDNS.CanonicalName(entry.Key)] = entry.Value + } + } + return &Transport{ + TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil), + files: files, + predefined: predefined, + }, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + return nil +} + +func (t *Transport) Close() error { + return nil +} + +func (t *Transport) Reset() { +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + domain := mDNS.CanonicalName(question.Name) + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + if addresses, ok := t.predefined[domain]; ok { + return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil + } + for _, file := range t.files { + addresses := file.Lookup(domain) + if len(addresses) > 0 { + return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil + } + } + } + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Rcode: mDNS.RcodeNameError, + Response: true, + }, + Question: []mDNS.Question{question}, + }, nil +} diff --git a/dns/transport/hosts/hosts_file.go b/dns/transport/hosts/hosts_file.go new file mode 100644 index 00000000..ec384882 --- /dev/null +++ b/dns/transport/hosts/hosts_file.go @@ -0,0 +1,102 @@ +package hosts + +import ( + "bufio" + "errors" + "io" + "net/netip" + "os" + "strings" + "sync" + "time" + + "github.com/miekg/dns" +) + +const cacheMaxAge = 5 * time.Second + +type File struct { + path string + access sync.Mutex + byName map[string][]netip.Addr + expire time.Time + modTime time.Time + size int64 +} + +func NewFile(path string) *File { + return &File{ + path: path, + } +} + +func (f *File) Lookup(name string) []netip.Addr { + f.access.Lock() + defer f.access.Unlock() + f.update() + return f.byName[dns.CanonicalName(name)] +} + +func (f *File) update() { + now := time.Now() + if now.Before(f.expire) && len(f.byName) > 0 { + return + } + stat, err := os.Stat(f.path) + if err != nil { + return + } + if f.modTime.Equal(stat.ModTime()) && f.size == stat.Size() { + f.expire = now.Add(cacheMaxAge) + return + } + byName := make(map[string][]netip.Addr) + file, err := os.Open(f.path) + if err != nil { + return + } + defer file.Close() + reader := bufio.NewReader(file) + var ( + prefix []byte + line []byte + isPrefix bool + ) + for { + line, isPrefix, err = reader.ReadLine() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return + } + if isPrefix { + prefix = append(prefix, line...) + continue + } else if len(prefix) > 0 { + line = append(prefix, line...) + prefix = nil + } + commentIndex := strings.IndexRune(string(line), '#') + if commentIndex != -1 { + line = line[:commentIndex] + } + fields := strings.Fields(string(line)) + if len(fields) < 2 { + continue + } + var addr netip.Addr + addr, err = netip.ParseAddr(fields[0]) + if err != nil { + continue + } + for index := 1; index < len(fields); index++ { + canonicalName := dns.CanonicalName(fields[index]) + byName[canonicalName] = append(byName[canonicalName], addr) + } + } + f.expire = now.Add(cacheMaxAge) + f.modTime = stat.ModTime() + f.size = stat.Size() + f.byName = byName +} diff --git a/dns/transport/hosts/hosts_test.go b/dns/transport/hosts/hosts_test.go new file mode 100644 index 00000000..3ae160b7 --- /dev/null +++ b/dns/transport/hosts/hosts_test.go @@ -0,0 +1,16 @@ +package hosts_test + +import ( + "net/netip" + "testing" + + "github.com/sagernet/sing-box/dns/transport/hosts" + + "github.com/stretchr/testify/require" +) + +func TestHosts(t *testing.T) { + t.Parallel() + require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost")) + require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost")) +} diff --git a/dns/transport/hosts/hosts_unix.go b/dns/transport/hosts/hosts_unix.go new file mode 100644 index 00000000..4caed8b4 --- /dev/null +++ b/dns/transport/hosts/hosts_unix.go @@ -0,0 +1,5 @@ +//go:build !windows + +package hosts + +var DefaultPath = "/etc/hosts" diff --git a/dns/transport/hosts/hosts_windows.go b/dns/transport/hosts/hosts_windows.go new file mode 100644 index 00000000..3144e50d --- /dev/null +++ b/dns/transport/hosts/hosts_windows.go @@ -0,0 +1,17 @@ +package hosts + +import ( + "path/filepath" + + "golang.org/x/sys/windows" +) + +var DefaultPath string + +func init() { + systemDirectory, err := windows.GetSystemDirectory() + if err != nil { + systemDirectory = "C:\\Windows\\System32" + } + DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts") +} diff --git a/dns/transport/hosts/testdata/hosts b/dns/transport/hosts/testdata/hosts new file mode 100644 index 00000000..9ddcc8c1 --- /dev/null +++ b/dns/transport/hosts/testdata/hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost +::1 localhost diff --git a/dns/transport/https.go b/dns/transport/https.go new file mode 100644 index 00000000..b508e6ea --- /dev/null +++ b/dns/transport/https.go @@ -0,0 +1,224 @@ +package transport + +import ( + "bytes" + "context" + "errors" + "io" + "net" + "net/http" + "net/url" + "strconv" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHTTP "github.com/sagernet/sing/protocol/http" + + mDNS "github.com/miekg/dns" + "golang.org/x/net/http2" +) + +const MimeType = "application/dns-message" + +var _ adapter.DNSTransport = (*HTTPSTransport)(nil) + +func RegisterHTTPS(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS) +} + +type HTTPSTransport struct { + dns.TransportAdapter + logger logger.ContextLogger + dialer N.Dialer + destination *url.URL + headers http.Header + transportAccess sync.Mutex + transport *HTTPSTransportWrapper + transportResetAt time.Time +} + +func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) + if err != nil { + return nil, err + } + tlsOptions := common.PtrValueOrDefault(options.TLS) + tlsOptions.Enabled = true + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) + if err != nil { + return nil, err + } + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) + } + headers := options.Headers.Build() + host := headers.Get("Host") + if host != "" { + headers.Del("Host") + } else { + if tlsConfig.ServerName() != "" { + host = tlsConfig.ServerName() + } else { + host = options.Server + } + } + destinationURL := url.URL{ + Scheme: "https", + Host: host, + } + if destinationURL.Host == "" { + destinationURL.Host = options.Server + } + if options.ServerPort != 0 && options.ServerPort != 443 { + destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort))) + } + path := options.Path + if path == "" { + path = "/dns-query" + } + err = sHTTP.URLSetPath(&destinationURL, path) + if err != nil { + return nil, err + } + serverAddr := options.DNSServerAddressOptions.Build() + if serverAddr.Port == 0 { + serverAddr.Port = 443 + } + if !serverAddr.IsValid() { + return nil, E.New("invalid server address: ", serverAddr) + } + return NewHTTPSRaw( + dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions), + logger, + transportDialer, + &destinationURL, + headers, + serverAddr, + tlsConfig, + ), nil +} + +func NewHTTPSRaw( + adapter dns.TransportAdapter, + logger log.ContextLogger, + dialer N.Dialer, + destination *url.URL, + headers http.Header, + serverAddr M.Socksaddr, + tlsConfig tls.Config, +) *HTTPSTransport { + return &HTTPSTransport{ + TransportAdapter: adapter, + logger: logger, + dialer: dialer, + destination: destination, + headers: headers, + transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr), + } +} + +func (t *HTTPSTransport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return dialer.InitializeDetour(t.dialer) +} + +func (t *HTTPSTransport) Close() error { + t.transportAccess.Lock() + defer t.transportAccess.Unlock() + t.transport.CloseIdleConnections() + t.transport = t.transport.Clone() + return nil +} + +func (t *HTTPSTransport) Reset() { + t.transportAccess.Lock() + defer t.transportAccess.Unlock() + t.transport.CloseIdleConnections() + t.transport = t.transport.Clone() +} + +func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + startAt := time.Now() + response, err := t.exchange(ctx, message) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + t.transportAccess.Lock() + defer t.transportAccess.Unlock() + if t.transportResetAt.After(startAt) { + return nil, err + } + t.transport.CloseIdleConnections() + t.transport = t.transport.Clone() + t.transportResetAt = time.Now() + } + return nil, err + } + return response, nil +} + +func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + exMessage := *message + exMessage.Id = 0 + exMessage.Compress = true + requestBuffer := buf.NewSize(1 + message.Len()) + rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes()) + if err != nil { + requestBuffer.Release() + return nil, err + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage)) + if err != nil { + requestBuffer.Release() + return nil, err + } + request.Header = t.headers.Clone() + request.Header.Set("Content-Type", MimeType) + request.Header.Set("Accept", MimeType) + t.transportAccess.Lock() + currentTransport := t.transport + t.transportAccess.Unlock() + response, err := currentTransport.RoundTrip(request) + requestBuffer.Release() + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, E.New("unexpected status: ", response.Status) + } + var responseMessage mDNS.Msg + if response.ContentLength > 0 { + responseBuffer := buf.NewSize(int(response.ContentLength)) + defer responseBuffer.Release() + _, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength)) + if err != nil { + return nil, err + } + err = responseMessage.Unpack(responseBuffer.Bytes()) + } else { + rawMessage, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + err = responseMessage.Unpack(rawMessage) + } + if err != nil { + return nil, err + } + return &responseMessage, nil +} diff --git a/dns/transport/https_transport.go b/dns/transport/https_transport.go new file mode 100644 index 00000000..84cfa17c --- /dev/null +++ b/dns/transport/https_transport.go @@ -0,0 +1,80 @@ +package transport + +import ( + "context" + "errors" + "net" + "net/http" + "sync/atomic" + + "github.com/sagernet/sing-box/common/tls" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + + "golang.org/x/net/http2" +) + +var errFallback = E.New("fallback to HTTP/1.1") + +type HTTPSTransportWrapper struct { + http2Transport *http2.Transport + httpTransport *http.Transport + fallback *atomic.Bool +} + +func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper { + var fallback atomic.Bool + return &HTTPSTransportWrapper{ + http2Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) { + tlsConn, err := dialer.DialTLSContext(ctx, serverAddr) + if err != nil { + return nil, err + } + state := tlsConn.ConnectionState() + if state.NegotiatedProtocol == http2.NextProtoTLS { + return tlsConn, nil + } + tlsConn.Close() + fallback.Store(true) + return nil, errFallback + }, + }, + httpTransport: &http.Transport{ + DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return dialer.DialTLSContext(ctx, serverAddr) + }, + }, + fallback: &fallback, + } +} + +func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) { + if h.fallback.Load() { + return h.httpTransport.RoundTrip(request) + } else { + response, err := h.http2Transport.RoundTrip(request) + if err != nil { + if errors.Is(err, errFallback) { + return h.httpTransport.RoundTrip(request) + } + return nil, err + } + return response, nil + } +} + +func (h *HTTPSTransportWrapper) CloseIdleConnections() { + h.http2Transport.CloseIdleConnections() + h.httpTransport.CloseIdleConnections() +} + +func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper { + return &HTTPSTransportWrapper{ + httpTransport: h.httpTransport, + http2Transport: &http2.Transport{ + DialTLSContext: h.http2Transport.DialTLSContext, + }, + fallback: h.fallback, + } +} diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go new file mode 100644 index 00000000..a3909acc --- /dev/null +++ b/dns/transport/local/local.go @@ -0,0 +1,94 @@ +//go:build !darwin + +package local + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/hosts" + "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" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport) +} + +var _ adapter.DNSTransport = (*Transport)(nil) + +type Transport struct { + dns.TransportAdapter + ctx context.Context + logger logger.ContextLogger + hosts *hosts.File + dialer N.Dialer + preferGo bool + resolved ResolvedResolver +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewLocalDialer(ctx, options) + if err != nil { + return nil, err + } + return &Transport{ + TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), + ctx: ctx, + logger: logger, + hosts: hosts.NewFile(hosts.DefaultPath), + dialer: transportDialer, + preferGo: options.PreferGo, + }, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateInitialize: + if !t.preferGo { + if isSystemdResolvedManaged() { + resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) + if err == nil { + err = resolvedResolver.Start() + if err == nil { + t.resolved = resolvedResolver + } else { + t.logger.Warn(E.Cause(err, "initialize resolved resolver")) + } + } + } + } + } + return nil +} + +func (t *Transport) Close() error { + if t.resolved != nil { + return t.resolved.Close() + } + return nil +} + +func (t *Transport) Reset() { +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if t.resolved != nil { + return t.resolved.Exchange(ctx, message) + } + question := message.Question[0] + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) + if len(addresses) > 0 { + return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil + } + } + return t.exchange(ctx, message, question.Name) +} diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go new file mode 100644 index 00000000..eb33d64f --- /dev/null +++ b/dns/transport/local/local_darwin.go @@ -0,0 +1,92 @@ +//go:build darwin + +package local + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/hosts" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport) +} + +var _ adapter.DNSTransport = (*Transport)(nil) + +type Transport struct { + dns.TransportAdapter + ctx context.Context + logger logger.ContextLogger + hosts *hosts.File + dialer N.Dialer + fallback bool + dhcpTransport dhcpTransport +} + +type dhcpTransport interface { + adapter.DNSTransport + Fetch() []M.Socksaddr + Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewLocalDialer(ctx, options) + if err != nil { + return nil, err + } + return &Transport{ + TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), + ctx: ctx, + logger: logger, + hosts: hosts.NewFile(hosts.DefaultPath), + dialer: transportDialer, + }, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + inboundManager := service.FromContext[adapter.InboundManager](t.ctx) + for _, inbound := range inboundManager.Inbounds() { + if inbound.Type() == C.TypeTun { + t.fallback = true + break + } + } + if t.fallback { + t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) + if t.dhcpTransport != nil { + err := t.dhcpTransport.Start(stage) + if err != nil { + return err + } + } + } + return nil +} + +func (t *Transport) Close() error { + return common.Close( + t.dhcpTransport, + ) +} + +func (t *Transport) Reset() { + if t.dhcpTransport != nil { + t.dhcpTransport.Reset() + } +} diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go new file mode 100644 index 00000000..318c38f3 --- /dev/null +++ b/dns/transport/local/local_darwin_cgo.go @@ -0,0 +1,249 @@ +//go:build darwin + +package local + +/* +#include +#include +#include + +static void *cgo_dns_open_super() { + return (void *)dns_open(NULL); +} + +static void cgo_dns_close(void *opaque) { + if (opaque != NULL) dns_free((dns_handle_t)opaque); +} + +static int cgo_dns_search(void *opaque, const char *name, int class, int type, + unsigned char *answer, int anslen) { + dns_handle_t handle = (dns_handle_t)opaque; + struct sockaddr_storage from; + uint32_t fromlen = sizeof(from); + return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen); +} + +static void *cgo_res_init() { + res_state state = calloc(1, sizeof(struct __res_state)); + if (state == NULL) return NULL; + if (res_ninit(state) != 0) { + free(state); + return NULL; + } + return state; +} + +static void cgo_res_destroy(void *opaque) { + res_state state = (res_state)opaque; + res_ndestroy(state); + free(state); +} + +static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type, + unsigned char *answer, int anslen, + int timeout_seconds, + int *out_h_errno) { + res_state state = (res_state)opaque; + state->retrans = timeout_seconds; + state->retry = 1; + int n = res_nsearch(state, dname, class, type, answer, anslen); + if (n < 0) { + *out_h_errno = state->res_h_errno; + } + return n; +} +*/ +import "C" + +import ( + "context" + "errors" + "time" + "unsafe" + + boxC "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + E "github.com/sagernet/sing/common/exceptions" + + mDNS "github.com/miekg/dns" +) + +const ( + darwinResolverHostNotFound = 1 + darwinResolverTryAgain = 2 + darwinResolverNoRecovery = 3 + darwinResolverNoData = 4 + + darwinResolverMaxPacketSize = 65535 +) + +var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated") + +func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) { + response, err := darwinSearchWithSystemRouting(name, class, qtype) + if err == nil { + return response, nil + } + fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds) + if fallbackErr == nil || fallbackResponse != nil { + return fallbackResponse, fallbackErr + } + return nil, E.Errors( + E.Cause(err, "dns_search"), + E.Cause(fallbackErr, "res_nsearch"), + ) +} + +func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) { + handle := C.cgo_dns_open_super() + if handle == nil { + return nil, E.New("dns_open failed") + } + defer C.cgo_dns_close(handle) + + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + + bufSize := 1232 + for { + answer := make([]byte, bufSize) + n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype), + (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer))) + if n <= 0 { + return nil, E.New("dns_search failed for ", name) + } + if int(n) > bufSize { + bufSize = int(n) + continue + } + return unpackDarwinResolverMessage(answer[:int(n)], "dns_search") + } +} + +func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) { + state := C.cgo_res_init() + if state == nil { + return nil, E.New("res_ninit failed") + } + defer C.cgo_res_destroy(state) + + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + + bufSize := 1232 + for { + answer := make([]byte, bufSize) + var hErrno C.int + n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype), + (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)), + C.int(timeoutSeconds), + &hErrno) + if n >= 0 { + if int(n) > bufSize { + bufSize = int(n) + continue + } + return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch") + } + response, err := handleDarwinResolvFailure(name, answer, int(hErrno)) + if err == nil { + return response, nil + } + if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize { + bufSize *= 2 + if bufSize > darwinResolverMaxPacketSize { + bufSize = darwinResolverMaxPacketSize + } + continue + } + return nil, err + } +} + +func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) { + var response mDNS.Msg + err := response.Unpack(packet) + if err != nil { + return nil, E.Cause(err, "unpack ", source, " response") + } + return &response, nil +} + +func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) { + response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure") + if err == nil && response.Response { + if response.Truncated && len(answer) < darwinResolverMaxPacketSize { + return nil, errDarwinNeedLargerBuffer + } + return response, nil + } + return nil, darwinResolverHErrno(name, hErrno) +} + +func darwinResolverHErrno(name string, hErrno int) error { + switch hErrno { + case darwinResolverHostNotFound: + return dns.RcodeNameError + case darwinResolverTryAgain: + return dns.RcodeServerFailure + case darwinResolverNoRecovery: + return dns.RcodeServerFailure + case darwinResolverNoData: + return dns.RcodeSuccess + default: + return E.New("res_nsearch: unknown error ", hErrno, " for ", name) + } +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) + if len(addresses) > 0 { + return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil + } + } + if t.fallback && t.dhcpTransport != nil { + dhcpServers := t.dhcpTransport.Fetch() + if len(dhcpServers) > 0 { + return t.dhcpTransport.Exchange0(ctx, message, dhcpServers) + } + } + name := question.Name + timeoutSeconds := int(boxC.DNSTimeout / time.Second) + if deadline, hasDeadline := ctx.Deadline(); hasDeadline { + remaining := time.Until(deadline) + if remaining <= 0 { + return nil, context.DeadlineExceeded + } + seconds := int(remaining.Seconds()) + if seconds < 1 { + seconds = 1 + } + timeoutSeconds = seconds + } + type resolvResult struct { + response *mDNS.Msg + err error + } + resultCh := make(chan resolvResult, 1) + go func() { + response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds) + resultCh <- resolvResult{response, err} + }() + var result resolvResult + select { + case <-ctx.Done(): + return nil, ctx.Err() + case result = <-resultCh: + } + if result.err != nil { + var rcodeError dns.RcodeError + if errors.As(result.err, &rcodeError) { + return dns.FixedResponseStatus(message, int(rcodeError)), nil + } + return nil, result.err + } + result.response.Id = message.Id + return result.response, nil +} diff --git a/dns/transport/local/local_darwin_dhcp.go b/dns/transport/local/local_darwin_dhcp.go new file mode 100644 index 00000000..b228b76a --- /dev/null +++ b/dns/transport/local/local_darwin_dhcp.go @@ -0,0 +1,16 @@ +//go:build darwin && with_dhcp + +package local + +import ( + "context" + + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/dhcp" + "github.com/sagernet/sing-box/log" + N "github.com/sagernet/sing/common/network" +) + +func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport { + return dhcp.NewRawTransport(transportAdapter, ctx, dialer, logger) +} diff --git a/dns/transport/local/local_darwin_nodhcp.go b/dns/transport/local/local_darwin_nodhcp.go new file mode 100644 index 00000000..5ce84690 --- /dev/null +++ b/dns/transport/local/local_darwin_nodhcp.go @@ -0,0 +1,15 @@ +//go:build darwin && !with_dhcp + +package local + +import ( + "context" + + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + N "github.com/sagernet/sing/common/network" +) + +func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport { + return nil +} diff --git a/dns/transport/local/local_resolved.go b/dns/transport/local/local_resolved.go new file mode 100644 index 00000000..e0128d6d --- /dev/null +++ b/dns/transport/local/local_resolved.go @@ -0,0 +1,13 @@ +package local + +import ( + "context" + + mDNS "github.com/miekg/dns" +) + +type ResolvedResolver interface { + Start() error + Close() error + Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) +} diff --git a/dns/transport/local/local_resolved_linux.go b/dns/transport/local/local_resolved_linux.go new file mode 100644 index 00000000..fc3ca2b7 --- /dev/null +++ b/dns/transport/local/local_resolved_linux.go @@ -0,0 +1,501 @@ +package local + +import ( + "bufio" + "context" + "errors" + "net/netip" + "os" + "strings" + "sync" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + dnsTransport "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/service/resolved" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + "github.com/godbus/dbus/v5" + mDNS "github.com/miekg/dns" +) + +func isSystemdResolvedManaged() bool { + resolvContent, err := os.Open("/etc/resolv.conf") + if err != nil { + return false + } + defer resolvContent.Close() + scanner := bufio.NewScanner(resolvContent) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || line[0] != '#' { + return false + } + if strings.Contains(line, "systemd-resolved") { + return true + } + } + return false +} + +type DBusResolvedResolver struct { + ctx context.Context + logger logger.ContextLogger + interfaceMonitor tun.DefaultInterfaceMonitor + interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] + systemBus *dbus.Conn + savedServerSet atomic.Pointer[resolvedServerSet] + closeOnce sync.Once +} + +type resolvedServerSet struct { + servers []resolvedServer +} + +type resolvedServer struct { + primaryTransport adapter.DNSTransport + fallbackTransport adapter.DNSTransport +} + +type resolvedServerSpecification struct { + address netip.Addr + port uint16 + serverName string +} + +func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { + interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() + if interfaceMonitor == nil { + return nil, os.ErrInvalid + } + systemBus, err := dbus.SystemBus() + if err != nil { + return nil, err + } + return &DBusResolvedResolver{ + ctx: ctx, + logger: logger, + interfaceMonitor: interfaceMonitor, + systemBus: systemBus, + }, nil +} + +func (t *DBusResolvedResolver) Start() error { + t.updateStatus() + t.interfaceCallback = t.interfaceMonitor.RegisterCallback(t.updateDefaultInterface) + err := t.systemBus.BusObject().AddMatchSignal( + "org.freedesktop.DBus", + "NameOwnerChanged", + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchArg(0, "org.freedesktop.resolve1"), + ).Err + if err != nil { + return E.Cause(err, "configure resolved restart listener") + } + err = t.systemBus.BusObject().AddMatchSignal( + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + dbus.WithMatchSender("org.freedesktop.resolve1"), + dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"), + ).Err + if err != nil { + return E.Cause(err, "configure resolved properties listener") + } + go t.loopUpdateStatus() + return nil +} + +func (t *DBusResolvedResolver) Close() error { + var closeErr error + t.closeOnce.Do(func() { + serverSet := t.savedServerSet.Swap(nil) + if serverSet != nil { + closeErr = serverSet.Close() + } + if t.interfaceCallback != nil { + t.interfaceMonitor.UnregisterCallback(t.interfaceCallback) + } + if t.systemBus != nil { + _ = t.systemBus.Close() + } + }) + return closeErr +} + +func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + serverSet := t.savedServerSet.Load() + if serverSet == nil { + var err error + serverSet, err = t.checkResolved(context.Background()) + if err != nil { + return nil, err + } + previousServerSet := t.savedServerSet.Swap(serverSet) + if previousServerSet != nil { + _ = previousServerSet.Close() + } + } + response, err := t.exchangeServerSet(ctx, message, serverSet) + if err == nil { + return response, nil + } + t.updateStatus() + refreshedServerSet := t.savedServerSet.Load() + if refreshedServerSet == nil || refreshedServerSet == serverSet { + return nil, err + } + return t.exchangeServerSet(ctx, message, refreshedServerSet) +} + +func (t *DBusResolvedResolver) loopUpdateStatus() { + signalChan := make(chan *dbus.Signal, 1) + t.systemBus.Signal(signalChan) + for signal := range signalChan { + switch signal.Name { + case "org.freedesktop.DBus.NameOwnerChanged": + if len(signal.Body) != 3 { + continue + } + newOwner, loaded := signal.Body[2].(string) + if !loaded || newOwner == "" { + continue + } + t.updateStatus() + case "org.freedesktop.DBus.Properties.PropertiesChanged": + if !shouldUpdateResolvedServerSet(signal) { + continue + } + t.updateStatus() + } + } +} + +func (t *DBusResolvedResolver) updateStatus() { + serverSet, err := t.checkResolved(context.Background()) + oldServerSet := t.savedServerSet.Swap(serverSet) + if oldServerSet != nil { + _ = oldServerSet.Close() + } + if err != nil { + var dbusErr dbus.Error + if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" { + t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable")) + } + if oldServerSet != nil { + t.logger.Debug("systemd-resolved service is gone") + } + return + } else if oldServerSet == nil { + t.logger.Debug("using systemd-resolved service as resolver") + } +} + +func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) { + if serverSet == nil || len(serverSet.servers) == 0 { + return nil, E.New("link has no DNS servers configured") + } + var lastError error + for _, server := range serverSet.servers { + response, err := server.primaryTransport.Exchange(ctx, message) + if err != nil && server.fallbackTransport != nil { + response, err = server.fallbackTransport.Exchange(ctx, message) + } + if err != nil { + lastError = err + continue + } + return response, nil + } + return nil, lastError +} + +func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) { + dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") + err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err + if err != nil { + return nil, err + } + defaultInterface := t.interfaceMonitor.DefaultInterface() + if defaultInterface == nil { + return nil, E.New("missing default interface") + } + call := dbusObject.(*dbus.Object).CallWithContext( + ctx, + "org.freedesktop.resolve1.Manager.GetLink", + 0, + int32(defaultInterface.Index), + ) + if call.Err != nil { + return nil, call.Err + } + var linkPath dbus.ObjectPath + err = call.Store(&linkPath) + if err != nil { + return nil, err + } + linkObject := t.systemBus.Object("org.freedesktop.resolve1", linkPath) + if linkObject == nil { + return nil, E.New("missing link object for default interface") + } + dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject) + if err != nil { + return nil, err + } + linkDNSEx, err := loadResolvedLinkDNSEx(linkObject) + if err != nil { + return nil, err + } + linkDNS, err := loadResolvedLinkDNS(linkObject) + if err != nil { + return nil, err + } + if len(linkDNSEx) == 0 && len(linkDNS) == 0 { + for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() { + if inbound.Type() == C.TypeTun { + return nil, E.New("No appropriate name servers or networks for name found") + } + } + return nil, E.New("link has no DNS servers configured") + } + serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ + BindInterface: defaultInterface.Name, + UDPFragmentDefault: true, + }) + if err != nil { + return nil, err + } + var serverSpecifications []resolvedServerSpecification + if len(linkDNSEx) > 0 { + for _, entry := range linkDNSEx { + serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name) + if !loaded { + continue + } + serverSpecifications = append(serverSpecifications, serverSpecification) + } + } else { + for _, entry := range linkDNS { + serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "") + if !loaded { + continue + } + serverSpecifications = append(serverSpecifications, serverSpecification) + } + } + if len(serverSpecifications) == 0 { + return nil, E.New("no valid DNS servers on link") + } + serverSet := &resolvedServerSet{ + servers: make([]resolvedServer, 0, len(serverSpecifications)), + } + for _, serverSpecification := range serverSpecifications { + server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification) + if createErr != nil { + _ = serverSet.Close() + return nil, createErr + } + serverSet.servers = append(serverSet.servers, server) + } + return serverSet, nil +} + +func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) { + if dnsOverTLSMode == "yes" { + primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) + if err != nil { + return resolvedServer{}, err + } + return resolvedServer{ + primaryTransport: primaryTransport, + }, nil + } + if dnsOverTLSMode == "opportunistic" { + primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) + if err != nil { + return resolvedServer{}, err + } + fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) + if err != nil { + _ = primaryTransport.Close() + return resolvedServer{}, err + } + return resolvedServer{ + primaryTransport: primaryTransport, + fallbackTransport: fallbackTransport, + }, nil + } + primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) + if err != nil { + return resolvedServer{}, err + } + return resolvedServer{ + primaryTransport: primaryTransport, + }, nil +} + +func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) { + serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS)) + if useTLS { + tlsAddress := serverSpecification.address + if tlsAddress.Zone() != "" { + tlsAddress = tlsAddress.WithZone("") + } + serverName := serverSpecification.serverName + if serverName == "" { + serverName = tlsAddress.String() + } + tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{ + Enabled: true, + ServerName: serverName, + }) + if err != nil { + return nil, err + } + serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig) + err = serverTransport.Start(adapter.StartStateStart) + if err != nil { + _ = serverTransport.Close() + return nil, err + } + return serverTransport, nil + } + serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress) + err := serverTransport.Start(adapter.StartStateStart) + if err != nil { + _ = serverTransport.Close() + return nil, err + } + return serverTransport, nil +} + +func (s *resolvedServerSet) Close() error { + var errors []error + for _, server := range s.servers { + errors = append(errors, server.primaryTransport.Close()) + if server.fallbackTransport != nil { + errors = append(errors, server.fallbackTransport.Close()) + } + } + return E.Errors(errors...) +} + +func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) { + address, loaded := netip.AddrFromSlice(rawAddress) + if !loaded { + return resolvedServerSpecification{}, false + } + if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" { + address = address.WithZone(interfaceName) + } + return resolvedServerSpecification{ + address: address, + port: port, + serverName: serverName, + }, true +} + +func resolvedServerPort(port uint16, useTLS bool) uint16 { + if port > 0 { + return port + } + if useTLS { + return 853 + } + return 53 +} + +func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) { + dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS") + if err != nil { + if isResolvedUnknownPropertyError(err) { + return nil, nil + } + return nil, err + } + var linkDNS []resolved.LinkDNS + err = dnsProperty.Store(&linkDNS) + if err != nil { + return nil, err + } + return linkDNS, nil +} + +func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) { + dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx") + if err != nil { + if isResolvedUnknownPropertyError(err) { + return nil, nil + } + return nil, err + } + var linkDNSEx []resolved.LinkDNSEx + err = dnsProperty.Store(&linkDNSEx) + if err != nil { + return nil, err + } + return linkDNSEx, nil +} + +func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) { + dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS") + if err != nil { + if isResolvedUnknownPropertyError(err) { + return "", nil + } + return "", err + } + var dnsOverTLSMode string + err = dnsOverTLSProperty.Store(&dnsOverTLSMode) + if err != nil { + return "", err + } + return dnsOverTLSMode, nil +} + +func isResolvedUnknownPropertyError(err error) bool { + var dbusError dbus.Error + return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty" +} + +func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool { + if len(signal.Body) != 3 { + return true + } + changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant) + if !loaded { + return true + } + for propertyName := range changedProperties { + switch propertyName { + case "DNS", "DNSEx", "DNSOverTLS": + return true + } + } + invalidatedProperties, loaded := signal.Body[2].([]string) + if !loaded { + return true + } + for _, propertyName := range invalidatedProperties { + switch propertyName { + case "DNS", "DNSEx", "DNSOverTLS": + return true + } + } + return false +} + +func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { + t.updateStatus() +} diff --git a/dns/transport/local/local_resolved_stub.go b/dns/transport/local/local_resolved_stub.go new file mode 100644 index 00000000..2e011851 --- /dev/null +++ b/dns/transport/local/local_resolved_stub.go @@ -0,0 +1,18 @@ +//go:build !linux + +package local + +import ( + "context" + "os" + + "github.com/sagernet/sing/common/logger" +) + +func isSystemdResolvedManaged() bool { + return false +} + +func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { + return nil, os.ErrInvalid +} diff --git a/dns/transport/local/local_shared.go b/dns/transport/local/local_shared.go new file mode 100644 index 00000000..64a23a9f --- /dev/null +++ b/dns/transport/local/local_shared.go @@ -0,0 +1,185 @@ +//go:build !darwin + +package local + +import ( + "context" + "errors" + "math/rand" + "syscall" + "time" + + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + 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 { + return t.exchangeParallel(ctx, systemConfig, message, domain) + } +} + +func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + var lastErr error + for _, fqdn := range systemConfig.nameList(domain) { + response, err := t.tryOneName(ctx, systemConfig, fqdn, message) + if err != nil { + lastErr = err + continue + } + return response, nil + } + return nil, lastErr +} + +func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + returned := make(chan struct{}) + defer close(returned) + type queryResult struct { + response *mDNS.Msg + err error + } + results := make(chan queryResult) + startRacer := func(ctx context.Context, fqdn string) { + response, err := t.tryOneName(ctx, systemConfig, fqdn, message) + select { + case results <- queryResult{response, err}: + case <-returned: + } + } + queryCtx, queryCancel := context.WithCancel(ctx) + defer queryCancel() + var nameCount int + for _, fqdn := range systemConfig.nameList(domain) { + nameCount++ + go startRacer(queryCtx, fqdn) + } + var errors []error + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case result := <-results: + if result.err == nil { + return result.response, nil + } + errors = append(errors, result.err) + if len(errors) == nameCount { + return nil, E.Errors(errors...) + } + } + } +} + +func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) { + serverOffset := config.serverOffset() + sLen := uint32(len(config.servers)) + var lastErr error + for i := 0; i < config.attempts; i++ { + for j := uint32(0); j < sLen; j++ { + server := config.servers[(serverOffset+j)%sLen] + question := message.Question[0] + question.Name = fqdn + response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD) + if err != nil { + lastErr = err + continue + } + return response, nil + } + } + return nil, E.Cause(lastErr, fqdn) +} + +func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) { + if server.Port == 0 { + server.Port = 53 + } + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: uint16(rand.Uint32()), + RecursionDesired: true, + AuthenticatedData: ad, + }, + Question: []mDNS.Question{question}, + Compress: true, + } + request.SetEdns0(buf.UDPBufferSize, false) + if !useTCP { + return t.exchangeUDP(ctx, server, request, timeout) + } else { + return t.exchangeTCP(ctx, server, request, timeout) + } +} + +func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server) + if err != nil { + return nil, err + } + defer conn.Close() + if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { + newDeadline := time.Now().Add(timeout) + if deadline.After(newDeadline) { + deadline = newDeadline + } + conn.SetDeadline(deadline) + } + buffer := buf.Get(buf.UDPBufferSize) + defer buf.Put(buffer) + rawMessage, err := request.PackBuffer(buffer) + if err != nil { + return nil, E.Cause(err, "pack request") + } + _, err = conn.Write(rawMessage) + if err != nil { + if errors.Is(err, syscall.EMSGSIZE) { + return t.exchangeTCP(ctx, server, request, timeout) + } + return nil, E.Cause(err, "write request") + } + n, err := conn.Read(buffer) + if err != nil { + if errors.Is(err, syscall.EMSGSIZE) { + return t.exchangeTCP(ctx, server, request, timeout) + } + return nil, E.Cause(err, "read response") + } + var response mDNS.Msg + err = response.Unpack(buffer[:n]) + if err != nil { + return nil, E.Cause(err, "unpack response") + } + if response.Truncated { + return t.exchangeTCP(ctx, server, request, timeout) + } + return &response, nil +} + +func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server) + if err != nil { + return nil, err + } + defer conn.Close() + if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { + newDeadline := time.Now().Add(timeout) + if deadline.After(newDeadline) { + deadline = newDeadline + } + conn.SetDeadline(deadline) + } + err = transport.WriteMessage(conn, 0, request) + if err != nil { + return nil, err + } + return transport.ReadMessage(conn) +} diff --git a/dns/transport/local/resolv.go b/dns/transport/local/resolv.go new file mode 100644 index 00000000..3586cbbf --- /dev/null +++ b/dns/transport/local/resolv.go @@ -0,0 +1,144 @@ +package local + +import ( + "context" + "os" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" +) + +type resolverConfig struct { + initOnce sync.Once + ch chan struct{} + lastChecked time.Time + dnsConfig atomic.Pointer[dnsConfig] +} + +var resolvConf resolverConfig + +func getSystemDNSConfig(ctx context.Context) *dnsConfig { + resolvConf.tryUpdate(ctx, "/etc/resolv.conf") + return resolvConf.dnsConfig.Load() +} + +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(ctx context.Context, name string) { + conf.initOnce.Do(func() { + conf.init(ctx) + }) + + if conf.dnsConfig.Load().noReload { + return + } + if !conf.tryAcquireSema() { + return + } + defer conf.releaseSema() + + now := time.Now() + if conf.lastChecked.After(now.Add(-5 * time.Second)) { + return + } + conf.lastChecked = now + if runtime.GOOS != "windows" { + var mtime time.Time + if fi, err := os.Stat(name); err == nil { + mtime = fi.ModTime() + } + if mtime.Equal(conf.dnsConfig.Load().mtime) { + return + } + } + dnsConf := dnsReadConfig(ctx, name) + conf.dnsConfig.Store(dnsConf) +} + +func (conf *resolverConfig) tryAcquireSema() bool { + select { + case conf.ch <- struct{}{}: + return true + default: + return false + } +} + +func (conf *resolverConfig) releaseSema() { + <-conf.ch +} + +type dnsConfig struct { + servers []string + search []string + ndots int + timeout time.Duration + attempts int + rotate bool + unknownOpt bool + lookup []string + err error + mtime time.Time + soffset uint32 + singleRequest bool + useTCP bool + trustAD bool + noReload bool +} + +func (c *dnsConfig) serverOffset() uint32 { + if c.rotate { + return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start + } + return 0 +} + +func (c *dnsConfig) nameList(name string) []string { + l := len(name) + rooted := l > 0 && name[l-1] == '.' + if l > 254 || l == 254 && !rooted { + return nil + } + + if rooted { + if avoidDNS(name) { + return nil + } + return []string{name} + } + + hasNdots := strings.Count(name, ".") >= c.ndots + name += "." + // l++ + + names := make([]string, 0, 1+len(c.search)) + if hasNdots && !avoidDNS(name) { + names = append(names, name) + } + for _, suffix := range c.search { + fqdn := name + suffix + if !avoidDNS(fqdn) && len(fqdn) <= 254 { + names = append(names, fqdn) + } + } + if !hasNdots && !avoidDNS(name) { + names = append(names, name) + } + return names +} + +func avoidDNS(name string) bool { + if name == "" { + return true + } + if name[len(name)-1] == '.' { + name = name[:len(name)-1] + } + return strings.HasSuffix(name, ".onion") +} diff --git a/dns/transport/local/resolv_default.go b/dns/transport/local/resolv_default.go new file mode 100644 index 00000000..0a7d8810 --- /dev/null +++ b/dns/transport/local/resolv_default.go @@ -0,0 +1,23 @@ +package local + +import ( + "os" + "strings" + _ "unsafe" + + "github.com/miekg/dns" +) + +//go:linkname defaultNS net.defaultNS +var defaultNS []string + +func dnsDefaultSearch() []string { + hn, err := os.Hostname() + if err != nil { + return nil + } + if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 { + return []string{dns.Fqdn(hn[i+1:])} + } + return nil +} diff --git a/dns/transport/local/resolv_test.go b/dns/transport/local/resolv_test.go new file mode 100644 index 00000000..546e8408 --- /dev/null +++ b/dns/transport/local/resolv_test.go @@ -0,0 +1,13 @@ +package local + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDNSReadConfig(t *testing.T) { + t.Parallel() + require.NoError(t, dnsReadConfig(context.Background(), "/etc/resolv.conf").err) +} diff --git a/dns/transport/local/resolv_unix.go b/dns/transport/local/resolv_unix.go new file mode 100644 index 00000000..51512f65 --- /dev/null +++ b/dns/transport/local/resolv_unix.go @@ -0,0 +1,156 @@ +//go:build !windows + +package local + +import ( + "bufio" + "context" + "net" + "net/netip" + "os" + "strings" + "time" + + "github.com/miekg/dns" +) + +func dnsReadConfig(_ context.Context, name string) *dnsConfig { + conf := &dnsConfig{ + ndots: 1, + timeout: 5 * time.Second, + attempts: 2, + } + file, err := os.Open(name) + if err != nil { + conf.servers = defaultNS + conf.search = dnsDefaultSearch() + conf.err = err + return conf + } + defer file.Close() + fi, err := file.Stat() + if err == nil { + conf.mtime = fi.ModTime() + } else { + conf.servers = defaultNS + conf.search = dnsDefaultSearch() + conf.err = err + return conf + } + reader := bufio.NewReader(file) + var ( + prefix []byte + line []byte + isPrefix bool + ) + for { + line, isPrefix, err = reader.ReadLine() + if err != nil { + break + } + if isPrefix { + prefix = append(prefix, line...) + continue + } else if len(prefix) > 0 { + line = append(prefix, line...) + prefix = nil + } + if len(line) > 0 && (line[0] == ';' || line[0] == '#') { + continue + } + f := strings.Fields(string(line)) + if len(f) < 1 { + continue + } + switch f[0] { + case "nameserver": + if len(f) > 1 && len(conf.servers) < 3 { + if _, err := netip.ParseAddr(f[1]); err == nil { + conf.servers = append(conf.servers, net.JoinHostPort(f[1], "53")) + } + } + case "domain": + if len(f) > 1 { + conf.search = []string{dns.Fqdn(f[1])} + } + + case "search": + conf.search = make([]string, 0, len(f)-1) + for i := 1; i < len(f); i++ { + name := dns.Fqdn(f[i]) + if name == "." { + continue + } + conf.search = append(conf.search, name) + } + + case "options": + for _, s := range f[1:] { + switch { + case strings.HasPrefix(s, "ndots:"): + n, _, _ := dtoi(s[6:]) + if n < 0 { + n = 0 + } else if n > 15 { + n = 15 + } + conf.ndots = n + case strings.HasPrefix(s, "timeout:"): + n, _, _ := dtoi(s[8:]) + if n < 1 { + n = 1 + } + conf.timeout = time.Duration(n) * time.Second + case strings.HasPrefix(s, "attempts:"): + n, _, _ := dtoi(s[9:]) + if n < 1 { + n = 1 + } + conf.attempts = n + case s == "rotate": + conf.rotate = true + case s == "single-request" || s == "single-request-reopen": + conf.singleRequest = true + case s == "use-vc" || s == "usevc" || s == "tcp": + conf.useTCP = true + case s == "trust-ad": + conf.trustAD = true + case s == "edns0": + case s == "no-reload": + conf.noReload = true + default: + conf.unknownOpt = true + } + } + + case "lookup": + conf.lookup = f[1:] + + default: + conf.unknownOpt = true + } + } + if len(conf.servers) == 0 { + conf.servers = defaultNS + } + if len(conf.search) == 0 { + conf.search = dnsDefaultSearch() + } + return conf +} + +const big = 0xFFFFFF + +func dtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { + n = n*10 + int(s[i]-'0') + if n >= big { + return big, i, false + } + } + if i == 0 { + return 0, 0, false + } + return n, i, true +} diff --git a/dns/transport/local/resolv_windows.go b/dns/transport/local/resolv_windows.go new file mode 100644 index 00000000..04b8d4ef --- /dev/null +++ b/dns/transport/local/resolv_windows.go @@ -0,0 +1,118 @@ +package local + +import ( + "context" + "net" + "net/netip" + "os" + "strconv" + "syscall" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" + + "golang.org/x/sys/windows" +) + +func dnsReadConfig(ctx context.Context, _ string) *dnsConfig { + conf := &dnsConfig{ + ndots: 1, + timeout: 5 * time.Second, + attempts: 2, + } + defer func() { + if len(conf.servers) == 0 { + conf.servers = defaultNS + } + }() + addresses, err := adapterAddresses() + if err != nil { + return nil + } + var dnsAddresses []struct { + ifName string + netip.Addr + } + for _, address := range addresses { + if address.OperStatus != windows.IfOperStatusUp { + continue + } + if address.IfType == windows.IF_TYPE_TUNNEL { + continue + } + 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 dnsServerAddr netip.Addr + switch sockaddr := rawSockaddr.(type) { + case *syscall.SockaddrInet4: + dnsServerAddr = netip.AddrFrom4(sockaddr.Addr) + case *syscall.SockaddrInet6: + 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 + // deprecated since 2004, see + // https://datatracker.ietf.org/doc/html/rfc3879 + continue + } + dnsServerAddr = netip.AddrFrom16(sockaddr.Addr) + if sockaddr.ZoneId != 0 { + dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10)) + } + default: + // Unexpected type. + continue + } + 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 +} + +func adapterAddresses() ([]*windows.IpAdapterAddresses, error) { + var b []byte + l := uint32(15000) // recommended initial size + for { + b = make([]byte, l) + const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS + err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) + if err == nil { + if l == 0 { + return nil, nil + } + break + } + if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { + return nil, os.NewSyscallError("getadaptersaddresses", err) + } + if l <= uint32(len(b)) { + return nil, os.NewSyscallError("getadaptersaddresses", err) + } + } + var aas []*windows.IpAdapterAddresses + for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next { + aas = append(aas, aa) + } + return aas, nil +} diff --git a/dns/transport/quic/http3.go b/dns/transport/quic/http3.go new file mode 100644 index 00000000..c3a5ca81 --- /dev/null +++ b/dns/transport/quic/http3.go @@ -0,0 +1,205 @@ +package quic + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "net/url" + "strconv" + "sync" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHTTP "github.com/sagernet/sing/protocol/http" + + mDNS "github.com/miekg/dns" +) + +var _ adapter.DNSTransport = (*HTTP3Transport)(nil) + +func RegisterHTTP3Transport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, NewHTTP3) +} + +type HTTP3Transport struct { + dns.TransportAdapter + logger logger.ContextLogger + dialer N.Dialer + destination *url.URL + headers http.Header + serverAddr M.Socksaddr + tlsConfig *tls.STDConfig + transportAccess sync.Mutex + transport *http3.Transport +} + +func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) + if err != nil { + return nil, err + } + tlsOptions := common.PtrValueOrDefault(options.TLS) + tlsOptions.Enabled = true + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) + if err != nil { + return nil, err + } + stdConfig, err := tlsConfig.STDConfig() + if err != nil { + return nil, err + } + headers := options.Headers.Build() + host := headers.Get("Host") + if host != "" { + headers.Del("Host") + } else { + if tlsConfig.ServerName() != "" { + host = tlsConfig.ServerName() + } else { + host = options.Server + } + } + destinationURL := url.URL{ + Scheme: "https", + Host: host, + } + if destinationURL.Host == "" { + destinationURL.Host = options.Server + } + if options.ServerPort != 0 && options.ServerPort != 443 { + destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort))) + } + path := options.Path + if path == "" { + path = "/dns-query" + } + err = sHTTP.URLSetPath(&destinationURL, path) + if err != nil { + return nil, err + } + serverAddr := options.DNSServerAddressOptions.Build() + if serverAddr.Port == 0 { + serverAddr.Port = 443 + } + if !serverAddr.IsValid() { + return nil, E.New("invalid server address: ", serverAddr) + } + t := &HTTP3Transport{ + TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions), + logger: logger, + dialer: transportDialer, + destination: &destinationURL, + headers: headers, + serverAddr: serverAddr, + tlsConfig: stdConfig, + } + t.transport = t.newTransport() + return t, nil +} + +func (t *HTTP3Transport) newTransport() *http3.Transport { + return &http3.Transport{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) { + conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) + if dialErr != nil { + return nil, dialErr + } + quicConn, dialErr := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg) + if dialErr != nil { + conn.Close() + return nil, dialErr + } + return quicConn, nil + }, + TLSClientConfig: t.tlsConfig, + } +} + +func (t *HTTP3Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return dialer.InitializeDetour(t.dialer) +} + +func (t *HTTP3Transport) Close() error { + t.transportAccess.Lock() + defer t.transportAccess.Unlock() + return t.transport.Close() +} + +func (t *HTTP3Transport) Reset() { + t.transportAccess.Lock() + defer t.transportAccess.Unlock() + t.transport.Close() + t.transport = t.newTransport() +} + +func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + exMessage := *message + exMessage.Id = 0 + exMessage.Compress = true + requestBuffer := buf.NewSize(1 + message.Len()) + rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes()) + if err != nil { + requestBuffer.Release() + return nil, err + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage)) + if err != nil { + requestBuffer.Release() + return nil, err + } + request.Header = t.headers.Clone() + request.Header.Set("Content-Type", transport.MimeType) + request.Header.Set("Accept", transport.MimeType) + t.transportAccess.Lock() + currentTransport := t.transport + t.transportAccess.Unlock() + response, err := currentTransport.RoundTrip(request) + requestBuffer.Release() + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, E.New("unexpected status: ", response.Status) + } + var responseMessage mDNS.Msg + if response.ContentLength > 0 { + responseBuffer := buf.NewSize(int(response.ContentLength)) + defer responseBuffer.Release() + _, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength)) + if err != nil { + return nil, err + } + err = responseMessage.Unpack(responseBuffer.Bytes()) + } else { + rawMessage, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + err = responseMessage.Unpack(rawMessage) + } + if err != nil { + return nil, err + } + return &responseMessage, nil +} diff --git a/dns/transport/quic/quic.go b/dns/transport/quic/quic.go new file mode 100644 index 00000000..26461006 --- /dev/null +++ b/dns/transport/quic/quic.go @@ -0,0 +1,209 @@ +package quic + +import ( + "context" + "errors" + "os" + + "github.com/sagernet/quic-go" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + sQUIC "github.com/sagernet/sing-quic" + "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" + + mDNS "github.com/miekg/dns" +) + +var _ adapter.DNSTransport = (*Transport)(nil) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, NewQUIC) +} + +type Transport struct { + *transport.BaseTransport + + ctx context.Context + dialer N.Dialer + serverAddr M.Socksaddr + tlsConfig tls.Config + + connector *transport.Connector[*quic.Conn] +} + +func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) + if err != nil { + return nil, err + } + tlsOptions := common.PtrValueOrDefault(options.TLS) + tlsOptions.Enabled = true + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) + if err != nil { + return nil, err + } + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{"doq"}) + } + serverAddr := options.DNSServerAddressOptions.Build() + if serverAddr.Port == 0 { + serverAddr.Port = 853 + } + if !serverAddr.IsValid() { + return nil, E.New("invalid server address: ", serverAddr) + } + + t := &Transport{ + BaseTransport: transport.NewBaseTransport( + dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), + logger, + ), + ctx: ctx, + dialer: transportDialer, + serverAddr: serverAddr, + tlsConfig: tlsConfig, + } + + t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{ + IsClosed: func(connection *quic.Conn) bool { + return common.Done(connection.Context()) + }, + Close: func(connection *quic.Conn) { + connection.CloseWithError(0, "") + }, + Reset: func(connection *quic.Conn) { + connection.CloseWithError(0, "") + }, + }) + + return t, nil +} + +func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial UDP connection") + } + earlyConnection, err := sQUIC.DialEarly( + ctx, + bufio.NewUnbindPacketConn(conn), + t.serverAddr.UDPAddr(), + t.tlsConfig, + nil, + ) + if err != nil { + conn.Close() + return nil, E.Cause(err, "establish QUIC connection") + } + return earlyConnection, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + err := t.SetStarted() + if err != nil { + return err + } + return dialer.InitializeDetour(t.dialer) +} + +func (t *Transport) Close() error { + return E.Errors(t.BaseTransport.Close(), t.connector.Close()) +} + +func (t *Transport) Reset() { + t.connector.Reset() +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if !t.BeginQuery() { + return nil, transport.ErrTransportClosed + } + defer t.EndQuery() + + var ( + conn *quic.Conn + err error + response *mDNS.Msg + ) + for i := 0; i < 2; i++ { + conn, err = t.connector.Get(ctx) + if err != nil { + return nil, err + } + response, err = t.exchange(ctx, message, conn) + if err == nil { + return response, nil + } else if !isQUICRetryError(err) { + return nil, err + } else { + t.connector.Reset() + continue + } + } + return nil, err +} + +func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn *quic.Conn) (*mDNS.Msg, error) { + stream, err := conn.OpenStreamSync(ctx) + if err != nil { + return nil, E.Cause(err, "open stream") + } + defer stream.CancelRead(0) + err = transport.WriteMessage(stream, 0, message) + if err != nil { + stream.Close() + return nil, E.Cause(err, "write request") + } + stream.Close() + response, err := transport.ReadMessage(stream) + if err != nil { + return nil, E.Cause(err, "read response") + } + return response, nil +} + +// https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394 +func isQUICRetryError(err error) (ok bool) { + if errors.Is(err, os.ErrClosed) { + return true + } + + var qAppErr *quic.ApplicationError + if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { + return true + } + + var qIdleErr *quic.IdleTimeoutError + if errors.As(err, &qIdleErr) { + return true + } + + var resetErr *quic.StatelessResetError + if errors.As(err, &resetErr) { + return true + } + + var qTransportError *quic.TransportError + if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError { + return true + } + + if errors.Is(err, quic.Err0RTTRejected) { + return true + } + + return false +} diff --git a/dns/transport/tcp.go b/dns/transport/tcp.go new file mode 100644 index 00000000..59333de8 --- /dev/null +++ b/dns/transport/tcp.go @@ -0,0 +1,119 @@ +package transport + +import ( + "context" + "encoding/binary" + "io" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +var _ adapter.DNSTransport = (*TCPTransport)(nil) + +func RegisterTCP(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeTCP, NewTCP) +} + +type TCPTransport struct { + dns.TransportAdapter + dialer N.Dialer + serverAddr M.Socksaddr +} + +func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewRemoteDialer(ctx, options) + if err != nil { + return nil, err + } + serverAddr := options.DNSServerAddressOptions.Build() + if serverAddr.Port == 0 { + serverAddr.Port = 53 + } + if !serverAddr.IsValid() { + return nil, E.New("invalid server address: ", serverAddr) + } + return &TCPTransport{ + TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options), + dialer: transportDialer, + serverAddr: serverAddr, + }, nil +} + +func (t *TCPTransport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return dialer.InitializeDetour(t.dialer) +} + +func (t *TCPTransport) Close() error { + return nil +} + +func (t *TCPTransport) Reset() { +} + +func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial TCP connection") + } + defer conn.Close() + err = WriteMessage(conn, 0, message) + if err != nil { + return nil, E.Cause(err, "write request") + } + response, err := ReadMessage(conn) + if err != nil { + return nil, E.Cause(err, "read response") + } + return response, nil +} + +func ReadMessage(reader io.Reader) (*mDNS.Msg, error) { + var responseLen uint16 + err := binary.Read(reader, binary.BigEndian, &responseLen) + if err != nil { + return nil, err + } + if responseLen < 10 { + return nil, mDNS.ErrShortRead + } + buffer := buf.NewSize(int(responseLen)) + defer buffer.Release() + _, err = buffer.ReadFullFrom(reader, int(responseLen)) + if err != nil { + return nil, err + } + var message mDNS.Msg + err = message.Unpack(buffer.Bytes()) + return &message, err +} + +func WriteMessage(writer io.Writer, messageId uint16, message *mDNS.Msg) error { + requestLen := message.Len() + buffer := buf.NewSize(3 + requestLen) + defer buffer.Release() + common.Must(binary.Write(buffer, binary.BigEndian, uint16(requestLen))) + exMessage := *message + exMessage.Id = messageId + exMessage.Compress = true + rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes()) + if err != nil { + return err + } + buffer.Truncate(2 + len(rawMessage)) + return common.Error(writer.Write(buffer.Bytes())) +} diff --git a/dns/transport/tls.go b/dns/transport/tls.go new file mode 100644 index 00000000..4d463296 --- /dev/null +++ b/dns/transport/tls.go @@ -0,0 +1,154 @@ +package transport + +import ( + "context" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" + + mDNS "github.com/miekg/dns" +) + +var _ adapter.DNSTransport = (*TLSTransport)(nil) + +func RegisterTLS(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS) +} + +type TLSTransport struct { + *BaseTransport + + dialer tls.Dialer + serverAddr M.Socksaddr + tlsConfig tls.Config + access sync.Mutex + connections list.List[*tlsDNSConn] +} + +type tlsDNSConn struct { + tls.Conn + queryId uint16 +} + +func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) + if err != nil { + return nil, err + } + tlsOptions := common.PtrValueOrDefault(options.TLS) + tlsOptions.Enabled = true + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) + if err != nil { + return nil, err + } + serverAddr := options.DNSServerAddressOptions.Build() + if serverAddr.Port == 0 { + serverAddr.Port = 853 + } + if !serverAddr.IsValid() { + return nil, E.New("invalid server address: ", serverAddr) + } + 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{ + BaseTransport: NewBaseTransport(adapter, logger), + dialer: tls.NewDialer(dialer, tlsConfig), + serverAddr: serverAddr, + tlsConfig: tlsConfig, + } +} + +func (t *TLSTransport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + err := t.SetStarted() + if err != nil { + return err + } + return dialer.InitializeDetour(t.dialer) +} + +func (t *TLSTransport) Close() error { + t.access.Lock() + for connection := t.connections.Front(); connection != nil; connection = connection.Next() { + connection.Value.Close() + } + t.connections.Init() + t.access.Unlock() + return t.BaseTransport.Close() +} + +func (t *TLSTransport) Reset() { + t.access.Lock() + defer t.access.Unlock() + for connection := t.connections.Front(); connection != nil; connection = connection.Next() { + connection.Value.Close() + } + t.connections.Init() +} + +func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if !t.BeginQuery() { + return nil, ErrTransportClosed + } + defer t.EndQuery() + + t.access.Lock() + conn := t.connections.PopFront() + t.access.Unlock() + if conn != nil { + response, err := t.exchange(ctx, message, conn) + if err == nil { + return response, nil + } + t.Logger.DebugContext(ctx, "discarded pooled connection: ", err) + } + tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial TLS connection") + } + return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn}) +} + +func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { + if deadline, ok := ctx.Deadline(); ok { + conn.SetDeadline(deadline) + } + conn.queryId++ + err := WriteMessage(conn, conn.queryId, message) + if err != nil { + conn.Close() + return nil, E.Cause(err, "write request") + } + response, err := ReadMessage(conn) + if err != nil { + conn.Close() + return nil, E.Cause(err, "read response") + } + t.access.Lock() + if t.State() >= StateClosing { + t.access.Unlock() + conn.Close() + return response, nil + } + conn.SetDeadline(time.Time{}) + t.connections.PushBack(conn) + t.access.Unlock() + return response, nil +} diff --git a/dns/transport/udp.go b/dns/transport/udp.go new file mode 100644 index 00000000..a7272545 --- /dev/null +++ b/dns/transport/udp.go @@ -0,0 +1,258 @@ +package transport + +import ( + "context" + "sync" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +var _ adapter.DNSTransport = (*UDPTransport)(nil) + +func RegisterUDP(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeUDP, NewUDP) +} + +type UDPTransport struct { + *BaseTransport + + dialer N.Dialer + serverAddr M.Socksaddr + udpSize atomic.Int32 + + connector *Connector[*Connection] + + callbackAccess sync.RWMutex + queryId uint16 + callbacks map[uint16]*udpCallback +} + +type udpCallback struct { + access sync.Mutex + response *mDNS.Msg + done chan struct{} +} + +func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) { + transportDialer, err := dns.NewRemoteDialer(ctx, options) + if err != nil { + return nil, err + } + serverAddr := options.DNSServerAddressOptions.Build() + if serverAddr.Port == 0 { + serverAddr.Port = 53 + } + if !serverAddr.IsValid() { + return nil, E.New("invalid server address: ", serverAddr) + } + return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil +} + +func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport { + t := &UDPTransport{ + BaseTransport: NewBaseTransport(adapter, logger), + dialer: dialerInstance, + serverAddr: serverAddr, + callbacks: make(map[uint16]*udpCallback), + } + t.udpSize.Store(2048) + t.connector = NewSingleflightConnector(t.CloseContext(), t.dial) + return t +} + +func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) { + rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial UDP connection") + } + conn := WrapConnection(rawConn) + go t.recvLoop(conn) + return conn, nil +} + +func (t *UDPTransport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + err := t.SetStarted() + if err != nil { + return err + } + return dialer.InitializeDetour(t.dialer) +} + +func (t *UDPTransport) Close() error { + return E.Errors(t.BaseTransport.Close(), t.connector.Close()) +} + +func (t *UDPTransport) Reset() { + t.connector.Reset() +} + +func (t *UDPTransport) nextAvailableQueryId() (uint16, error) { + start := t.queryId + for { + t.queryId++ + if _, exists := t.callbacks[t.queryId]; !exists { + return t.queryId, nil + } + if t.queryId == start { + return 0, E.New("no available query ID") + } + } +} + +func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if !t.BeginQuery() { + return nil, ErrTransportClosed + } + defer t.EndQuery() + + response, err := t.exchange(ctx, message) + if err != nil { + return nil, err + } + if response.Truncated { + t.Logger.InfoContext(ctx, "response truncated, retrying with TCP") + return t.exchangeTCP(ctx, message) + } + return response, nil +} + +func (t *UDPTransport) exchangeTCP(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial TCP connection") + } + defer conn.Close() + err = WriteMessage(conn, message.Id, message) + if err != nil { + return nil, E.Cause(err, "write request") + } + response, err := ReadMessage(conn) + if err != nil { + return nil, E.Cause(err, "read response") + } + return response, nil +} + +func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if edns0Opt := message.IsEdns0(); edns0Opt != nil { + udpSize := int32(edns0Opt.UDPSize()) + for { + current := t.udpSize.Load() + if udpSize <= current { + break + } + if t.udpSize.CompareAndSwap(current, udpSize) { + t.connector.Reset() + break + } + } + } + + conn, err := t.connector.Get(ctx) + if err != nil { + return nil, err + } + + callback := &udpCallback{ + done: make(chan struct{}), + } + + t.callbackAccess.Lock() + queryId, err := t.nextAvailableQueryId() + if err != nil { + t.callbackAccess.Unlock() + return nil, err + } + t.callbacks[queryId] = callback + t.callbackAccess.Unlock() + + defer func() { + t.callbackAccess.Lock() + delete(t.callbacks, queryId) + t.callbackAccess.Unlock() + }() + + buffer := buf.NewSize(1 + message.Len()) + defer buffer.Release() + + exMessage := *message + exMessage.Compress = true + originalId := message.Id + exMessage.Id = queryId + + rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes()) + if err != nil { + return nil, err + } + + _, err = conn.Write(rawMessage) + if err != nil { + conn.CloseWithError(err) + return nil, E.Cause(err, "write request") + } + + select { + case <-callback.done: + callback.response.Id = originalId + return callback.response, nil + case <-conn.Done(): + return nil, conn.CloseError() + case <-t.CloseContext().Done(): + return nil, ErrTransportClosed + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (t *UDPTransport) recvLoop(conn *Connection) { + for { + buffer := buf.NewSize(int(t.udpSize.Load())) + _, err := buffer.ReadOnceFrom(conn) + if err != nil { + buffer.Release() + conn.CloseWithError(err) + return + } + + var message mDNS.Msg + err = message.Unpack(buffer.Bytes()) + buffer.Release() + if err != nil { + t.Logger.Debug("discarded malformed UDP response: ", err) + continue + } + + t.callbackAccess.RLock() + callback, loaded := t.callbacks[message.Id] + t.callbackAccess.RUnlock() + + if !loaded { + continue + } + + callback.access.Lock() + select { + case <-callback.done: + default: + callback.response = &message + close(callback.done) + } + callback.access.Unlock() + } +} diff --git a/dns/transport_adapter.go b/dns/transport_adapter.go new file mode 100644 index 00000000..1e6620f2 --- /dev/null +++ b/dns/transport_adapter.go @@ -0,0 +1,55 @@ +package dns + +import ( + "github.com/sagernet/sing-box/option" +) + +type TransportAdapter struct { + transportType string + transportTag string + dependencies []string +} + +func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter { + return TransportAdapter{ + transportType: transportType, + transportTag: transportTag, + dependencies: dependencies, + } +} + +func NewTransportAdapterWithLocalOptions(transportType string, transportTag string, localOptions option.LocalDNSServerOptions) TransportAdapter { + var dependencies []string + if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" { + dependencies = append(dependencies, localOptions.DomainResolver.Server) + } + return TransportAdapter{ + transportType: transportType, + transportTag: transportTag, + dependencies: dependencies, + } +} + +func NewTransportAdapterWithRemoteOptions(transportType string, transportTag string, remoteOptions option.RemoteDNSServerOptions) TransportAdapter { + var dependencies []string + if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" { + dependencies = append(dependencies, remoteOptions.DomainResolver.Server) + } + return TransportAdapter{ + transportType: transportType, + transportTag: transportTag, + dependencies: dependencies, + } +} + +func (a *TransportAdapter) Type() string { + return a.transportType +} + +func (a *TransportAdapter) Tag() string { + return a.transportTag +} + +func (a *TransportAdapter) Dependencies() []string { + return a.dependencies +} diff --git a/dns/transport_dialer.go b/dns/transport_dialer.go new file mode 100644 index 00000000..971002ac --- /dev/null +++ b/dns/transport_dialer.go @@ -0,0 +1,26 @@ +package dns + +import ( + "context" + + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/option" + N "github.com/sagernet/sing/common/network" +) + +func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) { + return dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + DirectResolver: true, + }) +} + +func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) { + return dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + DirectResolver: true, + }) +} diff --git a/dns/transport_manager.go b/dns/transport_manager.go new file mode 100644 index 00000000..e289ccea --- /dev/null +++ b/dns/transport_manager.go @@ -0,0 +1,300 @@ +package dns + +import ( + "context" + "io" + "os" + "strings" + "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" + "github.com/sagernet/sing/common/logger" +) + +var _ adapter.DNSTransportManager = (*TransportManager)(nil) + +type TransportManager struct { + logger log.ContextLogger + registry adapter.DNSTransportRegistry + outbound adapter.OutboundManager + defaultTag string + access sync.RWMutex + started bool + stage adapter.StartStage + transports []adapter.DNSTransport + transportByTag map[string]adapter.DNSTransport + dependByTag map[string][]string + defaultTransport adapter.DNSTransport + defaultTransportFallback func() (adapter.DNSTransport, error) + fakeIPTransport adapter.FakeIPTransport +} + +func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransportRegistry, outbound adapter.OutboundManager, defaultTag string) *TransportManager { + return &TransportManager{ + logger: logger, + registry: registry, + outbound: outbound, + defaultTag: defaultTag, + transportByTag: make(map[string]adapter.DNSTransport), + dependByTag: make(map[string][]string), + } +} + +func (m *TransportManager) Initialize(defaultTransportFallback func() (adapter.DNSTransport, error)) { + m.defaultTransportFallback = defaultTransportFallback +} + +func (m *TransportManager) Start(stage adapter.StartStage) error { + m.access.Lock() + if m.started && m.stage >= stage { + panic("already started") + } + m.started = true + m.stage = stage + if stage == adapter.StartStateStart { + if m.defaultTag != "" && m.defaultTransport == nil { + m.access.Unlock() + return E.New("default DNS server not found: ", m.defaultTag) + } + if m.defaultTransport == nil { + defaultTransport, err := m.defaultTransportFallback() + if err != nil { + m.access.Unlock() + return E.Cause(err, "default DNS server fallback") + } + m.transports = append(m.transports, defaultTransport) + m.transportByTag[defaultTransport.Tag()] = defaultTransport + m.defaultTransport = defaultTransport + } + transports := m.transports + m.access.Unlock() + return m.startTransports(transports) + } else { + transports := m.transports + m.access.Unlock() + for _, outbound := range transports { + err := adapter.LegacyStart(outbound, stage) + if err != nil { + return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]") + } + } + } + return nil +} + +func (m *TransportManager) startTransports(transports []adapter.DNSTransport) error { + monitor := taskmonitor.New(m.logger, C.StartTimeout) + started := make(map[string]bool) + for { + canContinue := false + startOne: + for _, transportToStart := range transports { + transportTag := transportToStart.Tag() + if started[transportTag] { + continue + } + dependencies := transportToStart.Dependencies() + for _, dependency := range dependencies { + if !started[dependency] { + continue startOne + } + } + started[transportTag] = true + canContinue = true + if starter, isStarter := transportToStart.(adapter.Lifecycle); isStarter { + monitor.Start("start dns/", transportToStart.Type(), "[", transportTag, "]") + err := starter.Start(adapter.StartStateStart) + monitor.Finish() + if err != nil { + return E.Cause(err, "start dns/", transportToStart.Type(), "[", transportTag, "]") + } + } + } + if len(started) == len(transports) { + break + } + if canContinue { + continue + } + currentTransport := common.Find(transports, func(it adapter.DNSTransport) bool { + return !started[it.Tag()] + }) + var lintTransport func(oTree []string, oCurrent adapter.DNSTransport) error + lintTransport = func(oTree []string, oCurrent adapter.DNSTransport) error { + problemTransportTag := common.Find(oCurrent.Dependencies(), func(it string) bool { + return !started[it] + }) + if common.Contains(oTree, problemTransportTag) { + return E.New("circular server dependency: ", strings.Join(oTree, " -> "), " -> ", problemTransportTag) + } + m.access.Lock() + problemTransport := m.transportByTag[problemTransportTag] + m.access.Unlock() + if problemTransport == nil { + return E.New("dependency[", problemTransportTag, "] not found for server[", oCurrent.Tag(), "]") + } + return lintTransport(append(oTree, problemTransportTag), problemTransport) + } + return lintTransport([]string{currentTransport.Tag()}, currentTransport) + } + return nil +} + +func (m *TransportManager) Close() error { + monitor := taskmonitor.New(m.logger, C.StopTimeout) + m.access.Lock() + if !m.started { + m.access.Unlock() + return nil + } + m.started = false + transports := m.transports + m.transports = nil + m.access.Unlock() + var err error + for _, transport := range transports { + if closer, isCloser := transport.(io.Closer); isCloser { + monitor.Start("close server/", transport.Type(), "[", transport.Tag(), "]") + err = E.Append(err, closer.Close(), func(err error) error { + return E.Cause(err, "close server/", transport.Type(), "[", transport.Tag(), "]") + }) + monitor.Finish() + } + } + return nil +} + +func (m *TransportManager) Transports() []adapter.DNSTransport { + m.access.RLock() + defer m.access.RUnlock() + return m.transports +} + +func (m *TransportManager) Transport(tag string) (adapter.DNSTransport, bool) { + m.access.RLock() + outbound, found := m.transportByTag[tag] + m.access.RUnlock() + return outbound, found +} + +func (m *TransportManager) Default() adapter.DNSTransport { + m.access.RLock() + defer m.access.RUnlock() + return m.defaultTransport +} + +func (m *TransportManager) FakeIP() adapter.FakeIPTransport { + m.access.RLock() + defer m.access.RUnlock() + return m.fakeIPTransport +} + +func (m *TransportManager) Remove(tag string) error { + m.access.Lock() + defer m.access.Unlock() + transport, found := m.transportByTag[tag] + if !found { + return os.ErrInvalid + } + delete(m.transportByTag, tag) + index := common.Index(m.transports, func(it adapter.DNSTransport) bool { + return it == transport + }) + if index == -1 { + panic("invalid inbound index") + } + m.transports = append(m.transports[:index], m.transports[index+1:]...) + started := m.started + if m.defaultTransport == transport { + if len(m.transports) > 0 { + nextTransport := m.transports[0] + if nextTransport.Type() != C.DNSTypeFakeIP { + return E.New("default server cannot be fakeip") + } + m.defaultTransport = nextTransport + m.logger.Info("updated default server to ", m.defaultTransport.Tag()) + } else { + m.defaultTransport = nil + } + } + dependBy := m.dependByTag[tag] + if len(dependBy) > 0 { + return E.New("server[", tag, "] is depended by ", strings.Join(dependBy, ", ")) + } + dependencies := transport.Dependencies() + for _, dependency := range dependencies { + if len(m.dependByTag[dependency]) == 1 { + delete(m.dependByTag, dependency) + } else { + m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool { + return it != tag + }) + } + } + if started { + transport.Close() + } + return nil +} + +func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) error { + if tag == "" { + return os.ErrInvalid + } + transport, err := m.registry.CreateDNSTransport(ctx, logger, tag, transportType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + for _, stage := range adapter.ListStartStages { + err = adapter.LegacyStart(transport, stage) + if err != nil { + return E.Cause(err, stage, " dns/", transport.Type(), "[", transport.Tag(), "]") + } + } + } + if existsTransport, loaded := m.transportByTag[tag]; loaded { + if m.started { + err = common.Close(existsTransport) + if err != nil { + return E.Cause(err, "close dns/", existsTransport.Type(), "[", existsTransport.Tag(), "]") + } + } + existsIndex := common.Index(m.transports, func(it adapter.DNSTransport) bool { + return it == existsTransport + }) + if existsIndex == -1 { + panic("invalid inbound index") + } + m.transports = append(m.transports[:existsIndex], m.transports[existsIndex+1:]...) + } + m.transports = append(m.transports, transport) + m.transportByTag[tag] = transport + dependencies := transport.Dependencies() + for _, dependency := range dependencies { + m.dependByTag[dependency] = append(m.dependByTag[dependency], tag) + } + if tag == m.defaultTag || (m.defaultTag == "" && m.defaultTransport == nil) { + if transport.Type() == C.DNSTypeFakeIP { + return E.New("default server cannot be fakeip") + } + m.defaultTransport = transport + if m.started { + m.logger.Info("updated default server to ", transport.Tag()) + } + } + if transport.Type() == C.DNSTypeFakeIP { + if m.fakeIPTransport != nil { + return E.New("multiple fakeip server are not supported") + } + m.fakeIPTransport = transport.(adapter.FakeIPTransport) + } + return nil +} diff --git a/dns/transport_registry.go b/dns/transport_registry.go new file mode 100644 index 00000000..d838158b --- /dev/null +++ b/dns/transport_registry.go @@ -0,0 +1,72 @@ +package dns + +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 TransportConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.DNSTransport, error) + +func RegisterTransport[Options any](registry *TransportRegistry, transportType string, constructor TransportConstructorFunc[Options]) { + registry.register(transportType, func() any { + return new(Options) + }, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.DNSTransport, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.DNSTransportRegistry = (*TransportRegistry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.DNSTransport, error) +) + +type TransportRegistry struct { + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructors map[string]constructorFunc +} + +func NewTransportRegistry() *TransportRegistry { + return &TransportRegistry{ + optionsType: make(map[string]optionsConstructorFunc), + constructors: make(map[string]constructorFunc), + } +} + +func (r *TransportRegistry) CreateOptions(transportType string) (any, bool) { + r.access.Lock() + defer r.access.Unlock() + optionsConstructor, loaded := r.optionsType[transportType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (r *TransportRegistry) CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (adapter.DNSTransport, error) { + r.access.Lock() + defer r.access.Unlock() + constructor, loaded := r.constructors[transportType] + if !loaded { + return nil, E.New("transport type not found: " + transportType) + } + return constructor(ctx, logger, tag, options) +} + +func (r *TransportRegistry) register(transportType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + r.access.Lock() + defer r.access.Unlock() + r.optionsType[transportType] = optionsConstructor + r.constructors[transportType] = constructor +} diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..a4ffb2f2 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +sing-box.sagernet.org \ No newline at end of file diff --git a/docs/assets/icon.svg b/docs/assets/icon.svg new file mode 100644 index 00000000..146d085a --- /dev/null +++ b/docs/assets/icon.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..707b2b71 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,4178 @@ +--- +icon: material/alert-decagram +--- + +#### 1.14.0-alpha.12 + +* Fix fake-ip DNS server should return SUCCESS when another address type is not configured +* Fixes and improvements + +#### 1.13.8 + +* Update naiveproxy to v147.0.7727.49-1 +* Fix fake-ip DNS server should return SUCCESS when another address type is not configured +* Fixes and improvements + +#### 1.14.0-alpha.11 + +* Add optimistic DNS cache **1** +* Update NaiveProxy to 147.0.7727.49 +* Fixes and improvements + +**1**: + +Optimistic DNS cache returns an expired cached response immediately while +refreshing it in the background, reducing tail latency for repeated +queries. Enabled via [`optimistic`](/configuration/dns/#optimistic) +in DNS options, and can be persisted across restarts with the new +[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache +file option. A per-query +[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache) +field is also available on DNS rule actions and the `resolve` route rule +action. + +This deprecates the `independent_cache` DNS option (the DNS cache now +always keys by transport) and the `store_rdrc` cache file option +(replaced by `store_dns`); both will be removed in sing-box 1.16.0. +See [Migration](/migration/#migrate-independent-dns-cache). + +#### 1.14.0-alpha.10 + +* Add `evaluate` DNS rule action and Response Match Fields **1** +* `ip_version` and `query_type` now also take effect on internal DNS lookups **2** +* Add `package_name_regex` route, DNS and headless rule item **3** +* Add cloudflared inbound **4** +* Fixes and improvements + +**1**: + +Response Match Fields +([`response_rcode`](/configuration/dns/rule/#response_rcode), +[`response_answer`](/configuration/dns/rule/#response_answer), +[`response_ns`](/configuration/dns/rule/#response_ns), +and [`response_extra`](/configuration/dns/rule/#response_extra)) +match the evaluated DNS response. They are gated by the new +[`match_response`](/configuration/dns/rule/#match_response) field and +populated by a preceding +[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action; +the evaluated response can also be returned directly by a +[`respond`](/configuration/dns/rule_action/#respond) action. + +This deprecates the Legacy Address Filter Fields (`ip_cidr`, +`ip_is_private` without `match_response`) in DNS rules, the Legacy +`strategy` DNS rule action option, and the Legacy +`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed +in sing-box 1.16.0. +See [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + +**2**: + +`ip_version` and `query_type` in DNS rules, together with `query_type` in +referenced rule-sets, now take effect on every DNS rule evaluation, +including matches from internal domain resolutions that do not target a +specific DNS server (for example a `resolve` route rule action without +`server` set). In earlier versions they were silently ignored in that +path. Combining these fields with any of the legacy DNS fields deprecated +in **1** in the same DNS configuration is no longer supported and is +rejected at startup. +See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules). + +**3**: + +See [Route Rule](/configuration/route/rule/#package_name_regex), +[DNS Rule](/configuration/dns/rule/#package_name_regex) and +[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex). + +**4**: + +See [Cloudflared](/configuration/inbound/cloudflared/). + +#### 1.13.7 + +* Fixes and improvement + +#### 1.13.6 + +* Fixes and improvements + +#### 1.14.0-alpha.8 + +* Add BBR profile and hop interval randomization for Hysteria2 **1** +* Fixes and improvements + +**1**: + +See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile). + +#### 1.14.0-alpha.8 + +* Fixes and improvements + +#### 1.13.5 + +* Fixes and improvements + +#### 1.14.0-alpha.7 + +* Fixes and improvements + +#### 1.13.4 + +* Fixes and improvements + +#### 1.14.0-alpha.4 + +* Refactor ACME support to certificate provider system **1** +* Add Cloudflare Origin CA certificate provider **2** +* Add Tailscale certificate provider **3** +* Fixes and improvements + +**1**: + +See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider). + +**2**: + +See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca). + +**3**: + +See [Tailscale](/configuration/shared/certificate-provider/tailscale). + +#### 1.13.3 + +* Add OpenWrt and Alpine APK packages to release **1** +* Backport to macOS 10.13 High Sierra **2** +* OCM service: Add WebSocket support for Responses API **3** +* Fixes and improvements + +**1**: + +Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix: + +- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk` +- Alpine: `sing-box_{version}_linux_{architecture}.apk` + +**2**: + +Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support +macOS 10.13 High Sierra, built using Go 1.25 with patches +from [SagerNet/go](https://github.com/SagerNet/go). + +**3**: + +See [OCM](/configuration/service/ocm). + +#### 1.12.24 + +* Fixes and improvements + +#### 1.14.0-alpha.2 + +* Add OpenWrt and Alpine APK packages to release **1** +* Backport to macOS 10.13 High Sierra **2** +* OCM service: Add WebSocket support for Responses API **3** +* Fixes and improvements + +**1**: + +Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix: + +- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk` +- Alpine: `sing-box_{version}_linux_{architecture}.apk` + +**2**: + +Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support +macOS 10.13 High Sierra, built using Go 1.25 with patches +from [SagerNet/go](https://github.com/SagerNet/go). + +**3**: + +See [OCM](/configuration/service/ocm). + +#### 1.14.0-alpha.1 + +* Add `source_mac_address` and `source_hostname` rule items **1** +* Add `include_mac_address` and `exclude_mac_address` TUN options **2** +* Update NaiveProxy to 145.0.7632.159 **3** +* Fixes and improvements + +**1**: + +New rule items for matching LAN devices by MAC address and hostname via neighbor resolution. +Supported on Linux, macOS, or in graphical clients on Android and macOS. + +See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/). + +**2**: + +Limit or exclude devices from TUN routing by MAC address. +Only supported on Linux with `auto_route` and `auto_redirect` enabled. + +See [TUN](/configuration/inbound/tun/#include_mac_address). + +**3**: + +This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S. + +#### 1.13.2 + +* Fixes and improvements + +#### 1.13.1 + +* Fixes and improvements + +#### 1.12.14 + +* Backport fixes + +#### 1.13.0 + +Important changes since 1.12: + +* Add NaiveProxy outbound **1** +* Add pre-match support for `auto_redirect` **2** +* Improve `auto_redirect` **3** +* Add Chrome Root Store certificate option **4** +* Add new options for ACME DNS-01 challenge providers **5** +* Add Wi-Fi state support for Linux and Windows **6** +* Add curve preferences, pinned public key SHA256, mTLS and ECH `query_server_name` for TLS options **7** +* Add kTLS support **8** +* Add ICMP echo (ping) proxy support **9** +* Add `interface_address`, `network_interface_address` and `default_interface_address` rule items **10** +* Add `preferred_by` route rule item **11** +* Improve `local` DNS server **12** +* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for listen and dial fields **13** +* Add `bind_address_no_port` option for dial fields **14** +* Add system interface, relay server and advertise tags options for Tailscale endpoint **15** +* Add Claude Code Multiplexer service **16** +* Add OpenAI Codex Multiplexer service **17** +* Apple/Android: Refactor GUI +* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs) +* Android: Add support for resisting VPN detection via Xposed +* Drop support for go1.23 **18** +* Drop support for Android 5.0 **19** +* Update uTLS to v1.8.2 **20** +* Update quic-go to v0.59.0 +* Update gVisor to v20250811 +* Update Tailscale to v1.92.4 + +**1**: + +NaiveProxy outbound now supports QUIC, ECH, UDP over TCP, and configurable QUIC congestion control. + +Only available on Apple platforms, Android, Windows and some Linux architectures. +Each Windows release includes `libcronet.dll` — +ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`. + +See [NaiveProxy outbound](/configuration/outbound/naive/). + +**2**: + +`auto_redirect` now allows you to bypass sing-box for connections based on routing rules. + +A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly. + +This feature requires Linux with `auto_redirect` enabled. + +See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass). + +**3**: + +`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues. +You can change it to bypass sing-box via the new `exclude_mptcp` option. + +Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), +ensuring traffic is routed to the sing-box table when no route is found in system tables. +The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). + +See [TUN](/configuration/inbound/tun/#exclude_mptcp). + +**4**: + +Adds `chrome` as a new certificate store option alongside `mozilla`. +Both stores filter out China-based CA certificates. + +See [Certificate](/configuration/certificate/#store). + +**5**: + +See [DNS-01 Challenge](/configuration/shared/dns01_challenge/). + +**6**: + +sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`. + +See [Wi-Fi State](/configuration/shared/wifi-state/). + +**7**: + +See [TLS](/configuration/shared/tls/). + +**8**: + +Adds `kernel_tx` and `kernel_rx` options for TLS inbound. +Enables kernel-level TLS offloading via `splice(2)` on Linux 5.1+ with TLS 1.3. + +See [TLS](/configuration/shared/tls/). + +**9**: + +sing-box can now proxy ICMP echo (ping) requests. +A new `icmp` network type is available for route rules. +Supported from TUN, WireGuard and Tailscale inbounds to Direct, WireGuard and Tailscale outbounds. +The `reject` action can also reply to ICMP echo requests. + +**10**: + +New rule items for matching based on interface IP addresses, available in route rules, DNS rules and rule-sets. + +**11**: + +Matches outbounds' preferred routes. +For Tailscale: MagicDNS domains and peers' allowed IPs. For WireGuard: peers' allowed IPs. + +**12**: + +The `local` DNS server now uses platform-native resolution: +`getaddrinfo`/libresolv on Apple platforms, systemd-resolved DBus on Linux. +A new `prefer_go` option is available to opt out. + +See [Local DNS](/configuration/dns/server/local/). + +**13**: + +The default TCP keep-alive initial period has been updated from 10 minutes to 5 minutes. + +See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive). + +**14**: + +Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address. + +This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios. + +See [Dial Fields](/configuration/shared/dial/#bind_address_no_port). + +**15**: + +Tailscale endpoint can now create a system TUN interface to handle traffic directly. +New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections. +New `advertise_tags` option for ACL tag advertisement. + +See [Tailscale endpoint](/configuration/endpoint/tailscale/). + +**16**: + +CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients. + +See [CCM](/configuration/service/ccm). + +**17**: + +See [OCM](/configuration/service/ocm). + +**18**: + +Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile. + +**19**: + +Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0, +and only through a separate legacy build (with `-legacy-android-5` suffix). + +For standalone binaries, the minimum Android version has been raised to Android 6.0, +since Termux requires Android 7.0 or later. + +**20**: + +This update fixes missing padding extension for Chrome 120+ fingerprints. + +Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. +uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; +use NaiveProxy instead for TLS fingerprint resistance. + +#### 1.12.23 + +* Fixes and improvements + +#### 1.13.0-rc.5 + +* Add `mipsle`, `mips64le`, `riscv64` and `loong64` support for NaiveProxy outbound + +#### 1.12.22 + +* Fixes and improvements + +#### 1.13.0-rc.3 + +* Fixes and improvements + +#### 1.12.21 + +* Fixes and improvements + +#### 1.13.0-rc.2 + +* Fixes and improvements + +#### 1.12.20 + +* Fixes and improvements + +#### 1.13.0-rc.1 + +* Fixes and improvements + +#### 1.12.19 + +* Fixes and improvements + +#### 1.13.0-beta.8 + +* Add fallback routing rule for `auto_redirect` **1** +* Fixes and improvements + +**1**: + +Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), +ensuring traffic is routed to the sing-box table when no route is found in system tables. + +The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). + +#### 1.12.18 + +* Add fallback routing rule for `auto_redirect` **1** +* Fixes and improvements + +**1**: + +Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), +ensuring traffic is routed to the sing-box table when no route is found in system tables. + +The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). + +#### 1.13.0-beta.6 + +* Update uTLS to v1.8.2 **1** +* Fixes and improvements + +**1**: + +This update fixes missing padding extension for Chrome 120+ fingerprints. + +Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. +uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; +use NaiveProxy instead for TLS fingerprint resistance. + +#### 1.12.17 + +* Update uTLS to v1.8.2 **1** +* Fixes and improvements + +**1**: + +This update fixes missing padding extension for Chrome 120+ fingerprints. + +Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. +uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; +use NaiveProxy instead for TLS fingerprint resistance. + +#### 1.13.0-beta.5 + +* Fixes and improvements + +#### 1.12.16 + +* Fixes and improvements + +#### 1.13.0-beta.4 + +* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs) +* Android: Add support for resisting VPN detection via Xposed +* Update quic-go to v0.59.0 +* Fixes and improvements + +#### 1.13.0-beta.2 + +* Add `bind_address_no_port` option for dial fields **1** +* Fixes and improvements + +**1**: + +Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address. + +This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios. + +See [Dial Fields](/configuration/shared/dial/#bind_address_no_port). + +#### 1.13.0-beta.1 + +* Add system interface support for Tailscale endpoint **1** +* Fixes and improvements + +**1**: + +Tailscale endpoint can now create a system TUN interface to handle traffic directly. + +See [Tailscale endpoint](/configuration/endpoint/tailscale/#system_interface). + +#### 1.12.15 + +* Fixes and improvements + +#### 1.13.0-alpha.36 + +* Downgrade quic-go to v0.57.1 +* Fixes and improvements + +#### 1.13.0-alpha.35 + +* Add pre-match support for `auto_redirect` **1** +* Fixes and improvements + +**1**: + +`auto_redirect` now allows you to bypass sing-box for connections based on routing rules. + +A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly. + +This feature requires Linux with `auto_redirect` enabled. + +See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass). + +#### 1.13.0-alpha.34 + +* Add Chrome Root Store certificate option **1** +* Add new options for ACME DNS-01 challenge providers **2** +* Add Wi-Fi state support for Linux and Windows **3** +* Update naiveproxy to 143.0.7499.109 +* Update quic-go to v0.58.0 +* Update tailscale to v1.92.4 +* Drop support for go1.23 **4** +* Drop support for Android 5.0 **5** + +**1**: + +Adds `chrome` as a new certificate store option alongside `mozilla`. +Both stores filter out China-based CA certificates. + +See [Certificate](/configuration/certificate/#store). + +**2**: + +See [DNS-01 Challenge](/configuration/shared/dns01_challenge/). + +**3**: + +sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`. + +See [Wi-Fi State](/configuration/shared/wifi-state/). + +**4**: + +Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile. + +**5**: + +Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0, +and only through a separate legacy build (with `-legacy-android-5` suffix). + +For standalone binaries, the minimum Android version has been raised to Android 6.0, +since Termux requires Android 7.0 or later. + +#### 1.12.14 + +* Fixes and improvements + +#### 1.13.0-alpha.33 + +* Fixes and improvements + +#### 1.13.0-alpha.32 + +* Remove `certificate_public_key_sha256` option for NaiveProxy outbound **1** +* Fixes and improvements + +**1**: + +Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis. +For this reason, and due to maintenance costs, there is no reason to continue supporting `certificate_public_key_sha256`, which was designed to simplify the use of self-signed certificates. + +#### 1.13.0-alpha.31 + +* Add QUIC support for NaiveProxy outbound **1** +* Add QUIC congestion control option for NaiveProxy **2** +* Fixes and improvements + +**1**: + +NaiveProxy outbound now supports QUIC. + +See [NaiveProxy outbound](/configuration/outbound/naive/#quic). + +**2**: + +NaiveProxy inbound and outbound now supports configurable QUIC congestion control algorithms, including BBR and BBRv2. + +See [NaiveProxy inbound](/configuration/inbound/naive/#quic_congestion_control) and [NaiveProxy outbound](/configuration/outbound/naive/#quic_congestion_control). + +#### 1.13.0-alpha.30 + +* Add ECH support for NaiveProxy outbound **1** +* Add `tls.ech.query_server_name` option **2** +* Fix NaiveProxy outbound on Windows **3** +* Add OpenAI Codex Multiplexer service **4** +* Fixes and improvements + +**1**: + +See [NaiveProxy outbound](/configuration/outbound/naive/#tls). + +**2**: + +See [TLS](/configuration/shared/tls/#query_server_name). + +**3**: + +Each Windows release now includes `libcronet.dll`. +Ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`. + +**4**: + +See [OCM](/configuration/service/ocm). + +#### 1.13.0-alpha.29 + +* Add UDP over TCP support for naiveproxy outbound **1** +* Fixes and improvements + +**1**: + +See [NaiveProxy outbound](/configuration/outbound/naive/#udp_over_tcp). + +#### 1.13.0-alpha.28 + +* Add naiveproxy outbound **1** +* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for dial fields **2** +* Update default TCP keep-alive initial period from 10 minutes to 5 minutes +* Update quic-go to v0.57.1 +* Fixes and improvements + +**1**: + +Only available on Apple platforms, Android, Windows and some Linux architectures. + +See [NaiveProxy outbound](/configuration/outbound/naive/). + +**2**: + +See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive). + +* __Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client: +because system extensions require signatures to function, we have had to temporarily halt its release.__ + +__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then, +only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__ + + +#### 1.12.13 + +* Fix naive inbound +* Fixes and improvements + +__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client: +because system extensions require signatures to function, we have had to temporarily halt its release.__ + +__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then, +only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__ + +#### 1.12.12 + +* Fixes and improvements + +#### 1.13.0-alpha.26 + +* Update quic-go to v0.55.0 +* Fix memory leak in hysteria2 +* Fixes and improvements + +#### 1.12.11 + +* Fixes and improvements + +#### 1.13.0-alpha.24 + +* Add Claude Code Multiplexer service **1** +* Fixes and improvements + +**1**: + +CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients. + +See [CCM](/configuration/service/ccm). + +#### 1.13.0-alpha.23 + +* Fix compatibility with MPTCP **1** +* Fixes and improvements + +**1**: + +`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues, +but you can change it to bypass the sing-box via the new `exclude_mptcp` option. + +See [TUN](/configuration/inbound/tun/#exclude_mptcp). + +#### 1.13.0-alpha.22 + +* Update uTLS to v1.8.1 **1** +* Fixes and improvements + +**1**: + +This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected, +see https://github.com/refraction-networking/utls/pull/375. + +#### 1.12.10 + +* Update uTLS to v1.8.1 **1** +* Fixes and improvements + +**1**: + +This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected, +see https://github.com/refraction-networking/utls/pull/375. + +#### 1.13.0-alpha.21 + +* Fix missing mTLS support in client options **1** +* Fixes and improvements + +See [TLS](/configuration/shared/tls/). + +#### 1.12.9 + +* Fixes and improvements + +#### 1.13.0-alpha.16 + +* Add curve preferences, pinned public key SHA256 and mTLS for TLS options **1** +* Fixes and improvements + +See [TLS](/configuration/shared/tls/). + +#### 1.13.0-alpha.15 + +* Update quic-go to v0.54.0 +* Update gVisor to v20250811 +* Update Tailscale to v1.86.5 +* Fixes and improvements + +#### 1.12.8 + +* Fixes and improvements + +#### 1.13.0-alpha.11 + +* Fixes and improvements + +#### 1.12.5 + +* Fixes and improvements + +#### 1.13.0-alpha.10 + +* Improve kTLS support **1** +* Fixes and improvements + +**1**: + +kTLS is now compatible with custom TLS implementations other than uTLS. + +#### 1.12.4 + +* Fixes and improvements + +#### 1.12.3 + +* Fixes and improvements + +#### 1.12.2 + +* Fixes and improvements + +#### 1.12.1 + +* Fixes and improvements + +#### 1.12.0 + +* Refactor DNS servers **1** +* Add domain resolver options**2** +* Add TLS fragment/record fragment support to route options and outbound TLS options **3** +* Add certificate options **4** +* Add Tailscale endpoint and DNS server **5** +* Drop support for go1.22 **6** +* Add AnyTLS protocol **7** +* Migrate to stdlib ECH implementation **8** +* Add NTP sniffer **9** +* Add wildcard SNI support for ShadowTLS inbound **10** +* Improve `auto_redirect` **11** +* Add control options for listeners **12** +* Add DERP service **13** +* Add Resolved service and DNS server **14** +* Add SSM API service **15** +* Add loopback address support for tun **16** +* Improve tun performance on Apple platforms **17** +* Update quic-go to v0.52.0 +* Update gVisor to 20250319.0 +* Update the status of graphical clients in stores **18** + +**1**: + +DNS servers are refactored for better performance and scalability. + +See [DNS server](/configuration/dns/server/). + +For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats). + +Compatibility for old formats will be removed in sing-box 1.14.0. + +**2**: + +Legacy `outbound` DNS rules are deprecated +and can be replaced by the new `domain_resolver` option. + +See [Dial Fields](/configuration/shared/dial/#domain_resolver) and +[Route](/configuration/route/#default_domain_resolver). + +For migration, +see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). + +**3**: + +See [Route Action](/configuration/route/rule_action/#tls_fragment) and [TLS](/configuration/shared/tls/). + +**4**: + +New certificate options allow you to manage the default list of trusted X509 CA certificates. + +For the system certificate list, fixed Go not reading Android trusted certificates correctly. + +You can also use the Mozilla Included List instead, or add trusted certificates yourself. + +See [Certificate](/configuration/certificate/). + +**5**: + +See [Tailscale](/configuration/endpoint/tailscale/). + +**6**: + +Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. + +For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches +from [MetaCubeX/go](https://github.com/MetaCubeX/go). + +**7**: + +The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme. + +See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/). + +**8**: + +See [TLS](/configuration/shared/tls). + +The build tag `with_ech` is no longer needed and has been removed. + +**9**: + +See [Protocol Sniff](/configuration/route/sniff/). + +**10**: + +See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni). + +**11**: + +Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks, +see [Tun](/configuration/inbound/tun/#auto_redirect). + +**12**: + +You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields. + +See [Listen Fields](/configuration/shared/listen/). + +**13**: + +DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper). + +See [DERP Service](/configuration/service/derp/). + +**14**: + +Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs +(e.g. NetworkManager) and provide DNS resolution. + +See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/). + +**15**: + +SSM API service is a RESTful API server for managing Shadowsocks servers. + +See [SSM API Service](/configuration/service/ssm-api/). + +**16**: + +TUN now implements SideStore's StosVPN. + +See [Tun](/configuration/inbound/tun/#loopback_address). + +**17**: + +We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. + +The following data was tested +using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro. + +| Version | Stack | MTU | Upload | Download | +|-------------|--------|-------|--------|----------| +| 1.11.15 | gvisor | 1500 | 852M | 2.57G | +| 1.12.0-rc.4 | gvisor | 1500 | 2.90G | 4.68G | +| 1.11.15 | gvisor | 4064 | 2.31G | 6.34G | +| 1.12.0-rc.4 | gvisor | 4064 | 7.54G | 12.2G | +| 1.11.15 | gvisor | 65535 | 27.6G | 18.1G | +| 1.12.0-rc.4 | gvisor | 65535 | 39.8G | 34.7G | +| 1.11.15 | system | 1500 | 664M | 706M | +| 1.12.0-rc.4 | system | 1500 | 2.44G | 2.51G | +| 1.11.15 | system | 4064 | 1.88G | 1.94G | +| 1.12.0-rc.4 | system | 4064 | 6.45G | 6.27G | +| 1.11.15 | system | 65535 | 26.2G | 17.4G | +| 1.12.0-rc.4 | system | 65535 | 17.6G | 21.0G | + +**18**: + +We continue to experience issues updating our sing-box apps on the App Store and Play Store. +Until we rewrite and resubmit the apps, they are considered irrecoverable. +Therefore, after this release, we will not be repeating this notice unless there is new information. + +### 1.11.15 + +* 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.32 + +* Improve tun performance on Apple platforms **1** +* Fixes and improvements + +**1**: + +We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. + +### 1.11.14 + +* 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.24 + +* Allow `tls_fragment` and `tls_record_fragment` to be enabled together **1** +* Also add fragment options for TLS client configuration **2** +* Fixes and improvements + +**1**: + +For debugging only, it is recommended to disable if record fragmentation works. + +See [Route Action](/configuration/route/rule_action/#tls_fragment). + +**2**: + +See [TLS](/configuration/shared/tls/). + +#### 1.12.0-beta.23 + +* Add loopback address support for tun **1** +* Add cache support for ssm-api **2** +* Fixes and improvements + +**1**: + +TUN now implements SideStore's StosVPN. + +See [Tun](/configuration/inbound/tun/#loopback_address). + +**2**: + +See [SSM API Service](/configuration/service/ssm-api/#cache_path). + +#### 1.12.0-beta.21 + +* Fix missing `home` option for DERP service **1** +* Fixes and improvements + +**1**: + +You can now choose what the DERP home page shows, just like with derper's `-home` flag. + +See [DERP](/configuration/service/derp/#home). + +### 1.11.13 + +* 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.17 + +* Update quic-go to v0.52.0 +* Fixes and improvements + +#### 1.12.0-beta.15 + +* Add DERP service **1** +* Add Resolved service and DNS server **2** +* Add SSM API service **3** +* Fixes and improvements + +**1**: + +DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper). + +See [DERP Service](/configuration/service/derp/). + +**2**: + +Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs +(e.g. NetworkManager) and provide DNS resolution. + +See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/). + +**3**: + +SSM API service is a RESTful API server for managing Shadowsocks servers. + +See [SSM API Service](/configuration/service/ssm-api/). + +### 1.11.11 + +* 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.13 + +* Add TLS record fragment route options **1** +* Add missing `accept_routes` option for Tailscale **2** +* Fixes and improvements + +**1**: + +See [Route Action](/configuration/route/rule_action/#tls_record_fragment). + +**2**: + +See [Tailscale](/configuration/endpoint/tailscale/#accept_routes). + +#### 1.12.0-beta.10 + +* Add control options for listeners **1** +* Fixes and improvements + +**1**: + +You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields. + +See [Listen Fields](/configuration/shared/listen/). + +### 1.11.10 + +* Undeprecate the `block` outbound **1** +* Fixes and improvements + +**1**: + +Since we don’t have a replacement for using the `block` outbound in selectors yet, +we decided to temporarily undeprecate the `block` outbound until a replacement is available in the future. + +_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.9 + +* Update quic-go to v0.51.0 +* Fixes and improvements + +### 1.11.9 + +* 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.5 + +* Fixes and improvements + +### 1.11.8 + +* Improve `auto_redirect` **1** +* Fixes and improvements + +**1**: + +Now `auto_redirect` fixes compatibility issues between TUN and Docker bridge networks, +see [Tun](/configuration/inbound/tun/#auto_redirect). + +_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.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 +* Fixes and improvements + +#### 1.12.0-alpha.18 + +* Add wildcard SNI support for ShadowTLS inbound **1** +* Fixes and improvements + +**1**: + +See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni). + +#### 1.12.0-alpha.17 + +* Add NTP sniffer **1** +* Fixes and improvements + +**1**: + +See [Protocol Sniff](/configuration/route/sniff/). + +#### 1.12.0-alpha.16 + +* Update `domain_resolver` behavior **1** +* Fixes and improvements + +**1**: + +`route.default_domain_resolver` or `outbound.domain_resolver` is now optional when only one DNS server is configured. + +See [Dial Fields](/configuration/shared/dial/#domain_resolver). + +### 1.11.5 + +* 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.13 + +* Move `predefined` DNS server to DNS rule action **1** +* Fixes and improvements + +**1**: + +See [DNS Rule Action](/configuration/dns/rule_action/#predefined). + +### 1.11.4 + +* Fixes and improvements + +#### 1.12.0-alpha.11 + +* Fixes and improvements + +#### 1.12.0-alpha.10 + +* Add AnyTLS protocol **1** +* Improve `resolve` route action **2** +* Migrate to stdlib ECH implementation **3** +* Fixes and improvements + +**1**: + +The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme. + +See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/). + +**2**: + +`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, +see [Route Action](/configuration/route/rule_action). + +**3**: + +See [TLS](/configuration/shared/tls). + +The build tag `with_ech` is no longer needed and has been removed. + +#### 1.12.0-alpha.7 + +* Add Tailscale DNS server **1** +* Fixes and improvements + +**1**: + +See [Tailscale](/configuration/dns/server/tailscale/). + +#### 1.12.0-alpha.6 + +* Add Tailscale endpoint **1** +* Drop support for go1.22 **2** +* Fixes and improvements + +**1**: + +See [Tailscale](/configuration/endpoint/tailscale/). + +**2**: + +Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. + +For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches +from [MetaCubeX/go](https://github.com/MetaCubeX/go). + +### 1.11.3 + +* Fixes and improvements + +_This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration +process._ + +#### 1.12.0-alpha.5 + +* Fixes and improvements + +### 1.11.1 + +* Fixes and improvements + +#### 1.12.0-alpha.2 + +* Update quic-go to v0.49.0 +* Fixes and improvements + +#### 1.12.0-alpha.1 + +* Refactor DNS servers **1** +* Add domain resolver options**2** +* Add TLS fragment route options **3** +* Add certificate options **4** + +**1**: + +DNS servers are refactored for better performance and scalability. + +See [DNS server](/configuration/dns/server/). + +For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats). + +Compatibility for old formats will be removed in sing-box 1.14.0. + +**2**: + +Legacy `outbound` DNS rules are deprecated +and can be replaced by the new `domain_resolver` option. + +See [Dial Fields](/configuration/shared/dial/#domain_resolver) and +[Route](/configuration/route/#default_domain_resolver). + +For migration, +see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). + +**3**: + +The new TLS fragment route options allow you to fragment TLS handshakes to bypass firewalls. + +This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used +to circumvent real censorship. + +Since it is not designed for performance, it should not be applied to all connections, but only to server names that are +known to be blocked. + +See [Route Action](/configuration/route/rule_action/#tls_fragment). + +**4**: + +New certificate options allow you to manage the default list of trusted X509 CA certificates. + +For the system certificate list, fixed Go not reading Android trusted certificates correctly. + +You can also use the Mozilla Included List instead, or add trusted certificates yourself. + +See [Certificate](/configuration/certificate/). + +### 1.11.0 + +Important changes since 1.10: + +* Introducing rule actions **1** +* Improve tun compatibility **3** +* Merge route options to route actions **4** +* Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **5** +* Add multi network dialing **6** +* Add `cache_capacity` DNS option **7** +* Add `override_address` and `override_port` route options **8** +* Upgrade WireGuard outbound to endpoint **9** +* Add UDP GSO support for WireGuard +* Make GSO adaptive **10** +* Add UDP timeout route option **11** +* Add more masquerade options for hysteria2 **12** +* Add `rule-set merge` command +* Add port hopping support for Hysteria2 **13** +* Hysteria2 `ignore_client_bandwidth` behavior update **14** + +**1**: + +New rule actions replace legacy inbound fields and special outbound fields, +and can be used for pre-matching **2**. + +See [Rule](/configuration/route/rule/), +[Rule Action](/configuration/route/rule_action/), +[DNS Rule](/configuration/dns/rule/) and +[DNS Rule Action](/configuration/dns/rule_action/). + +For migration, see +[Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions), +[Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions) +and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions). + +**2**: + +Similar to Surge's pre-matching. + +Specifically, new rule actions allow you to reject connections with +TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets) +before connection established to improve tun's compatibility. + +See [Rule Action](/configuration/route/rule_action/). + +**3**: + +When `gvisor` tun stack is enabled, even if the request passes routing, +if the outbound connection establishment fails, +the connection still does not need to be established and a TCP RST is replied. + +**4**: + +Route options in DNS route actions will no longer be considered deprecated, +see [DNS Route Action](/configuration/dns/rule_action/). + +Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action, +see [Route Action](/configuration/route/rule_action/). + +**5**: + +When using in graphical clients, new routing rule items allow you to match on +network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled. + +See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/) +and [Headless Rule](/configuration/rule-set/headless-rule/). + +**6**: + +Similar to Surge's strategy. + +New options allow you to connect using multiple network interfaces, +prefer or only use one type of interface, +and configure a timeout to fallback to other interfaces. + +See [Dial Fields](/configuration/shared/dial/#network_strategy), +[Rule Action](/configuration/route/rule_action/#network_strategy) +and [Route](/configuration/route/#default_network_strategy). + +**7**: + +See [DNS](/configuration/dns/#cache_capacity). + +**8**: + +See [Rule Action](/configuration/route/#override_address) and +[Migrate destination override fields to route options](/migration/#migrate-destination-override-fields-to-route-options). + +**9**: + +The new WireGuard endpoint combines inbound and outbound capabilities, +and the old outbound will be removed in sing-box 1.13.0. + +See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/) +and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint). + +**10**: + +For WireGuard outbound and endpoint, GSO will be automatically enabled when available, +see [WireGuard Outbound](/configuration/outbound/wireguard/#gso). + +For TUN, GSO has been removed, +see [Deprecated](/deprecated/#gso-option-in-tun). + +**11**: + +See [Rule Action](/configuration/route/rule_action/#udp_timeout). + +**12**: + +See [Hysteria2](/configuration/inbound/hysteria2/#masquerade). + +**13**: + +See [Hysteria2](/configuration/outbound/hysteria2/). + +**14**: + +When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC. + +### 1.10.7 + +* Fixes and improvements + +#### 1.11.0-beta.20 + +* Hysteria2 `ignore_client_bandwidth` behavior update **1** +* Fixes and improvements + +**1**: + +When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC. + +See [Hysteria2](/configuration/inbound/hysteria2/#ignore_client_bandwidth). + +#### 1.11.0-beta.17 + +* Add port hopping support for Hysteria2 **1** +* Fixes and improvements + +**1**: + +See [Hysteria2](/configuration/outbound/hysteria2/). + +#### 1.11.0-beta.14 + +* Allow adding route (exclude) address sets to routes **1** +* Fixes and improvements + +**1**: + +When `auto_redirect` is not enabled, directly add `route[_exclude]_address_set` +to tun routes (equivalent to `route[_exclude]_address`). + +Note that it **doesn't work on the Android graphical client** due to +the Android VpnService not being able to handle a large number of routes (DeadSystemException), +but otherwise it works fine on all command line clients and Apple platforms. + +See [route_address_set](/configuration/inbound/tun/#route_address_set) and +[route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). + +#### 1.11.0-beta.12 + +* Add `rule-set merge` command +* Fixes and improvements + +#### 1.11.0-beta.3 + +* Add more masquerade options for hysteria2 **1** +* Fixes and improvements + +**1**: + +See [Hysteria2](/configuration/inbound/hysteria2/#masquerade). + +#### 1.11.0-alpha.25 + +* Update quic-go to v0.48.2 +* Fixes and improvements + +#### 1.11.0-alpha.22 + +* Add UDP timeout route option **1** +* Fixes and improvements + +**1**: + +See [Rule Action](/configuration/route/rule_action/#udp_timeout). + +#### 1.11.0-alpha.20 + +* Add UDP GSO support for WireGuard +* Make GSO adaptive **1** + +**1**: + +For WireGuard outbound and endpoint, GSO will be automatically enabled when available, +see [WireGuard Outbound](/configuration/outbound/wireguard/#gso). + +For TUN, GSO has been removed, +see [Deprecated](/deprecated/#gso-option-in-tun). + +#### 1.11.0-alpha.19 + +* Upgrade WireGuard outbound to endpoint **1** +* Fixes and improvements + +**1**: + +The new WireGuard endpoint combines inbound and outbound capabilities, +and the old outbound will be removed in sing-box 1.13.0. + +See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/) +and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint). + +### 1.10.2 + +* Add deprecated warnings +* Fix proxying websocket connections in HTTP/mixed inbounds +* Fixes and improvements + +#### 1.11.0-alpha.18 + +* Fixes and improvements + +#### 1.11.0-alpha.16 + +* Add `cache_capacity` DNS option **1** +* Add `override_address` and `override_port` route options **2** +* Fixes and improvements + +**1**: + +See [DNS](/configuration/dns/#cache_capacity). + +**2**: + +See [Rule Action](/configuration/route/#override_address) and +[Migrate destination override fields to route options](/migration/#migrate-destination-override-fields-to-route-options). + +#### 1.11.0-alpha.15 + +* Improve multi network dialing **1** +* Fixes and improvements + +**1**: + +New options allow you to configure the network strategy flexibly. + +See [Dial Fields](/configuration/shared/dial/#network_strategy), +[Rule Action](/configuration/route/rule_action/#network_strategy) +and [Route](/configuration/route/#default_network_strategy). + +#### 1.11.0-alpha.14 + +* Add multi network dialing **1** +* Fixes and improvements + +**1**: + +Similar to Surge's strategy. + +New options allow you to connect using multiple network interfaces, +prefer or only use one type of interface, +and configure a timeout to fallback to other interfaces. + +See [Dial Fields](/configuration/shared/dial/#network_strategy), +[Rule Action](/configuration/route/rule_action/#network_strategy) +and [Route](/configuration/route/#default_network_strategy). + +#### 1.11.0-alpha.13 + +* Fixes and improvements + +#### 1.11.0-alpha.12 + +* Merge route options to route actions **1** +* Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **2** +* Fixes and improvements + +**1**: + +Route options in DNS route actions will no longer be considered deprecated, +see [DNS Route Action](/configuration/dns/rule_action/). + +Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action, +see [Route Action](/configuration/route/rule_action/). + +**2**: + +When using in graphical clients, new routing rule items allow you to match on +network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled. + +See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/) +and [Headless Rule](/configuration/rule-set/headless-rule/). + +#### 1.11.0-alpha.9 + +* Improve tun compatibility **1** +* Fixes and improvements + +**1**: + +When `gvisor` tun stack is enabled, even if the request passes routing, +if the outbound connection establishment fails, +the connection still does not need to be established and a TCP RST is replied. + +#### 1.11.0-alpha.7 + +* Introducing rule actions **1** + +**1**: + +New rule actions replace legacy inbound fields and special outbound fields, +and can be used for pre-matching **2**. + +See [Rule](/configuration/route/rule/), +[Rule Action](/configuration/route/rule_action/), +[DNS Rule](/configuration/dns/rule/) and +[DNS Rule Action](/configuration/dns/rule_action/). + +For migration, see +[Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions), +[Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions) +and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions). + +**2**: + +Similar to Surge's pre-matching. + +Specifically, new rule actions allow you to reject connections with +TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets) +before connection established to improve tun's compatibility. + +See [Rule Action](/configuration/route/rule_action/). + +#### 1.11.0-alpha.6 + +* Update quic-go to v0.48.1 +* Set gateway for tun correctly +* Fixes and improvements + +#### 1.11.0-alpha.2 + +* Add warnings for usage of deprecated features +* Fixes and improvements + +#### 1.11.0-alpha.1 + +* Update quic-go to v0.48.0 +* Fixes and improvements + +### 1.10.1 + +* Fixes and improvements + +### 1.10.0 + +Important changes since 1.9: + +* Introducing auto-redirect **1** +* Add AdGuard DNS Filter support **2** +* TUN address fields are merged **3** +* Add custom options for `auto-route` and `auto-redirect` **4** +* Drop support for go1.18 and go1.19 **5** +* Add tailing comma support in JSON configuration +* Improve sniffers **6** +* Add new `inline` rule-set type **7** +* Add access control options for Clash API **8** +* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **9** +* Add auto reload support for local rule-set +* Update fsnotify usages **10** +* Add IP address support for `rule-set match` command +* Add `rule-set decompile` command +* Add `process_path_regex` rule item +* Update uTLS to v1.6.7 **11** +* Optimize memory usages of rule-sets **12** + +**1**: + +The new auto-redirect feature allows TUN to automatically +configure connection redirection to improve proxy performance. + +When auto-redirect is enabled, new route address set options will allow you to +automatically configure destination IP CIDR rules from a specified rule set to the firewall. + +Specified or unspecified destinations will bypass the sing-box routes to get better performance +(for example, keep hardware offloading of direct traffics on the router). + +See [TUN](/configuration/inbound/tun). + +**2**: + +The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home. + +See [AdGuard DNS Filter](/configuration/rule-set/adguard/). + +**3**: + +See [Migration](/migration/#tun-address-fields-are-merged). + +**4**: + +See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index), +[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index), +[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and +[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark). + +**5**: + +Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. + +**6**: + +BitTorrent, DTLS, RDP, SSH sniffers are added. + +Now the QUIC sniffer can correctly extract the server name from Chromium requests and +can identify common QUIC clients, including +Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome). + +**7**: + +The new [rule-set](/configuration/rule-set/) type inline (which also becomes the default type) +allows you to write headless rules directly without creating a rule-set file. + +**8**: + +With new access control options, not only can you allow Clash dashboards +to access the Clash API on your local network, +you can also manually limit the websites that can access the API instead of allowing everyone. + +See [Clash API](/configuration/experimental/clash-api/). + +**9**: + +See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty). + +**10**: + +sing-box now uses fsnotify correctly and will not cancel watching +if the target file is deleted or recreated via rename (e.g. `mv`). + +This affects all path options that support reload, including +`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`. + +**11**: + +Some legacy chrome fingerprints have been removed and will fallback to chrome, +see [utls](/configuration/shared/tls#utls). + +**12**: + +See [Source Format](/configuration/rule-set/source-format/#version). + +### 1.9.7 + +* Fixes and improvements + +#### 1.10.0-beta.11 + +* Update uTLS to v1.6.7 **1** + +**1**: + +Some legacy chrome fingerprints have been removed and will fallback to chrome, +see [utls](/configuration/shared/tls#utls). + +#### 1.10.0-beta.10 + +* Add `process_path_regex` rule item +* Fixes and improvements + +_The macOS standalone versions of sing-box (>=1.9.5/<1.10.0-beta.11) now silently fail and require manual granting of +the **Full Disk Access** permission to system extension to start, probably due to Apple's changed security policy. We +will prompt users about this in feature versions._ + +### 1.9.6 + +* Fixes and improvements + +### 1.9.5 + +* Update quic-go to v0.47.0 +* Fix direct dialer not resolving domain +* Fix no error return when empty DNS cache retrieved +* Fix build with go1.23 +* Fix stream sniffer +* Fix bad redirect in clash-api +* Fix wireguard events chan leak +* Fix cached conn eats up read deadlines +* Fix disconnected interface selected as default in windows +* Update Bundle Identifiers for Apple platform clients **1** + +**1**: + +See [Migration](/migration/#bundle-identifier-updates-in-apple-platform-clients). + +We are still working on getting all sing-box apps back on the App Store, which should be completed within a week +(SFI on the App Store and others on TestFlight are already available). + +#### 1.10.0-beta.8 + +* Fixes and improvements + +_With the help of a netizen, we are in the process of getting sing-box apps back on the App Store, which should be +completed within a month (TestFlight is already available)._ + +#### 1.10.0-beta.7 + +* Update quic-go to v0.47.0 +* Fixes and improvements + +#### 1.10.0-beta.6 + +* Add RDP sniffer +* Fixes and improvements + +#### 1.10.0-beta.5 + +* Add PNA support for [Clash API](/configuration/experimental/clash-api/) +* Fixes and improvements + +#### 1.10.0-beta.3 + +* Add SSH sniffer +* Fixes and improvements + +#### 1.10.0-beta.2 + +* Build with go1.23 +* Fixes and improvements + +### 1.9.4 + +* Update quic-go to v0.46.0 +* Update Hysteria2 BBR congestion control +* Filter HTTPS ipv4hint/ipv6hint with domain strategy +* Fix crash on Android when using process rules +* Fix non-IP queries accepted by address filter rules +* Fix UDP server for shadowsocks AEAD multi-user inbounds +* Fix default next protos for v2ray QUIC transport +* Fix default end value of port range configuration options +* Fix reset v2ray transports +* Fix panic caused by rule-set generation of duplicate keys for `domain_suffix` +* Fix UDP connnection leak when sniffing +* Fixes and improvements + +_Due to problems with our Apple developer account, +sing-box apps on Apple platforms are temporarily unavailable for download or update. +If your company or organization is willing to help us return to the App Store, +please [contact us](mailto:contact@sagernet.org)._ + +#### 1.10.0-alpha.29 + +* Update quic-go to v0.46.0 +* Fixes and improvements + +#### 1.10.0-alpha.25 + +* Add AdGuard DNS Filter support **1** + +**1**: + +The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home. + +See [AdGuard DNS Filter](/configuration/rule-set/adguard/). + +#### 1.10.0-alpha.23 + +* Add Chromium support for QUIC sniffer +* Add client type detect support for QUIC sniffer **1** +* Fixes and improvements + +**1**: + +Now the QUIC sniffer can correctly extract the server name from Chromium requests and +can identify common QUIC clients, including +Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome). + +See [Protocol Sniff](/configuration/route/sniff/) and [Route Rule](/configuration/route/rule/#client). + +#### 1.10.0-alpha.22 + +* Optimize memory usages of rule-sets **1** +* Fixes and improvements + +**1**: + +See [Source Format](/configuration/rule-set/source-format/#version). + +#### 1.10.0-alpha.20 + +* Add DTLS sniffer +* Fixes and improvements + +#### 1.10.0-alpha.19 + +* Add `rule-set decompile` command +* Add IP address support for `rule-set match` command +* Fixes and improvements + +#### 1.10.0-alpha.18 + +* Add new `inline` rule-set type **1** +* Add auto reload support for local rule-set +* Update fsnotify usages **2** +* Fixes and improvements + +**1**: + +The new [rule-set](/configuration/rule-set/) type inline (which also becomes the default type) +allows you to write headless rules directly without creating a rule-set file. + +**2**: + +sing-box now uses fsnotify correctly and will not cancel watching +if the target file is deleted or recreated via rename (e.g. `mv`). + +This affects all path options that support reload, including +`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`. + +#### 1.10.0-alpha.17 + +* Some chaotic changes **1** +* `rule_set_ipcidr_match_source` rule items are renamed **2** +* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3** +* Update quic-go to v0.45.1 +* Fixes and improvements + +**1**: + +Something may be broken, please actively report problems with this version. + +**2**: + +`rule_set_ipcidr_match_source` route and DNS rule items are renamed to +`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +**3**: + +See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty). + +#### 1.10.0-alpha.16 + +* Add custom options for `auto-route` and `auto-redirect` **1** +* Fixes and improvements + +**1**: + +See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index), +[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index), +[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and +[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark). + +#### 1.10.0-alpha.13 + +* TUN address fields are merged **1** +* Add route address set support for auto-redirect **2** + +**1**: + +See [Migration](/migration/#tun-address-fields-are-merged). + +**2**: + +The new feature will allow you to configure the destination IP CIDR rules +in the specified rule-sets to the firewall automatically. + +Specified or unspecified destinations will bypass the sing-box routes to get better performance +(for example, keep hardware offloading of direct traffics on the router). + +See [route_address_set](/configuration/inbound/tun/#route_address_set) +and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). + +#### 1.10.0-alpha.12 + +* Fix auto-redirect not configuring nftables forward chain correctly +* Fixes and improvements + +### 1.9.3 + +* Fixes and improvements + +#### 1.10.0-alpha.10 + +* Fixes and improvements + +### 1.9.2 + +* Fixes and improvements + +#### 1.10.0-alpha.8 + +* Drop support for go1.18 and go1.19 **1** +* Update quic-go to v0.45.0 +* Update Hysteria2 BBR congestion control +* Fixes and improvements + +**1**: + +Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. + +### 1.9.1 + +* Fixes and improvements + +#### 1.10.0-alpha.7 + +* Fixes and improvements + +#### 1.10.0-alpha.5 + +* Improve auto-redirect **1** + +**1**: + +nftables support and DNS hijacking has been added. + +Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**. + +#### 1.10.0-alpha.4 + +* Fix auto-redirect **1** +* Improve auto-route on linux **2** + +**1**: + +Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers. + +**2**: + +Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers, +but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated. + +#### 1.10.0-alpha.2 + +* Move auto-redirect to Tun **1** +* Fixes and improvements + +**1**: + +Linux support are added. + +See [Tun](/configuration/inbound/tun/#auto_redirect). + +#### 1.10.0-alpha.1 + +* Add tailing comma support in JSON configuration +* Add simple auto-redirect for Android **1** +* Add BitTorrent sniffer **2** + +**1**: + +It allows you to use redirect inbound in the sing-box Android client +and automatically configures IPv4 TCP redirection via su. + +This may alleviate the symptoms of some OCD patients who think that +redirect can effectively save power compared to the system HTTP Proxy. + +See [Redirect](/configuration/inbound/redirect/). + +**2**: + +See [Protocol Sniff](/configuration/route/sniff/). + +### 1.9.0 + +* Fixes and improvements + +Important changes since 1.8: + +* `domain_suffix` behavior update **1** +* `process_path` format update on Windows **2** +* Add address filter DNS rule items **3** +* Add support for `client-subnet` DNS options **4** +* Add rejected DNS response cache support **5** +* Add `bypass_domain` and `search_domain` platform HTTP proxy options **6** +* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **7** +* Handle Windows power events +* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled +* Improve DNS truncate behavior +* Update Hysteria protocol +* Update quic-go to v0.43.1 +* Update gVisor to 20240422.0 +* Mitigating TunnelVision attacks **8** + +**1**: + +See [Migration](/migration/#domain_suffix-behavior-update). + +**2**: + +See [Migration](/migration/#process_path-format-update-on-windows). + +**3**: + +The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS +if using this method. + +See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields). + +[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. + +**4**: + +See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule). + +Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests, +the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated. + +**5**: + +The new feature allows you to cache the check results of +[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration. + +**6**: + +See [TUN](/configuration/inbound/tun) inbound. + +**7**: + +See [DNS Rule](/configuration/dns/rule/). + +**8**: + +See [TunnelVision](/manual/misc/tunnelvision). + +#### 1.9.0-rc.22 + +* Fixes and improvements + +#### 1.9.0-rc.20 + +* Prioritize `*_route_address` in linux auto-route +* Fix `*_route_address` in darwin auto-route + +#### 1.8.14 + +* Fix hysteria2 panic +* Fixes and improvements + +#### 1.9.0-rc.18 + +* Add custom prefix support in EDNS0 client subnet options +* Fix hysteria2 crash +* Fix `store_rdrc` corrupted +* Update quic-go to v0.43.1 +* Fixes and improvements + +#### 1.9.0-rc.16 + +* Mitigating TunnelVision attacks **1** +* Fixes and improvements + +**1**: + +See [TunnelVision](/manual/misc/tunnelvision). + +#### 1.9.0-rc.15 + +* Fixes and improvements + +#### 1.8.13 + +* Fix fake-ip mapping +* Fixes and improvements + +#### 1.9.0-rc.14 + +* Fixes and improvements + +#### 1.9.0-rc.13 + +* Update Hysteria protocol +* Update quic-go to v0.43.0 +* Update gVisor to 20240422.0 +* Fixes and improvements + +#### 1.8.12 + +* Now we have official APT and DNF repositories **1** +* Fix packet MTU for QUIC protocols +* Fixes and improvements + +**1**: + +Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/ + +#### 1.9.0-rc.11 + +* Fixes and improvements + +#### 1.8.11 + +* Fixes and improvements + +#### 1.8.10 + +* Fixes and improvements + +#### 1.9.0-beta.17 + +* Update `quic-go` to v0.42.0 +* Fixes and improvements + +#### 1.9.0-beta.16 + +* Fixes and improvements + +_Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions) +and you cannot join the test, install or update the sing-box beta app right now. +Please wait patiently for processing._ + +#### 1.9.0-beta.14 + +* Update gVisor to 20240212.0-65-g71212d503 +* Fixes and improvements + +#### 1.8.9 + +* Fixes and improvements + +#### 1.8.8 + +* Fixes and improvements + +#### 1.9.0-beta.7 + +* Fixes and improvements + +#### 1.9.0-beta.6 + +* Fix address filter DNS rule items **1** +* Fix DNS outbound responding with wrong data +* Fixes and improvements + +**1**: + +Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances. +If you have enabled `store_rdrc` to save results, consider clearing the cache file. + +#### 1.8.7 + +* Fixes and improvements + +#### 1.9.0-alpha.15 + +* Fixes and improvements + +#### 1.9.0-alpha.14 + +* Improve DNS truncate behavior +* Fixes and improvements + +#### 1.9.0-alpha.13 + +* Fixes and improvements + +#### 1.8.6 + +* Fixes and improvements + +#### 1.9.0-alpha.12 + +* Handle Windows power events +* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled +* Fixes and improvements + +#### 1.9.0-alpha.11 + +* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1** +* Fixes and improvements + +**1**: + +See [DNS Rule](/configuration/dns/rule/). + +#### 1.9.0-alpha.10 + +* Add `bypass_domain` and `search_domain` platform HTTP proxy options **1** +* Fixes and improvements + +**1**: + +See [TUN](/configuration/inbound/tun) inbound. + +#### 1.9.0-alpha.8 + +* Add rejected DNS response cache support **1** +* Fixes and improvements + +**1**: + +The new feature allows you to cache the check results of +[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration. + +#### 1.9.0-alpha.7 + +* Update gVisor to 20240206.0 +* Fixes and improvements + +#### 1.9.0-alpha.6 + +* Fixes and improvements + +#### 1.9.0-alpha.3 + +* Update `quic-go` to v0.41.0 +* Fixes and improvements + +#### 1.9.0-alpha.2 + +* Add support for `client-subnet` DNS options **1** +* Fixes and improvements + +**1**: + +See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule). + +Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests, +the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated. + +#### 1.9.0-alpha.1 + +* `domain_suffix` behavior update **1** +* `process_path` format update on Windows **2** +* Add address filter DNS rule items **3** + +**1**: + +See [Migration](/migration/#domain_suffix-behavior-update). + +**2**: + +See [Migration](/migration/#process_path-format-update-on-windows). + +**3**: + +The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS +if using this method. + +See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields). + +[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. + +#### 1.8.5 + +* Fixes and improvements + +#### 1.8.4 + +* Fixes and improvements + +#### 1.8.2 + +* Fixes and improvements + +#### 1.8.1 + +* Fixes and improvements + +### 1.8.0 + +* Fixes and improvements + +Important changes since 1.7: + +* Migrate cache file from Clash API to independent options **1** +* Introducing [rule-set](/configuration/rule-set/) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** +* Independent `source_ip_is_private` and `ip_is_private` rules **5** +* Add context to JSON decode error message **6** +* Reject internal fake-ip queries **7** +* Add GSO support for TUN and WireGuard system interface **8** +* Add `idle_timeout` for URLTest outbound **9** +* Add simple loopback detect +* Optimize memory usage of idle connections +* Update uTLS to 1.5.4 **10** +* Update dependencies **11** + +**1**: + +See [Cache File](/configuration/experimental/cache-file/) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +rule-set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule/), +[DNS Rule](/configuration/dns/rule/), +[rule-set](/configuration/rule-set/), +[Source Format](/configuration/rule-set/source-format/) and +[Headless Rule](/configuration/rule-set/headless-rule/). + +For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + +**5**: + +The `private` GeoIP country never existed and was actually implemented inside V2Ray. +Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). + +**6**: + +JSON parse errors will now include the current key path. +Only takes effect when compiled with Go 1.21+. + +**7**: + +All internal DNS queries now skip DNS rules with `server` type `fakeip`, +and the default DNS server can no longer be `fakeip`. + +This change is intended to break incorrect usage and essentially requires no action. + +**8**: + +See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound. + +**9**: + +When URLTest is idle for a certain period of time, the scheduled delay test will be paused. + +**10**: + +Added some new [fingerprints](/configuration/shared/tls#utls). +Also, starting with this release, uTLS requires at least Go 1.20. + +**11**: + +Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor` +to `20231204.0` + +#### 1.8.0-rc.11 + +* Fixes and improvements + +#### 1.7.8 + +* Fixes and improvements + +#### 1.8.0-rc.10 + +* Fixes and improvements + +#### 1.7.7 + +* Fix V2Ray transport `path` validation behavior **1** +* Fixes and improvements + +**1**: + +See [V2Ray transport](/configuration/shared/v2ray-transport/). + +#### 1.8.0-rc.7 + +* Fixes and improvements + +#### 1.8.0-rc.3 + +* Fix V2Ray transport `path` validation behavior **1** +* Fixes and improvements + +**1**: + +See [V2Ray transport](/configuration/shared/v2ray-transport/). + +#### 1.7.6 + +* Fixes and improvements + +#### 1.8.0-rc.1 + +* Fixes and improvements + +#### 1.8.0-beta.9 + +* Add simple loopback detect +* Fixes and improvements + +#### 1.7.5 + +* Fixes and improvements + +#### 1.8.0-alpha.17 + +* Add GSO support for TUN and WireGuard system interface **1** +* Update uTLS to 1.5.4 **2** +* Update dependencies **3** +* Fixes and improvements + +**1**: + +See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound. + +**2**: + +Added some new [fingerprints](/configuration/shared/tls#utls). +Also, starting with this release, uTLS requires at least Go 1.20. + +**3**: + +Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, and `gvisor` to `20231204.0` + +This may break something, good luck! + +#### 1.7.4 + +* Fixes and improvements + +_Due to the long waiting time, this version is no longer waiting for approval +by the Apple App Store, so updates to Apple Platforms will be delayed._ + +#### 1.8.0-alpha.16 + +* Fixes and improvements + +#### 1.8.0-alpha.15 + +* Some chaotic changes **1** +* Fixes and improvements + +**1**: + +Designed to optimize memory usage of idle connections, may take effect on the following protocols: + +| Protocol | TCP | UDP | +|------------------------------------------------------|------------------|------------------| +| HTTP proxy server | :material-check: | / | +| SOCKS5 | :material-close: | :material-check: | +| Shadowsocks none/AEAD/AEAD2022 | :material-check: | :material-check: | +| Trojan | / | :material-check: | +| TUIC/Hysteria/Hysteria2 | :material-close: | :material-check: | +| Multiplex | :material-close: | :material-check: | +| Plain TLS (Trojan/VLESS without extra sub-protocols) | :material-check: | / | +| Other protocols | :material-close: | :material-close: | + +At the same time, everything existing may be broken, please actively report problems with this version. + +#### 1.8.0-alpha.13 + +* Fixes and improvements + +#### 1.8.0-alpha.10 + +* Add `idle_timeout` for URLTest outbound **1** +* Fixes and improvements + +**1**: + +When URLTest is idle for a certain period of time, the scheduled delay test will be paused. + +#### 1.7.2 + +* Fixes and improvements + +#### 1.8.0-alpha.8 + +* Add context to JSON decode error message **1** +* Reject internal fake-ip queries **2** +* Fixes and improvements + +**1**: + +JSON parse errors will now include the current key path. +Only takes effect when compiled with Go 1.21+. + +**2**: + +All internal DNS queries now skip DNS rules with `server` type `fakeip`, +and the default DNS server can no longer be `fakeip`. + +This change is intended to break incorrect usage and essentially requires no action. + +#### 1.8.0-alpha.7 + +* Fixes and improvements + +#### 1.7.1 + +* Fixes and improvements + +#### 1.8.0-alpha.6 + +* Fix rule-set matching logic **1** +* Fixes and improvements + +**1**: + +Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets, +rather than completely following the AND logic. + +#### 1.8.0-alpha.5 + +* Parallel rule-set initialization +* Independent `source_ip_is_private` and `ip_is_private` rules **1** + +**1**: + +The `private` GeoIP country never existed and was actually implemented inside V2Ray. +Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). + +#### 1.8.0-alpha.1 + +* Migrate cache file from Clash API to independent options **1** +* Introducing [rule-set](/configuration/rule-set/) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** + +**1**: + +See [Cache File](/configuration/experimental/cache-file/) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +rule-set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule/), +[DNS Rule](/configuration/dns/rule/), +[rule-set](/configuration/rule-set/), +[Source Format](/configuration/rule-set/source-format/) and +[Headless Rule](/configuration/rule-set/headless-rule/). + +For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + +### 1.7.0 + +* Fixes and improvements + +Important changes since 1.6: + +* Add [exclude route support](/configuration/inbound/tun/) for TUN inbound +* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen/) **1** +* Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **2** +* Migrate multiplex and UoT server to inbound **3** +* Add TCP Brutal support for multiplex **4** +* Add `wifi_ssid` and `wifi_bssid` route and DNS rules **5** +* Update quic-go to v0.40.0 +* Update gVisor to 20231113.0 + +**1**: + +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. + +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. + +**2**: + +Introduced in V2Ray 5.10.0. + +The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. + +**3**: + +Starting in 1.7.0, multiplexing support is no longer enabled by default +and needs to be turned on explicitly in inbound +options. + +**4** + +Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, +see [TCP Brutal](/configuration/shared/tcp-brutal/) for details. + +**5**: + +Only supported in graphical clients on Android and Apple platforms. + +#### 1.7.0-rc.3 + +* Fixes and improvements + +#### 1.6.7 + +* macOS: Add button for uninstall SystemExtension in the standalone graphical client +* Fix missing UDP user context on TUIC/Hysteria2 inbounds +* Fixes and improvements + +#### 1.7.0-rc.2 + +* Fix missing UDP user context on TUIC/Hysteria2 inbounds +* macOS: Add button for uninstall SystemExtension in the standalone graphical client + +#### 1.6.6 + +* Fixes and improvements + +#### 1.7.0-rc.1 + +* Fixes and improvements + +#### 1.7.0-beta.5 + +* Update gVisor to 20231113.0 +* Fixes and improvements + +#### 1.7.0-beta.4 + +* Add `wifi_ssid` and `wifi_bssid` route and DNS rules **1** +* Fixes and improvements + +**1**: + +Only supported in graphical clients on Android and Apple platforms. + +#### 1.7.0-beta.3 + +* Fix zero TTL was incorrectly reset +* Fixes and improvements + +#### 1.6.5 + +* Fix crash if TUIC inbound authentication failed +* Fixes and improvements + +#### 1.7.0-beta.2 + +* Fix crash if TUIC inbound authentication failed +* Update quic-go to v0.40.0 +* Fixes and improvements + +#### 1.6.4 + +* Fixes and improvements + +#### 1.7.0-beta.1 + +* Fixes and improvements + +#### 1.6.3 + +* iOS/Android: Fix profile auto update +* Fixes and improvements + +#### 1.7.0-alpha.11 + +* iOS/Android: Fix profile auto update +* Fixes and improvements + +#### 1.7.0-alpha.10 + +* Fix tcp-brutal not working with TLS +* Fix Android client not closing in some cases +* Fixes and improvements + +#### 1.6.2 + +* Fixes and improvements + +#### 1.6.1 + +* Our [Android client](/installation/clients/sfa/) is now available in the Google Play Store ▶️ +* Fixes and improvements + +#### 1.7.0-alpha.6 + +* Fixes and improvements + +#### 1.7.0-alpha.4 + +* Migrate multiplex and UoT server to inbound **1** +* Add TCP Brutal support for multiplex **2** + +**1**: + +Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound +options. + +**2** + +Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, +see [TCP Brutal](/configuration/shared/tcp-brutal/) for details. + +#### 1.7.0-alpha.3 + +* Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **1** +* Fixes and improvements + +**1**: + +Introduced in V2Ray 5.10.0. + +The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. + +### 1.6.0 + +* Fixes and improvements + +Important changes since 1.5: + +* Our [Apple tvOS client](/installation/clients/sft/) is now available in the App Store 🍎 +* Update BBR congestion control for TUIC and Hysteria2 **1** +* Update brutal congestion control for Hysteria2 +* Add `brutal_debug` option for Hysteria2 +* Update legacy Hysteria protocol **2** +* Add TLS self sign key pair generate command +* Remove [Deprecated Features](/deprecated/) by agreement + +**1**: + +None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. +This update is intended to address the multi-send defects of the old implementation and may introduce new issues. + +**2** + +Based on discussions with the original author, the brutal CC and QUIC protocol parameters of +the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 + +#### 1.7.0-alpha.2 + +* Fix bugs introduced in 1.7.0-alpha.1 + +#### 1.7.0-alpha.1 + +* Add [exclude route support](/configuration/inbound/tun/) for TUN inbound +* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen/) **1** +* Fixes and improvements + +**1**: + +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. + +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. + +#### 1.5.5 + +* Fix IPv6 `auto_route` for Linux **1** +* Add legacy builds for old Windows and macOS systems **2** +* Fixes and improvements + +**1**: + +When `auto_route` is enabled and `strict_route` is disabled, the device can now be reached from external IPv6 addresses. + +**2**: + +Built using Go 1.20, the last version that will run on +Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. + +#### 1.6.0-rc.4 + +* Fixes and improvements + +#### 1.6.0-rc.1 + +* Add legacy builds for old Windows and macOS systems **1** +* Fixes and improvements + +**1**: + +Built using Go 1.20, the last version that will run on +Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. + +#### 1.6.0-beta.4 + +* Fix IPv6 `auto_route` for Linux **1** +* Fixes and improvements + +**1**: + +When `auto_route` is enabled and `strict_route` is disabled, the device can now be reached from external IPv6 addresses. + +#### 1.5.4 + +* Fix Clash cache crash on arm32 devices +* Fixes and improvements + +#### 1.6.0-beta.3 + +* Update the legacy Hysteria protocol **1** +* Fixes and improvements + +**1** + +Based on discussions with the original author, the brutal CC and QUIC protocol parameters of +the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 + +#### 1.6.0-beta.2 + +* Add TLS self sign key pair generate command +* Update brutal congestion control for Hysteria2 +* Fix Clash cache crash on arm32 devices +* Update golang.org/x/net to v0.17.0 +* Fixes and improvements + +#### 1.6.0-beta.3 + +* Update the legacy Hysteria protocol **1** +* Fixes and improvements + +**1** + +Based on discussions with the original author, the brutal CC and QUIC protocol parameters of +the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 + +#### 1.6.0-beta.2 + +* Add TLS self sign key pair generate command +* Update brutal congestion control for Hysteria2 +* Fix Clash cache crash on arm32 devices +* Update golang.org/x/net to v0.17.0 +* Fixes and improvements + +#### 1.5.3 + +* Fix compatibility with Android 14 +* Fixes and improvements + +#### 1.6.0-beta.1 + +* Fixes and improvements + +#### 1.6.0-alpha.5 + +* Fix compatibility with Android 14 +* Update BBR congestion control for TUIC and Hysteria2 **1** +* Fixes and improvements + +**1**: + +None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. +This update is intended to fix a memory leak flaw in the new implementation introduced in 1.6.0-alpha.1 and may +introduce new issues. + +#### 1.6.0-alpha.4 + +* Add `brutal_debug` option for Hysteria2 +* Fixes and improvements + +#### 1.5.2 + +* Our [Apple tvOS client](/installation/clients/sft/) is now available in the App Store 🍎 +* Fixes and improvements + +#### 1.6.0-alpha.3 + +* Fixes and improvements + +#### 1.6.0-alpha.2 + +* Fixes and improvements + +#### 1.5.1 + +* Fixes and improvements + +#### 1.6.0-alpha.1 + +* Update BBR congestion control for TUIC and Hysteria2 **1** +* Update quic-go to v0.39.0 +* Update gVisor to 20230814.0 +* Remove [Deprecated Features](/deprecated/) by agreement +* Fixes and improvements + +**1**: + +None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. +This update is intended to address the multi-send defects of the old implementation and may introduce new issues. + +### 1.5.0 + +* Fixes and improvements + +Important changes since 1.4: + +* Add TLS [ECH server](/configuration/shared/tls/) support +* Improve TLS TCH client configuration +* Add TLS ECH key pair generator **1** +* Add TLS ECH support for QUIC based protocols **2** +* Add KDE support for the `set_system_proxy` option in HTTP inbound +* Add Hysteria2 protocol support **3** +* Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **4** +* Add DNS01 challenge support for ACME TLS certificate issuer **5** +* Add `merge` command **6** +* Mark [Deprecated Features](/deprecated/) + +**1**: + +Command: `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` + +**2**: + +All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria[/2]`, `TUIC` and `V2ray QUIC transport`. + +**3**: + +See [Hysteria2 inbound](/configuration/inbound/hysteria2/) and [Hysteria2 outbound](/configuration/outbound/hysteria2/) + +For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network) + +**4**: + +Interrupt existing connections when the selected outbound has changed. + +Only inbound connections are affected by this setting, internal connections will always be interrupted. + +**5**: + +Only `Alibaba Cloud DNS` and `Cloudflare` are supported, see [ACME Fields](/configuration/shared/tls#acme-fields) +and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/). + +**6**: + +This command also parses path resources that appear in the configuration file and replaces them with embedded +configuration, such as TLS certificates or SSH private keys. + +#### 1.5.0-rc.6 + +* Fixes and improvements + +#### 1.4.6 + +* Fixes and improvements + +#### 1.5.0-rc.5 + +* Fixed an improper authentication vulnerability in the SOCKS5 inbound +* Fixes and improvements + +**Security Advisory** + +This update fixes an improper authentication vulnerability in the sing-box SOCKS inbound. This vulnerability allows an +attacker to craft special requests to bypass user authentication. All users exposing SOCKS servers with user +authentication in an insecure environment are advised to update immediately. + +此更新修复了 sing-box SOCKS 入站中的一个不正确身份验证漏洞。 该漏洞允许攻击者制作特殊请求来绕过用户身份验证。建议所有将使用用户认证的 +SOCKS 服务器暴露在不安全环境下的用户立更新。 + +#### 1.4.5 + +* Fixed an improper authentication vulnerability in the SOCKS5 inbound +* Fixes and improvements + +**Security Advisory** + +This update fixes an improper authentication vulnerability in the sing-box SOCKS inbound. This vulnerability allows an +attacker to craft special requests to bypass user authentication. All users exposing SOCKS servers with user +authentication in an insecure environment are advised to update immediately. + +此更新修复了 sing-box SOCKS 入站中的一个不正确身份验证漏洞。 该漏洞允许攻击者制作特殊请求来绕过用户身份验证。建议所有将使用用户认证的 +SOCKS 服务器暴露在不安全环境下的用户立更新。 + +#### 1.5.0-rc.3 + +* Fixes and improvements + +#### 1.5.0-beta.12 + +* Add `merge` command **1** +* Fixes and improvements + +**1**: + +This command also parses path resources that appear in the configuration file and replaces them with embedded +configuration, such as TLS certificates or SSH private keys. + +``` +Merge configurations + +Usage: + sing-box merge [output] [flags] + +Flags: + -h, --help help for merge + +Global Flags: + -c, --config stringArray set configuration file path + -C, --config-directory stringArray set configuration directory path + -D, --directory string set working directory + --disable-color disable color output +``` + +#### 1.5.0-beta.11 + +* Add DNS01 challenge support for ACME TLS certificate issuer **1** +* Fixes and improvements + +**1**: + +Only `Alibaba Cloud DNS` and `Cloudflare` are supported, +see [ACME Fields](/configuration/shared/tls#acme-fields) +and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/). + +#### 1.5.0-beta.10 + +* Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **1** +* Fixes and improvements + +**1**: + +Interrupt existing connections when the selected outbound has changed. + +Only inbound connections are affected by this setting, internal connections will always be interrupted. + +#### 1.4.3 + +* Fixes and improvements + +#### 1.5.0-beta.8 + +* Fixes and improvements + +#### 1.4.2 + +* Fixes and improvements + +#### 1.5.0-beta.6 + +* Fix compatibility issues with official Hysteria2 server and client +* Fixes and improvements +* Mark [deprecated features](/deprecated/) + +#### 1.5.0-beta.3 + +* Fixes and improvements +* Updated Hysteria2 documentation **1** + +**1**: + +Added notes indicating compatibility issues with the official +Hysteria2 server and client when using `fastOpen=false` or UDP MTU >= 1200. + +#### 1.5.0-beta.2 + +* Add hysteria2 protocol support **1** +* Fixes and improvements + +**1**: + +See [Hysteria2 inbound](/configuration/inbound/hysteria2/) and [Hysteria2 outbound](/configuration/outbound/hysteria2/) + +For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network) + +#### 1.5.0-beta.1 + +* Add TLS [ECH server](/configuration/shared/tls/) support +* Improve TLS TCH client configuration +* Add TLS ECH key pair generator **1** +* Add TLS ECH support for QUIC based protocols **2** +* Add KDE support for the `set_system_proxy` option in HTTP inbound + +**1**: + +Command: `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` + +**2**: + +All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `TUIC` and `V2ray QUIC transport`. + +#### 1.4.1 + +* Fixes and improvements + +### 1.4.0 + +* Fix bugs and update dependencies + +Important changes since 1.3: + +* Add TUIC support **1** +* Add `udp_over_stream` option for TUIC client **2** +* Add MultiPath TCP support **3** +* Add `include_interface` and `exclude_interface` options for tun inbound +* Pause recurring tasks when no network or device idle +* Improve Android and Apple platform clients + +*1*: + +See [TUIC inbound](/configuration/inbound/tuic/) +and [TUIC outbound](/configuration/outbound/tuic/) + +**2**: + +This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC +stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or +another program compatible with the protocol as a server. + +This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP +traffic (basically QUIC streams). + +*3*: + +Requires sing-box to be compiled with Go 1.21. + +#### 1.4.0-rc.3 + +* Fixes and improvements + +#### 1.4.0-rc.2 + +* Fixes and improvements + +#### 1.4.0-rc.1 + +* Fix TUIC UDP + +#### 1.4.0-beta.6 + +* Add `udp_over_stream` option for TUIC client **1** +* Add `include_interface` and `exclude_interface` options for tun inbound +* Fixes and improvements + +**1**: + +This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC +stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or +another program compatible with the protocol as a server. + +This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP +traffic (basically QUIC streams). + +#### 1.4.0-beta.5 + +* Fixes and improvements + +#### 1.4.0-beta.4 + +* Graphical clients: Persistence group expansion state +* Fixes and improvements + +#### 1.4.0-beta.3 + +* Fixes and improvements + +#### 1.4.0-beta.2 + +* Add MultiPath TCP support **1** +* Drop QUIC support for Go 1.18 and 1.19 due to upstream changes +* Fixes and improvements + +*1*: + +Requires sing-box to be compiled with Go 1.21. + +#### 1.4.0-beta.1 + +* Add TUIC support **1** +* Pause recurring tasks when no network or device idle +* Fixes and improvements + +*1*: + +See [TUIC inbound](/configuration/inbound/tuic/) +and [TUIC outbound](/configuration/outbound/tuic/) + +#### 1.3.6 + +* Fixes and improvements + +#### 1.3.5 + +* Fixes and improvements +* Introducing our [Apple tvOS](/installation/clients/sft/) client applications **1** +* Add per app proxy and app installed/updated trigger support for Android client +* Add profile sharing support for Android/iOS/macOS clients + +**1**: + +Due to the requirement of tvOS 17, the app cannot be submitted to the App Store for the time being, and can only be +downloaded through TestFlight. + +#### 1.3.4 + +* Fixes and improvements +* We're now on the [App Store](https://apps.apple.com/us/app/sing-box/id6451272673), always free! It should be noted + that due to stricter and slower review, the release of Store versions will be delayed. +* We've made a standalone version of the macOS client (the original Application Extension relies on App Store + distribution), which you can download as SFM-version-universal.zip in the release artifacts. + +#### 1.3.3 + +* Fixes and improvements + +#### 1.3.1-rc.1 + +* Fix bugs and update dependencies + +#### 1.3.1-beta.3 + +* Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1 + ** +* Fixes and improvements + +**1**: + +The old testflight link and app are no longer valid. + +#### 1.3.1-beta.2 + +* Fix bugs and update dependencies + +#### 1.3.1-beta.1 + +* Fixes and improvements + +### 1.3.0 + +* Fix bugs and update dependencies + +Important changes since 1.2: + +* Add [FakeIP](/configuration/dns/fakeip/) support **1** +* Improve multiplex **2** +* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support +* Add `rewrite_ttl` DNS rule action +* Add `store_fakeip` Clash API option +* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound +* Add loopback detect +* Add Clash.Meta API compatibility for Clash API +* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty +* Add path and headers option for HTTP outbound +* Perform URLTest recheck after network changes +* Fix `system` tun stack for ios +* Fix network monitor for android/ios +* Update VLESS and XUDP protocol +* Make splice work with traffic statistics systems like Clash API +* Significantly reduces memory usage of idle connections +* Improve DNS caching +* Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS +* Reimplemented shadowsocks client +* Add multiplex support for VLESS outbound +* Automatically add Windows firewall rules in order for the system tun stack to work +* Fix TLS 1.2 support for shadow-tls client +* Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file +* Fix `local` DNS transport for Android + +*1*: + +See [FAQ](/faq/fakeip/) for more information. + +*2*: + +Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex/). + +#### 1.3-rc2 + +* Fix `local` DNS transport for Android +* Fix bugs and update dependencies + +#### 1.3-rc1 + +* Fix bugs and update dependencies + +#### 1.3-beta14 + +* Fixes and improvements + +#### 1.3-beta13 + +* Fix resolving fakeip domains **1** +* Deprecate L3 routing +* Fix bugs and update dependencies + +**1**: + +If the destination address of the connection is obtained from fakeip, dns rules with server type fakeip will be skipped. + +#### 1.3-beta12 + +* Automatically add Windows firewall rules in order for the system tun stack to work +* Fix TLS 1.2 support for shadow-tls client +* Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file +* Fixes and improvements + +#### 1.3-beta11 + +* Fix bugs and update dependencies + +#### 1.3-beta10 + +* Improve direct copy **1** +* Improve DNS caching +* Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS +* Reimplemented shadowsocks client **2** +* Add multiplex support for VLESS outbound +* Set TCP keepalive for WireGuard gVisor TCP connections +* Fixes and improvements + +**1**: + +* Make splice work with traffic statistics systems like Clash API +* Significantly reduces memory usage of idle connections + +**2**: + +Improved performance and reduced memory usage. + +#### 1.3-beta9 + +* Improve multiplex **1** +* Fixes and improvements + +*1*: + +Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex/). + +#### 1.2.6 + +* Fix bugs and update dependencies + +#### 1.3-beta8 + +* Fix `system` tun stack for ios +* Fix network monitor for android/ios +* Update VLESS and XUDP protocol **1** +* Fixes and improvements + +*1: + +This is an incompatible update for XUDP in VLESS if vision flow is enabled. + +#### 1.3-beta7 + +* Add `path` and `headers` options for HTTP outbound +* Add multi-user support for Shadowsocks legacy AEAD inbound +* Fixes and improvements + +#### 1.2.4 + +* Fixes and improvements + +#### 1.3-beta6 + +* Fix WireGuard reconnect +* Perform URLTest recheck after network changes +* Fix bugs and update dependencies + +#### 1.3-beta5 + +* Add Clash.Meta API compatibility for Clash API +* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty +* Add path and headers option for HTTP outbound +* Fixes and improvements + +#### 1.3-beta4 + +* Fix bugs + +#### 1.3-beta2 + +* Download clash-dashboard if the specified Clash `external_ui` directory is empty +* Fix bugs and update dependencies + +#### 1.3-beta1 + +* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support +* Add [L3 routing](/configuration/route/ip-rule/) support **1** +* Add `rewrite_ttl` DNS rule action +* Add [FakeIP](/configuration/dns/fakeip/) support **2** +* Add `store_fakeip` Clash API option +* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound +* Add loopback detect + +*1*: + +It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct/) or block connections +at the IP layer. + +*2*: + +See [FAQ](/faq/fakeip/) for more information. + +#### 1.2.3 + +* Introducing our [new Android client application](/installation/clients/sfa/) +* Improve UDP domain destination NAT +* Update reality protocol +* Fix TTL calculation for DNS response +* Fix v2ray HTTP transport compatibility +* Fix bugs and update dependencies + +#### 1.2.2 + +* Accept `any` outbound in dns rule **1** +* Fix bugs and update dependencies + +*1*: + +Now you can use the `any` outbound rule to match server address queries instead of filling in all server domains +to `domain` rule. + +#### 1.2.1 + +* Fix missing default host in v2ray http transport`s request +* Flush DNS cache for macOS when tun start/close +* Fix tun's DNS hijacking compatibility with systemd-resolved + +### 1.2.0 + +* Fix bugs and update dependencies + +Important changes since 1.1: + +* Introducing our [new iOS client application](/installation/clients/sfi/) +* Introducing [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp/) +* Add [platform options](/configuration/inbound/tun#platform) for tun inbound +* Add [ShadowTLS protocol v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) +* Add [VLESS server](/configuration/inbound/vless/) and [vision](/configuration/outbound/vless#flow) support +* Add [reality TLS](/configuration/shared/tls/) support +* Add [NTP service](/configuration/ntp/) +* Add [DHCP DNS server](/configuration/dns/server/) support +* Add SSH [host key validation](/configuration/outbound/ssh/) support +* Add [query_type](/configuration/dns/rule/) DNS rule item +* Add fallback support for v2ray transport +* Add custom TLS server support for http based v2ray transports +* Add health check support for http-based v2ray transports +* Add multiple configuration support + +#### 1.2-rc1 + +* Fix bugs and update dependencies + +#### 1.2-beta10 + +* Add multiple configuration support **1** +* Fix bugs and update dependencies + +*1*: + +Now you can pass the parameter `--config` or `-c` multiple times, or use the new parameter `--config-directory` or `-C` +to load all configuration files in a directory. + +Loaded configuration files are sorted by name. If you want to control the merge order, add a numeric prefix to the file +name. + +#### 1.1.7 + +* Improve the stability of the VMESS server +* Fix `auto_detect_interface` incorrectly identifying the default interface on Windows +* Fix bugs and update dependencies + +#### 1.2-beta9 + +* Introducing the [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp/) +* Add health check support for http-based v2ray transports +* Remove length limit on short_id for reality TLS config +* Fix bugs and update dependencies + +#### 1.2-beta8 + +* Update reality and uTLS libraries +* Fix `auto_detect_interface` incorrectly identifying the default interface on Windows + +#### 1.2-beta7 + +* Fix the compatibility issue between VLESS's vision sub-protocol and the Xray-core client +* Improve the stability of the VMESS server + +#### 1.2-beta6 + +* Introducing our [new iOS client application](/installation/clients/sfi/) +* Add [platform options](/configuration/inbound/tun#platform) for tun inbound +* Add custom TLS server support for http based v2ray transports +* Add generate commands +* Enable XUDP by default in VLESS +* Update reality server +* Update vision protocol +* Fixed [user flow in vless server](/configuration/inbound/vless#usersflow) +* Bug fixes +* Update dependencies + +#### 1.2-beta5 + +* Add [VLESS server](/configuration/inbound/vless/) and [vision](/configuration/outbound/vless#flow) support +* Add [reality TLS](/configuration/shared/tls/) support +* Fix match private address + +#### 1.1.6 + +* Improve vmess request +* Fix ipv6 redirect on Linux +* Fix match geoip private +* Fix parse hysteria UDP message +* Fix socks connect response +* Disable vmess header protection if transport enabled +* Update QUIC v2 version number and initial salt + +#### 1.2-beta4 + +* Add [NTP service](/configuration/ntp/) +* Add Add multiple server names and multi-user support for shadowtls +* Add strict mode support for shadowtls v3 +* Add uTLS support for shadowtls v3 + +#### 1.2-beta3 + +* Update QUIC v2 version number and initial salt +* Fix shadowtls v3 implementation + +#### 1.2-beta2 + +* Add [ShadowTLS protocol v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) +* Add fallback support for v2ray transport +* Fix parse hysteria UDP message +* Fix socks connect response +* Disable vmess header protection if transport enabled + +#### 1.2-beta1 + +* Add [DHCP DNS server](/configuration/dns/server/) support +* Add SSH [host key validation](/configuration/outbound/ssh/) support +* Add [query_type](/configuration/dns/rule/) DNS rule item +* Add v2ray [user stats](/configuration/experimental#statsusers) api +* Add new clash DNS query api +* Improve vmess request +* Fix ipv6 redirect on Linux +* Fix match geoip private + +#### 1.1.5 + +* Add Go 1.20 support +* Fix inbound default DF value +* Fix auth_user route for naive inbound +* Fix gRPC lite header +* Ignore domain case in route rules + +#### 1.1.4 + +* Fix DNS log +* Fix write to h2 conn after closed +* Fix create UDP DNS transport from plain IPv6 address + +#### 1.1.2 + +* Fix http proxy auth +* Fix user from stream packet conn +* Fix DNS response TTL +* Fix override packet conn +* Skip override system proxy bypass list +* Improve DNS log + +#### 1.1.1 + +* Fix acme config +* Fix vmess packet conn +* Suppress quic-go set DF error + +#### 1.1 + +* Fix close clash cache + +Important changes since 1.0: + +* Add support for use with android VPNService +* Add tun support for WireGuard outbound +* Add system tun stack +* Add comment filter for config +* Add option for allow optional proxy protocol header +* Add Clash mode and persistence support +* Add TLS ECH and uTLS support for outbound TLS options +* Add internal simple-obfs and v2ray-plugin +* Add ShadowsocksR outbound +* Add VLESS outbound and XUDP +* Skip wait for hysteria tcp handshake response +* Add v2ray mux support for all inbound +* Add XUDP support for VMess +* Improve websocket writer +* Refine tproxy write back +* Fix DNS leak caused by + Windows' ordinary multihomed DNS resolution behavior +* Add sniff_timeout listen option +* Add custom route support for tun +* Add option for custom wireguard reserved bytes +* Split bind_address into ipv4 and ipv6 +* Add ShadowTLS v1 and v2 support + +#### 1.1-rc1 + +* Fix TLS config for h2 server +* Fix crash when input bad method in shadowsocks multi-user inbound +* Fix listen UDP +* Fix check invalid packet on macOS + +#### 1.1-beta18 + +* Enhance defense against active probe for shadowtls server **1** + +**1**: + +The `fallback_after` option has been removed. + +#### 1.1-beta17 + +* Fix shadowtls server **1** + +*1*: + +Added [fallback_after](/configuration/inbound/shadowtls#fallback_after) option. + +#### 1.0.7 + +* Add support for new x/h2 deadline +* Fix copy pipe +* Fix decrypt xplus packet +* Fix macOS Ventura process name match +* Fix smux keepalive +* Fix vmess request buffer +* Fix h2c transport +* Fix tor geoip +* Fix udp connect for mux client +* Fix default dns transport strategy + +#### 1.1-beta16 + +* Improve shadowtls server +* Fix default dns transport strategy +* Update uTLS to v1.2.0 + +#### 1.1-beta15 + +* Add support for new x/h2 deadline +* Fix udp connect for mux client +* Fix dns buffer +* Fix quic dns retry +* Fix create TLS config +* Fix websocket alpn +* Fix tor geoip + +#### 1.1-beta14 + +* Add multi-user support for hysteria inbound **1** +* Add custom tls client support for std grpc +* Fix smux keep alive +* Fix vmess request buffer +* Fix default local DNS server behavior +* Fix h2c transport + +*1*: + +The `auth` and `auth_str` fields have been replaced by the `users` field. + +#### 1.1-beta13 + +* Add custom worker count option for WireGuard outbound +* Split bind_address into ipv4 and ipv6 +* Move WFP manipulation to strict route +* Fix WireGuard outbound panic when close +* Fix macOS Ventura process name match +* Fix QUIC connection migration by @HyNetwork +* Fix handling QUIC client SNI by @HyNetwork + +#### 1.1-beta12 + +* Fix uTLS config +* Update quic-go to v0.30.0 +* Update cloudflare-tls to go1.18.7 + +#### 1.1-beta11 + +* Add option for custom wireguard reserved bytes +* Fix shadowtls v2 +* Fix h3 dns transport +* Fix copy pipe +* Fix decrypt xplus packet +* Fix v2ray api +* Suppress no network error +* Improve local dns transport + +#### 1.1-beta10 + +* Add [sniff_timeout](/configuration/shared/listen#sniff_timeout) listen option +* Add [custom route](/configuration/inbound/tun#inet4_route_address) support for tun **1** +* Fix interface monitor +* Fix websocket headroom +* Fix uTLS handshake +* Fix ssh outbound +* Fix sniff fragmented quic client hello +* Fix DF for hysteria +* Fix naive overflow +* Check destination before udp connect +* Update uTLS to v1.1.5 +* Update tfo-go to v2.0.2 +* Update fsnotify to v1.6.0 +* Update grpc to v1.50.1 + +*1*: + +The `strict_route` on windows is removed. + +#### 1.0.6 + +* Fix ssh outbound +* Fix sniff fragmented quic client hello +* Fix naive overflow +* Check destination before udp connect + +#### 1.1-beta9 + +* Fix windows route **1** +* Add [v2ray statistics api](/configuration/experimental#v2ray-api-fields) +* Add ShadowTLS v2 support **2** +* Fixes and improvements + +**1**: + +* Fix 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) +* Flush Windows DNS cache when start/close + +**2**: + +See [ShadowTLS inbound](/configuration/inbound/shadowtls#version) +and [ShadowTLS outbound](/configuration/outbound/shadowtls#version) + +#### 1.1-beta8 + +* Fix leaks on close +* Improve websocket writer +* Refine tproxy write back +* Refine 4in6 processing +* Fix shadowsocks plugins +* Fix missing source address from transport connection +* Fix fqdn socks5 outbound connection +* Fix read source address from grpc-go + +#### 1.0.5 + +* Fix missing source address from transport connection +* Fix fqdn socks5 outbound connection +* Fix read source address from grpc-go + +#### 1.1-beta7 + +* Add v2ray mux and XUDP support for VMess inbound +* Add XUDP support for VMess outbound +* Disable DF on direct outbound by default +* Fix bugs in 1.1-beta6 + +#### 1.1-beta6 + +* Add [URLTest outbound](/configuration/outbound/urltest/) +* Fix bugs in 1.1-beta5 + +#### 1.1-beta5 + +* Print tags in version command +* Redirect clash hello to external ui +* Move shadowsocksr implementation to clash +* Make gVisor optional **1** +* Refactor to miekg/dns +* Refactor bind control +* Fix build on go1.18 +* Fix clash store-selected +* Fix close grpc conn +* Fix port rule match logic +* Fix clash api proxy type + +*1*: + +The build tag `no_gvisor` is replaced by `with_gvisor`. + +The default tun stack is changed to system. + +#### 1.0.4 + +* Fix close grpc conn +* Fix port rule match logic +* Fix clash api proxy type + +#### 1.1-beta4 + +* Add internal simple-obfs and v2ray-plugin [Shadowsocks plugins](/configuration/outbound/shadowsocks#plugin) +* Add [ShadowsocksR outbound](/configuration/outbound/shadowsocksr/) +* Add [VLESS outbound and XUDP](/configuration/outbound/vless/) +* Skip wait for hysteria tcp handshake response +* Fix socks4 client +* Fix hysteria inbound +* Fix concurrent write + +#### 1.0.3 + +* Fix socks4 client +* Fix hysteria inbound +* Fix concurrent write + +#### 1.1-beta3 + +* Fix using custom TLS client in http2 client +* Fix bugs in 1.1-beta2 + +#### 1.1-beta2 + +* Add Clash mode and persistence support **1** +* Add TLS ECH and uTLS support for outbound TLS options **2** +* Fix socks4 request +* Fix processing empty dns result + +*1*: + +Switching modes using the Clash API, and `store-selected` are now supported, +see [Experimental](/configuration/experimental/). + +*2*: + +ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello +message, see [TLS#ECH](/configuration/shared/tls#ech). + +uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance, +see [TLS#uTLS](/configuration/shared/tls#utls). + +#### 1.0.2 + +* Fix socks4 request +* Fix processing empty dns result + +#### 1.1-beta1 + +* Add support for use with android VPNService **1** +* Add tun support for WireGuard outbound **2** +* Add system tun stack **3** +* Add comment filter for config **4** +* Add option for allow optional proxy protocol header +* Add half close for smux +* Set UDP DF by default **5** +* Set default tun mtu to 9000 +* Update gVisor to 20220905.0 + +*1*: + +In previous versions, Android VPN would not work with tun enabled. + +The usage of tun over VPN and VPN over tun is now supported, see [Tun Inbound](/configuration/inbound/tun#auto_route). + +*2*: + +In previous releases, WireGuard outbound support was backed by the lower performance gVisor virtual interface. + +It achieves the same performance as wireguard-go by providing automatic system interface support. + +*3*: + +It does not depend on gVisor and has better performance in some cases. + +It is less compatible and may not be available in some environments. + +*4*: + +Annotated json configuration files are now supported. + +*5*: + +UDP fragmentation is now blocked by default. + +Including shadowsocks-libev, shadowsocks-rust and quic-go all disable segmentation by default. + +See [Dial Fields](/configuration/shared/dial#udp_fragment) +and [Listen Fields](/configuration/shared/listen#udp_fragment). + +#### 1.0.1 + +* Fix match 4in6 address in ip_cidr +* Fix clash api log level format error +* Fix clash api unknown proxy type + +#### 1.0 + +* Fix wireguard reconnect +* Fix naive inbound +* Fix json format error message +* Fix processing vmess termination signal +* Fix hysteria stream error +* Fix listener close when proxyproto failed + +#### 1.0-rc1 + +* Fix write log timestamp +* Fix write zero +* Fix dial parallel in direct outbound +* Fix write trojan udp +* Fix DNS routing +* Add attribute support for geosite +* Update documentation for [Dial Fields](/configuration/shared/dial/) + +#### 1.0-beta3 + +* Add [chained inbound](/configuration/shared/listen#detour) support +* Add process_path rule item +* Add macOS redirect support +* Add ShadowTLS [Inbound](/configuration/inbound/shadowtls/), [Outbound](/configuration/outbound/shadowtls/) + and [Examples](/examples/shadowtls/) +* Fix search android package in non-owner users +* Fix socksaddr type condition +* Fix smux session status +* Refactor inbound and outbound documentation +* Minor fixes + +#### 1.0-beta2 + +* Add strict_route option for [Tun inbound](/configuration/inbound/tun#strict_route) +* Add packetaddr support for [VMess outbound](/configuration/outbound/vmess#packet_addr) +* Add better performing alternative gRPC implementation +* Add [docker image](https://github.com/SagerNet/sing-box/pkgs/container/sing-box) +* Fix sniff override destination + +#### 1.0-beta1 + +* Initial release + +##### 2022/08/26 + +* Fix ipv6 route on linux +* Fix read DNS message + +##### 2022/08/25 + +* Let vmess use zero instead of auto if TLS enabled +* Add trojan fallback for ALPN +* Improve ip_cidr rule +* Fix format bind_address +* Fix http proxy with compressed response +* Fix route connections + +##### 2022/08/24 + +* Fix naive padding +* Fix unix search path +* Fix close non-duplex connections +* Add ACME EAB support +* Fix early close on windows and catch any +* Initial zh-CN document translation + +##### 2022/08/23 + +* Add [V2Ray Transport](/configuration/shared/v2ray-transport/) support for VMess and Trojan +* Allow plain http request in Naive inbound (It can now be used with nginx) +* Add proxy protocol support +* Free memory after start +* Parse X-Forward-For in HTTP requests +* Handle SIGHUP signal + +##### 2022/08/22 + +* Add strategy setting for each [DNS server](/configuration/dns/server/) +* Add bind address to outbound options + +##### 2022/08/21 + +* Add [Tor outbound](/configuration/outbound/tor/) +* Add [SSH outbound](/configuration/outbound/ssh/) + +##### 2022/08/20 + +* Attempt to unwrap ip-in-fqdn socksaddr +* Fix read packages in android 12 +* Fix route on some android devices +* Improve linux process searcher +* Fix write socks5 username password auth request +* Skip bind connection with private destination to interface +* Add [Trojan connection fallback](/configuration/inbound/trojan#fallback) + +##### 2022/08/19 + +* Add Hysteria [Inbound](/configuration/inbound/hysteria/) and [Outbund](/configuration/outbound/hysteria/) +* Add [ACME TLS certificate issuer](/configuration/shared/tls/) +* Allow read config from stdin (-c stdin) +* Update gVisor to 20220815.0 + +##### 2022/08/18 + +* Fix find process with lwip stack +* Fix crash on shadowsocks server +* Fix crash on darwin tun +* Fix write log to file + +##### 2022/08/17 + +* Improve async dns transports + +##### 2022/08/16 + +* Add ip_version (route/dns) rule item +* Add [WireGuard](/configuration/outbound/wireguard/) outbound + +##### 2022/08/15 + +* Add uid, android user and package rules support in [Tun](/configuration/inbound/tun/) routing. + +##### 2022/08/13 + +* Fix dns concurrent write + +##### 2022/08/12 + +* Performance improvements +* Add UoT option for [SOCKS](/configuration/outbound/socks/) outbound + +##### 2022/08/11 + +* Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks/) outbound, UoT support for all inbounds + +##### 2022/08/10 + +* Add full-featured [Naive](/configuration/inbound/naive/) inbound +* Fix default dns server option [#9] by iKirby + +##### 2022/08/09 + +No changelog before. + +[#9]: https://github.com/SagerNet/sing-box/pull/9 diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md new file mode 100644 index 00000000..b76a6418 --- /dev/null +++ b/docs/clients/android/features.md @@ -0,0 +1,67 @@ +# :material-decagram: Features + +#### UI options + +* Display realtime network speed in the notification + +#### Service + +SFA allows you to run sing-box through ForegroundService or VpnService (when TUN is required). + +#### TUN + +SFA provides an unprivileged TUN implementation through Android VpnService. + +| TUN inbound option | Available | Note | +|-------------------------------|------------------|--------------------| +| `interface_name` | :material-close: | Managed by Android | +| `inet4_address` | :material-check: | / | +| `inet6_address` | :material-check: | / | +| `mtu` | :material-check: | / | +| `gso` | :material-close: | No permission | +| `auto_route` | :material-check: | / | +| `strict_route` | :material-close: | Not implemented | +| `inet4_route_address` | :material-check: | / | +| `inet6_route_address` | :material-check: | / | +| `inet4_route_exclude_address` | :material-check: | / | +| `inet6_route_exclude_address` | :material-check: | / | +| `endpoint_independent_nat` | :material-check: | / | +| `stack` | :material-check: | / | +| `include_interface` | :material-close: | No permission | +| `exclude_interface` | :material-close: | No permission | +| `include_uid` | :material-close: | No permission | +| `exclude_uid` | :material-close: | No permission | +| `include_android_user` | :material-close: | No permission | +| `include_package` | :material-check: | / | +| `exclude_package` | :material-check: | / | +| `platform` | :material-check: | / | + +| Route/DNS rule option | Available | Note | +|-----------------------|------------------|-----------------------------------| +| `process_name` | :material-close: | No permission | +| `process_path` | :material-close: | No permission | +| `process_path_regex` | :material-close: | No permission | +| `package_name` | :material-check: | / | +| `package_name_regex` | :material-check: | / | +| `user` | :material-close: | Use `package_name` instead | +| `user_id` | :material-close: | Use `package_name` instead | +| `wifi_ssid` | :material-check: | Fine location permission required | +| `wifi_bssid` | :material-check: | Fine location permission required | + +### Override + +Overrides profile configuration items with platform-specific values. + +#### Per-app proxy + +SFA allows you to select a list of Android apps that require proxying or bypassing in the graphical interface to +override the `include_package` and `exclude_package` configuration items. + +In particular, the selector also provides the “China apps” scanning feature, providing Chinese users with an excellent +experience to bypass apps that do not require a proxy. Specifically, by scanning China application or SDK +characteristics through dex class path and other means, there will be almost no missed reports. + +### Chore + +* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory) +* Crash logs is located in `$working_directory/stderr.log` diff --git a/docs/clients/android/index.md b/docs/clients/android/index.md new file mode 100644 index 00000000..19cc9005 --- /dev/null +++ b/docs/clients/android/index.md @@ -0,0 +1,23 @@ +--- +icon: material/android +--- + +# sing-box for Android + +SFA allows users to manage and run local or remote sing-box configuration files, and provides +platform-specific function implementation, such as TUN transparent proxy implementation. + +## :material-graph: Requirements + +* Android 5.0+ + +## :material-download: Download + +* [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa) +* [Play Store (Beta)](https://play.google.com/apps/testing/io.nekohasekai.sfa) +* [GitHub Releases](https://github.com/SagerNet/sing-box/releases) +* [F-Droid](https://f-droid.org/packages/io.nekohasekai.sfa/) (Unified signature via reproducible builds) + +## :material-source-repository: Source code + +* [GitHub](https://github.com/SagerNet/sing-box-for-android) diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md new file mode 100644 index 00000000..e1f3d7cc --- /dev/null +++ b/docs/clients/apple/features.md @@ -0,0 +1,55 @@ +# :material-decagram: Features + +#### UI options + +* Always On +* Include All Networks (Proxy traffic for LAN and cellular services) +* (Apple tvOS) Import profile from iPhone/iPad + +#### Service + +SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application Extension or System Extension. + +#### TUN + +SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension. + +| TUN inbound option | Available | Note | +|-------------------------------|-------------------|-------------------| +| `interface_name` | :material-close:️ | Managed by Darwin | +| `inet4_address` | :material-check: | / | +| `inet6_address` | :material-check: | / | +| `mtu` | :material-check: | / | +| `gso` | :material-close: | Not implemented | +| `auto_route` | :material-check: | / | +| `strict_route` | :material-close:️ | Not implemented | +| `inet4_route_address` | :material-check: | / | +| `inet6_route_address` | :material-check: | / | +| `inet4_route_exclude_address` | :material-check: | / | +| `inet6_route_exclude_address` | :material-check: | / | +| `endpoint_independent_nat` | :material-check: | / | +| `stack` | :material-check: | / | +| `include_interface` | :material-close:️ | Not implemented | +| `exclude_interface` | :material-close:️ | Not implemented | +| `include_uid` | :material-close:️ | Not implemented | +| `exclude_uid` | :material-close:️ | Not implemented | +| `include_android_user` | :material-close:️ | Not implemented | +| `include_package` | :material-close:️ | Not implemented | +| `exclude_package` | :material-close:️ | Not implemented | +| `platform` | :material-check: | / | + +| Route/DNS rule option | Available | Note | +|-----------------------|------------------|-----------------------| +| `process_name` | :material-close: | No permission | +| `process_path` | :material-close: | No permission | +| `process_path_regex` | :material-close: | No permission | +| `package_name` | :material-close: | / | +| `package_name_regex` | :material-close: | / | +| `user` | :material-close: | No permission | +| `user_id` | :material-close: | No permission | +| `wifi_ssid` | :material-alert: | Only supported on iOS | +| `wifi_bssid` | :material-alert: | Only supported on iOS | + +### Chore + +* Crash logs is located in `Settings` -> `View Service Log` diff --git a/docs/clients/apple/index.md b/docs/clients/apple/index.md new file mode 100644 index 00000000..ef9807e7 --- /dev/null +++ b/docs/clients/apple/index.md @@ -0,0 +1,41 @@ +--- +icon: material/apple +--- + +# sing-box for Apple platforms + +SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides +platform-specific function implementation, such as TUN transparent proxy implementation. + +!!! failure "" + + Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected) + +## :material-graph: Requirements + +* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+ +* An Apple account outside of mainland China + +## :material-download: Download + +* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~ +* TestFlight (Beta) + +TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai) +(one-time sponsorships are accepted). +Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot) +or sending us your Apple ID [via email](mailto:contact@sagernet.org). + +## ~~:material-file-download: Download (macOS standalone version)~~ + +* ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~ + +```bash +# brew install sfm +``` + +* ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~ + +## :material-source-repository: Source code + +* [GitHub](https://github.com/SagerNet/sing-box-for-apple) diff --git a/docs/clients/general.md b/docs/clients/general.md new file mode 100644 index 00000000..4d57c991 --- /dev/null +++ b/docs/clients/general.md @@ -0,0 +1,63 @@ +--- +icon: material/pencil-ruler +--- + +# General + +Describes and explains the functions implemented uniformly by sing-box graphical clients. + +### Profile + +Profile describes a sing-box configuration file and its state. + +#### Local + +* Local Profile represents a local sing-box configuration with minimal state +* The graphical client must provide an editor to modify configuration content + +#### iCloud (on iOS and macOS) + +* iCloud Profile represents a remote sing-box configuration with iCloud as the update source +* The configuration file is stored in the sing-box folder under iCloud +* The graphical client must provide an editor to modify configuration content + +#### Remote + +* Remote Profile represents a remote sing-box configuration with a URL as the update source. +* The graphical client should provide a configuration content viewer +* The graphical client must implement automatic profile update (default interval is 60 minutes) and HTTP Basic + authorization. + +At the same time, the graphical client must provide support for importing remote profiles +through a specific URL Scheme. The URL is defined as follows: + +``` +sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName +``` + +### Dashboard + +While the sing-box service is running, the graphical client should provide a Dashboard interface to manage the service. + +#### Status + +Dashboard should display status information such as memory, connection, and traffic. + +#### Mode + +Dashboard should provide a Mode selector for switching when the configuration uses at least two `clash_mode` values. + +#### Groups + +When the configuration includes group outbounds (specifically, Selector or URLTest), +the dashboard should provide a Group selector for status display or switching. + +### Chore + +#### Core + +Graphical clients should provide a Core region: + +* Display the current sing-box version +* Provides a button to clean the working directory +* Provides a memory limiter switch \ No newline at end of file diff --git a/docs/clients/index.md b/docs/clients/index.md new file mode 100644 index 00000000..45d2c9a9 --- /dev/null +++ b/docs/clients/index.md @@ -0,0 +1,13 @@ +# :material-cellphone-link: Graphical Clients + +Maintained by Project S to provide a unified experience and platform-specific functionality. + +| Platform | Client | +|---------------------------------------|------------------------------------------| +| :material-android: Android | [sing-box for Android](./android/) | +| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) | +| :material-laptop: Desktop | Working in progress | + +Some third-party projects that claim to use sing-box or use sing-box as a selling point are not listed here. The core +motivation of the maintainers of such projects is to acquire more users, and even though they provide friendly VPN +client features, the code is usually of poor quality and contains ads. diff --git a/docs/clients/index.zh.md b/docs/clients/index.zh.md new file mode 100644 index 00000000..736b42ea --- /dev/null +++ b/docs/clients/index.zh.md @@ -0,0 +1,12 @@ +# :material-cellphone-link: 图形界面客户端 + +由 Project S 维护,提供统一的体验与平台特定的功能。 + +| 平台 | 客户端 | +|---------------------------------------|------------------------------------------| +| :material-android: Android | [sing-box for Android](./android/) | +| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) | +| :material-laptop: Desktop | 施工中 | + +此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业 +VPN 客户端功能, 但代码质量很差且包含广告。 diff --git a/docs/clients/privacy.md b/docs/clients/privacy.md new file mode 100644 index 00000000..61df7102 --- /dev/null +++ b/docs/clients/privacy.md @@ -0,0 +1,18 @@ +--- +icon: material/security +--- + +# Privacy policy + +sing-box and official graphics clients do not collect or share personal data, +and the data generated by the software is always on your device. + +## Android + +The broad package (App) visibility (QUERY_ALL_PACKAGES) permission +is used to provide per-application proxy features for VPN, +sing-box will not collect your app list. + +If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules, +sing-box uses the location permission in the background +to get information about the connected Wi-Fi network to make them work. diff --git a/docs/configuration/certificate/index.md b/docs/configuration/certificate/index.md new file mode 100644 index 00000000..88d73380 --- /dev/null +++ b/docs/configuration/certificate/index.md @@ -0,0 +1,59 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [Chrome Root Store](#store) + +# Certificate + +### Structure + +```json +{ + "store": "", + "certificate": [], + "certificate_path": [], + "certificate_directory_path": [] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### store + +The default X509 trusted CA certificate list. + +| Type | Description | +|--------------------|----------------------------------------------------------------------------------------------------------------| +| `system` (default) | System trusted CA certificates | +| `mozilla` | [Mozilla Included List](https://wiki.mozilla.org/CA/Included_Certificates) with China CA certificates removed | +| `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy) with China CA certificates removed | +| `none` | Empty list | + +#### certificate + +The certificate line array to trust, in PEM format. + +#### certificate_path + +!!! note "" + + Will be automatically reloaded if file modified. + +The paths to certificates to trust, in PEM format. + +#### certificate_directory_path + +!!! note "" + + Will be automatically reloaded if file modified. + +The directory path to search for certificates to trust,in PEM format. diff --git a/docs/configuration/certificate/index.zh.md b/docs/configuration/certificate/index.zh.md new file mode 100644 index 00000000..77f3fd88 --- /dev/null +++ b/docs/configuration/certificate/index.zh.md @@ -0,0 +1,59 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [Chrome Root Store](#store) + +# 证书 + +### 结构 + +```json +{ + "store": "", + "certificate": [], + "certificate_path": [], + "certificate_directory_path": [] +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 字段 + +#### store + +默认的 X509 受信任 CA 证书列表。 + +| 类型 | 描述 | +|-------------------|--------------------------------------------------------------------------------------------| +| `system`(默认) | 系统受信任的 CA 证书 | +| `mozilla` | [Mozilla 包含列表](https://wiki.mozilla.org/CA/Included_Certificates)(已移除中国 CA 证书) | +| `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy)(已移除中国 CA 证书) | +| `none` | 空列表 | + +#### certificate + +要信任的证书行数组,PEM 格式。 + +#### certificate_path + +!!! note "" + + 文件修改时将自动重新加载。 + +要信任的证书路径,PEM 格式。 + +#### certificate_directory_path + +!!! note "" + + 文件修改时将自动重新加载。 + +搜索要信任的证书的目录路径,PEM 格式。 \ No newline at end of file diff --git a/docs/configuration/dns/fakeip.md b/docs/configuration/dns/fakeip.md new file mode 100644 index 00000000..a0524dc8 --- /dev/null +++ b/docs/configuration/dns/fakeip.md @@ -0,0 +1,31 @@ +--- +icon: material/note-remove +--- + +!!! failure "Removed in sing-box 1.14.0" + + Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats). + +### Structure + +```json +{ + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" +} +``` + +### Fields + +#### enabled + +Enable FakeIP service. + +#### inet4_range + +IPv4 address range for FakeIP. + +#### inet6_range + +IPv6 address range for FakeIP. diff --git a/docs/configuration/dns/fakeip.zh.md b/docs/configuration/dns/fakeip.zh.md new file mode 100644 index 00000000..1e5eca60 --- /dev/null +++ b/docs/configuration/dns/fakeip.zh.md @@ -0,0 +1,31 @@ +--- +icon: material/note-remove +--- + +!!! failure "已在 sing-box 1.14.0 移除" + + 旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 + +### 结构 + +```json +{ + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" +} +``` + +### 字段 + +#### enabled + +启用 FakeIP 服务。 + +#### inet4_range + +用于 FakeIP 的 IPv4 地址范围。 + +#### inet6_range + +用于 FakeIP 的 IPv6 地址范围。 diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md new file mode 100644 index 00000000..b78a49e7 --- /dev/null +++ b/docs/configuration/dns/index.md @@ -0,0 +1,133 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [independent_cache](#independent_cache) + :material-plus: [optimistic](#optimistic) + +!!! quote "Changes in sing-box 1.12.0" + + :material-decagram: [servers](#servers) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [cache_capacity](#cache_capacity) + +# DNS + +### Structure + +```json +{ + "dns": { + "servers": [], + "rules": [], + "final": "", + "strategy": "", + "disable_cache": false, + "disable_expire": false, + "independent_cache": false, + "cache_capacity": 0, + "optimistic": false, // or {} + "reverse_mapping": false, + "client_subnet": "", + "fakeip": {} + } +} + +``` + +### Fields + +| Key | Format | +|----------|---------------------------------| +| `server` | List of [DNS Server](./server/) | +| `rules` | List of [DNS Rule](./rule/) | +| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) | + +#### final + +Default dns server tag. + +The first server will be used if empty. + +#### strategy + +Default domain strategy for resolving the domain names. + +One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. + +#### disable_cache + +Disable dns cache. + +Conflict with `optimistic`. + +#### disable_expire + +Disable dns cache expire. + +Conflict with `optimistic`. + +#### independent_cache + +!!! failure "Deprecated in sing-box 1.14.0" + + `independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache). + +Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance. + +#### cache_capacity + +!!! question "Since sing-box 1.11.0" + +LRU cache capacity. + +Value less than 1024 will be ignored. + +#### optimistic + +!!! question "Since sing-box 1.14.0" + +Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window, +the stale response is returned immediately while a background refresh is triggered. + +Conflict with `disable_cache` and `disable_expire`. + +Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used. + +```json +{ + "enabled": true, + "timeout": "3d" +} +``` + +##### enabled + +Enable optimistic DNS caching. + +##### timeout + +The maximum time an expired cache entry can be served optimistically. + +`3d` is used by default. + +#### reverse_mapping + +Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. + +Since this process relies on the act of resolving domain names by an application before making a request, it can be +problematic in environments such as macOS, where DNS is proxied and cached by the system. + +#### client_subnet + +!!! question "Since sing-box 1.9.0" + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md new file mode 100644 index 00000000..ae06b8ab --- /dev/null +++ b/docs/configuration/dns/index.zh.md @@ -0,0 +1,135 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [independent_cache](#independent_cache) + :material-plus: [optimistic](#optimistic) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-decagram: [servers](#servers) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [cache_capacity](#cache_capacity) + +# DNS + +### 结构 + +```json +{ + "dns": { + "servers": [], + "rules": [], + "final": "", + "strategy": "", + "disable_cache": false, + "disable_expire": false, + "independent_cache": false, + "cache_capacity": 0, + "optimistic": false, // or {} + "reverse_mapping": false, + "client_subnet": "", + "fakeip": {} + } +} + +``` + +### 字段 + +| 键 | 格式 | +|----------|-------------------------| +| `server` | 一组 [DNS 服务器](./server/) | +| `rules` | 一组 [DNS 规则](./rule/) | + +#### final + +默认 DNS 服务器的标签。 + +默认使用第一个服务器。 + +#### strategy + +默认解析域名策略。 + +可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 + +#### disable_cache + +禁用 DNS 缓存。 + +与 `optimistic` 冲突。 + +#### disable_expire + +禁用 DNS 缓存过期。 + +与 `optimistic` 冲突。 + +#### independent_cache + +!!! failure "已在 sing-box 1.14.0 废弃" + + `independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。 + +使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 + +#### cache_capacity + +!!! question "自 sing-box 1.11.0 起" + +LRU 缓存容量。 + +小于 1024 的值将被忽略。 + +#### optimistic + +!!! question "自 sing-box 1.14.0 起" + +启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时, +立即返回过期的响应,同时在后台触发刷新。 + +与 `disable_cache` 和 `disable_expire` 冲突。 + +接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。 + +```json +{ + "enabled": true, + "timeout": "3d" +} +``` + +##### enabled + +启用乐观 DNS 缓存。 + +##### timeout + +过期缓存条目可被乐观提供的最长时间。 + +默认使用 `3d`。 + +#### reverse_mapping + +在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 + +由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。 + +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。 + +#### fakeip :material-note-remove: + +[FakeIP](./fakeip/) 设置。 diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md new file mode 100644 index 00000000..b0785b77 --- /dev/null +++ b/docs/configuration/dns/rule.md @@ -0,0 +1,699 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [match_response](#match_response) + :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [response_rcode](#response_rcode) + :material-plus: [response_answer](#response_answer) + :material-plus: [response_ns](#response_ns) + :material-plus: [response_extra](#response_extra) + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [ip_version](#ip_version) + :material-alert: [query_type](#query_type) + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [interface_address](#interface_address) + :material-plus: [network_interface_address](#network_interface_address) + :material-plus: [default_interface_address](#default_interface_address) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [ip_accept_any](#ip_accept_any) + :material-delete-clock: [outbound](#outbound) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [action](#action) + :material-alert: [server](#server) + :material-alert: [disable_cache](#disable_cache) + :material-alert: [rewrite_ttl](#rewrite_ttl) + :material-alert: [client_subnet](#client_subnet) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + +!!! quote "Changes in sing-box 1.10.0" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [process_path_regex](#process_path_regex) + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [geoip](#geoip) + :material-plus: [ip_cidr](#ip_cidr) + :material-plus: [ip_is_private](#ip_is_private) + :material-plus: [client_subnet](#client_subnet) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + +### Structure + +```json +{ + "dns": { + "rules": [ + { + "inbound": [ + "mixed-in" + ], + "ip_version": 6, + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": "tcp", + "auth_user": [ + "usera", + "userb" + ], + "protocol": [ + "tls", + "http", + "quic" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_ip_is_private": false, + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "package_name_regex": [ + "^com\\.termux.*" + ], + "user": [ + "sekai" + ], + "user_id": [ + 1000 + ], + "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "interface_address": { + "en0": [ + "2000::/3" + ] + }, + "network_interface_address": { + "wifi": [ + "2000::/3" + ] + }, + "default_interface_address": [ + "2000::/3" + ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], + "rule_set_ip_cidr_match_source": false, + "match_response": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, + "ip_accept_any": false, + "response_rcode": "", + "response_answer": [], + "response_ns": [], + "response_extra": [], + "invert": false, + "outbound": [ + "direct" + ], + "action": "route", + "server": "local", + + // Deprecated + + "rule_set_ip_cidr_accept_empty": false, + "rule_set_ipcidr_match_source": false, + "geosite": [ + "cn" + ], + "source_geoip": [ + "private" + ], + "geoip": [ + "cn" + ] + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "action": "route", + "server": "local" + } + ] + } +} + +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) && + (`port` || `port_range`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && + (`source_port` || `source_port_range`) && + `other fields` + + Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. + +#### inbound + +Tags of [Inbound](/configuration/inbound/). + +#### ip_version + +!!! quote "Changes in sing-box 1.14.0" + + This field now also applies when a DNS rule is matched from an internal + domain resolution that does not target a specific DNS server, such as a + [`resolve`](../../route/rule_action/#resolve) route rule action without a + `server` set. In earlier versions, only DNS queries received from a + client evaluated this field. See + [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules) + for the full list. + + Setting this field makes the DNS rule incompatible in the same DNS + configuration with Legacy Address Filter Fields in DNS rules, the Legacy + `strategy` DNS rule action option, and the Legacy + `rule_set_ip_cidr_accept_empty` DNS rule item. To combine with + address-based filtering, use the [`evaluate`](../rule_action/#evaluate) + action and [`match_response`](#match_response). + +4 (A DNS query) or 6 (AAAA DNS query). + +Not limited if empty. + +#### query_type + +!!! quote "Changes in sing-box 1.14.0" + + This field now also applies when a DNS rule is matched from an internal + domain resolution that does not target a specific DNS server, such as a + [`resolve`](../../route/rule_action/#resolve) route rule action without a + `server` set. In earlier versions, only DNS queries received from a + client evaluated this field. See + [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules) + for the full list. + + Setting this field makes the DNS rule incompatible in the same DNS + configuration with Legacy Address Filter Fields in DNS rules, the Legacy + `strategy` DNS rule action option, and the Legacy + `rule_set_ip_cidr_accept_empty` DNS rule item. To combine with + address-based filtering, use the [`evaluate`](../rule_action/#evaluate) + action and [`match_response`](#match_response). + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### auth_user + +Username, see each inbound for details. + +#### protocol + +Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### geosite + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). + +Match geosite. + +#### source_geoip + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). + +Match source geoip. + +#### source_ip_cidr + +Match source IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + +#### package_name + +Match android package name. + +#### package_name_regex + +!!! question "Since sing-box 1.14.0" + +Match android package name using regular expression. + +#### user + +!!! quote "" + + Only supported on Linux. + +Match user name. + +#### user_id + +!!! quote "" + + Only supported on Linux. + +Match user id. + +#### clash_mode + +Match Clash mode. + +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match network type. + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match if network is considered Metered (on Android) or considered expensive, +such as Cellular or a Personal Hotspot (on Apple platforms). + +#### network_is_constrained + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Match if network is in Low Data Mode. + +#### interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match interface address. + +#### network_interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Matches network interface (same values as `network_type`) address. + +#### default_interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match default interface address. + +#### source_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. + +Match source device MAC address. + +#### source_hostname + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. + +Match source device hostname from DHCP leases. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms, or on Linux. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms, or on Linux. + +Match WiFi BSSID. + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [rule-set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.9.0" + +!!! failure "Deprecated in sing-box 1.10.0" + + `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +Make `ip_cidr` rule items in rule-sets match the source IP. + +#### rule_set_ip_cidr_match_source + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` rule items in rule-sets match the source IP. + +#### match_response + +!!! question "Since sing-box 1.14.0" + +Enable response-based matching. When enabled, this rule matches against the evaluated response +(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action) +instead of only matching the original query. + +The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action. + +Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`). +Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields. + +#### ip_accept_any + +!!! question "Since sing-box 1.12.0" + +Match when the DNS query response contains at least one address. + +#### invert + +Invert match result. + +#### outbound + +!!! failure "Deprecated in sing-box 1.12.0" + + `outbound` rule items are deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). + +Match outbound. + +`any` can be used as a value to match any outbound. + +#### action + +==Required== + +See [DNS Rule Actions](../rule_action/) for details. + +#### server + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [DNS Rule Action](../rule_action#route). + +#### disable_cache + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [DNS Rule Action](../rule_action#route). + +#### rewrite_ttl + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [DNS Rule Action](../rule_action#route). + +#### client_subnet + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [DNS Rule Action](../rule_action#route). + +### Legacy Address Filter Fields + +!!! failure "Deprecated in sing-box 1.14.0" + + Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0, + check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + +Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped. + +!!! info "" + + `ip_cidr` items in included rule-sets also takes effect as an address filtering field. + +!!! note "" + + Enable `experimental.cache_file.store_rdrc` to cache results. + +#### geoip + +!!! failure "Removed in sing-box 1.12.0" + + GeoIP is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). + +Match GeoIP with query response. + +#### ip_cidr + +!!! question "Since sing-box 1.9.0" + +Match IP CIDR with query response. + +As a Legacy Address Filter Field, deprecated. Use with `match_response` instead, +check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + +#### ip_is_private + +!!! question "Since sing-box 1.9.0" + +Match private IP with query response. + +As a Legacy Address Filter Field, deprecated. Use with `match_response` instead, +check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + +#### rule_set_ip_cidr_accept_empty + +!!! question "Since sing-box 1.10.0" + +!!! failure "Deprecated in sing-box 1.14.0" + + `rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0, + check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + +Make `ip_cidr` rules in rule-sets accept empty query response. + +### Response Match Fields + +!!! question "Since sing-box 1.14.0" + +Match fields for the evaluated response. Require `match_response` to be set to `true` +and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response. + +That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action. + +#### response_rcode + +Match DNS response code. + +Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode). + +#### response_answer + +Match DNS answer records. + +Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer). + +#### response_ns + +Match DNS name server records. + +Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns). + +#### response_extra + +Match DNS extra records. + +Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra). + +### Logical Fields + +#### type + +`logical` + +#### mode + +`and` or `or` + +#### rules + +Included rules. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md new file mode 100644 index 00000000..cc0a3037 --- /dev/null +++ b/docs/configuration/dns/rule.zh.md @@ -0,0 +1,694 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [match_response](#match_response) + :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [response_rcode](#response_rcode) + :material-plus: [response_answer](#response_answer) + :material-plus: [response_ns](#response_ns) + :material-plus: [response_extra](#response_extra) + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [ip_version](#ip_version) + :material-alert: [query_type](#query_type) + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [interface_address](#interface_address) + :material-plus: [network_interface_address](#network_interface_address) + :material-plus: [default_interface_address](#default_interface_address) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [ip_accept_any](#ip_accept_any) + :material-delete-clock: [outbound](#outbound) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [action](#action) + :material-alert: [server](#server) + :material-alert: [disable_cache](#disable_cache) + :material-alert: [rewrite_ttl](#rewrite_ttl) + :material-alert: [client_subnet](#client_subnet) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + +!!! quote "sing-box 1.10.0 中的更改" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [process_path_regex](#process_path_regex) + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [geoip](#geoip) + :material-plus: [ip_cidr](#ip_cidr) + :material-plus: [ip_is_private](#ip_is_private) + :material-plus: [client_subnet](#client_subnet) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + +### 结构 + +```json +{ + "dns": { + "rules": [ + { + "inbound": [ + "mixed-in" + ], + "ip_version": 6, + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": "tcp", + "auth_user": [ + "usera", + "userb" + ], + "protocol": [ + "tls", + "http", + "quic" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_ip_is_private": false, + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "package_name_regex": [ + "^com\\.termux.*" + ], + "user": [ + "sekai" + ], + "user_id": [ + 1000 + ], + "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "interface_address": { + "en0": [ + "2000::/3" + ] + }, + "network_interface_address": { + "wifi": [ + "2000::/3" + ] + }, + "default_interface_address": [ + "2000::/3" + ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], + "rule_set_ip_cidr_match_source": false, + "match_response": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, + "ip_accept_any": false, + "response_rcode": "", + "response_answer": [], + "response_ns": [], + "response_extra": [], + "invert": false, + "outbound": [ + "direct" + ], + "action": "route", + "server": "local", + + // 已弃用 + + "rule_set_ip_cidr_accept_empty": false, + "rule_set_ipcidr_match_source": false, + "geosite": [ + "cn" + ], + "source_geoip": [ + "private" + ], + "geoip": [ + "cn" + ] + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "action": "route", + "server": "local" + } + ] + } +} + +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 默认字段 + +!!! note "" + + 默认规则使用以下匹配逻辑: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) && + (`port` || `port_range`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && + (`source_port` || `source_port_range`) && + `other fields` + + 另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。 + +#### inbound + +[入站](/zh/configuration/inbound/) 标签. + +#### ip_version + +!!! quote "sing-box 1.14.0 中的更改" + + 此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效, + 例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。 + 此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅 + [迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。 + + 在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与 + 旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项, + 或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与 + 基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和 + [`match_response`](#match_response)。 + +4 (A DNS 查询) 或 6 (AAAA DNS 查询)。 + +默认不限制。 + +#### query_type + +!!! quote "sing-box 1.14.0 中的更改" + + 此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效, + 例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。 + 此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅 + [迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。 + + 在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与 + 旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项, + 或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与 + 基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和 + [`match_response`](#match_response)。 + +DNS 查询类型。值可以为整数或者类型名称字符串。 + +#### network + +`tcp` 或 `udp`。 + +#### auth_user + +认证用户名,参阅入站设置。 + +#### protocol + +探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。 + +#### domain + +匹配完整域名。 + +#### domain_suffix + +匹配域名后缀。 + +#### domain_keyword + +匹配域名关键字。 + +#### domain_regex + +匹配域名正则表达式。 + +#### geosite + +!!! failure "已在 sing-box 1.12.0 中被移除" + + GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 + +匹配 Geosite。 + +#### source_geoip + +!!! failure "已在 sing-box 1.12.0 中被移除" + + GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 + +匹配源 GeoIP。 + +#### source_ip_cidr + +匹配源 IP CIDR。 + +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + +#### source_port + +匹配源端口。 + +#### source_port_range + +匹配源端口范围。 + +#### port + +匹配端口。 + +#### port_range + +匹配端口范围。 + +#### process_name + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配进程名称。 + +#### process_path + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配进程路径。 + +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + +#### package_name + +匹配 Android 应用包名。 + +#### package_name_regex + +!!! question "自 sing-box 1.14.0 起" + +使用正则表达式匹配 Android 应用包名。 + +#### user + +!!! quote "" + + 仅支持 Linux。 + +匹配用户名。 + +#### user_id + +!!! quote "" + + 仅支持 Linux。 + +匹配用户 ID。 + +#### clash_mode + +匹配 Clash 模式。 + +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络类型。 + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配如果网络被视为计费 (在 Android) 或被视为昂贵, +像蜂窝网络或个人热点 (在 Apple 平台)。 + +#### network_is_constrained + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +匹配如果网络在低数据模式下。 + +#### interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配接口地址。 + +#### network_interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络接口(可用值同 `network_type`)地址。 + +#### default_interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配默认接口地址。 + +#### source_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 + +匹配源设备 MAC 地址。 + +#### source_hostname + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 + +匹配源设备从 DHCP 租约获取的主机名。 + +#### wifi_ssid + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。 + +匹配 WiFi SSID。 + +#### wifi_bssid + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。 + +匹配 WiFi BSSID。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.9.0 起" + +!!! failure "已在 sing-box 1.10.0 废弃" + + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### rule_set_ip_cidr_match_source + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### match_response + +!!! question "自 sing-box 1.14.0 起" + +启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。 + +该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。 + +响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。 +当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr`、`ip_is_private` 和 `ip_accept_any` 也需要此选项。 + +#### ip_accept_any + +!!! question "自 sing-box 1.12.0 起" + +当 DNS 查询响应包含至少一个地址时匹配。 + +#### invert + +反选匹配结果。 + +#### outbound + +!!! failure "已在 sing-box 1.12.0 废弃" + + `outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。 + +匹配出站。 + +`any` 可作为值用于匹配任意出站。 + +#### action + +==必填== + +参阅 [规则动作](../rule_action/)。 + +#### server + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [DNS 规则动作](../rule_action#route). + +#### disable_cache + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [DNS 规则动作](../rule_action#route). + +#### rewrite_ttl + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [DNS 规则动作](../rule_action#route). + +#### client_subnet + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [DNS 规则动作](../rule_action#route). + +### 旧版地址筛选字段 + +!!! failure "已在 sing-box 1.14.0 废弃" + + 旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除, + 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 + +!!! info "" + + 引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。 + +!!! note "" + + 启用 `experimental.cache_file.store_rdrc` 以缓存结果。 + +#### geoip + +!!! failure "已在 sing-box 1.12.0 中被移除" + + GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 + + +与查询响应匹配 GeoIP。 + +#### ip_cidr + +!!! question "自 sing-box 1.9.0 起" + +与查询响应匹配 IP CIDR。 + +作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +#### ip_is_private + +!!! question "自 sing-box 1.9.0 起" + +与查询响应匹配非公开 IP。 + +作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +#### rule_set_ip_cidr_accept_empty + +!!! question "自 sing-box 1.10.0 起" + +!!! failure "已在 sing-box 1.14.0 废弃" + + `rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除, + 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +使规则集中的 `ip_cidr` 规则接受空查询响应。 + +### 响应匹配字段 + +!!! question "自 sing-box 1.14.0 起" + +已评估的响应的匹配字段。需要将 `match_response` 设为 `true`, +且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。 + +该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。 + +#### response_rcode + +匹配 DNS 响应码。 + +接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。 + +#### response_answer + +匹配 DNS 应答记录。 + +记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。 + +#### response_ns + +匹配 DNS 名称服务器记录。 + +记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。 + +#### response_extra + +匹配 DNS 额外记录。 + +记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。 + +### 逻辑字段 + +#### type + +`logical` + +#### mode + +==必填== + +`and` 或 `or` + +#### rules + +==必填== + +包括的规则。 diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md new file mode 100644 index 00000000..e5a99be3 --- /dev/null +++ b/docs/configuration/dns/rule_action.md @@ -0,0 +1,231 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [strategy](#strategy) + :material-plus: [evaluate](#evaluate) + :material-plus: [respond](#respond) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [strategy](#strategy) + :material-plus: [predefined](#predefined) + +!!! question "Since sing-box 1.11.0" + +### route + +```json +{ + "action": "route", // default + "server": "", + "strategy": "", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`route` inherits the classic rule behavior of routing DNS requests to the specified server. + +#### server + +==Required== + +Tag of target server. + +#### strategy + +!!! question "Since sing-box 1.12.0" + +!!! failure "Deprecated in sing-box 1.14.0" + + `strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0. + +Set domain strategy for this query. + +One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. + +#### disable_cache + +Disable cache and save cache in this query. + +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + +#### rewrite_ttl + +Rewrite TTL in DNS responses. + +#### client_subnet + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Will override `dns.client_subnet`. + +### evaluate + +!!! question "Since sing-box 1.14.0" + +```json +{ + "action": "evaluate", + "server": "", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules +to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields. +Unlike `route`, it does **not** terminate rule evaluation. + +Only allowed on top-level DNS rules (not inside logical sub-rules). +Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields +require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action +does not satisfy this requirement, because matching happens before the action runs. + +#### server + +==Required== + +Tag of target server. + +#### disable_cache + +Disable cache and save cache in this query. + +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + +#### rewrite_ttl + +Rewrite TTL in DNS responses. + +#### client_subnet + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Will override `dns.client_subnet`. + +### respond + +!!! question "Since sing-box 1.14.0" + +```json +{ + "action": "respond" +} +``` + +`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action. + +This action does not send a new DNS query and has no extra options. + +Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules. + +### route-options + +```json +{ + "action": "route-options", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`route-options` set options for routing. + +### reject + +```json +{ + "action": "reject", + "method": "", + "no_drop": false +} +``` + +`reject` reject DNS requests. + +#### method + +- `default`: Reply with REFUSED. +- `drop`: Drop the request. + +`default` will be used by default. + +#### no_drop + +If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. + +Not available when `method` is set to drop. + +### predefined + +!!! question "Since sing-box 1.12.0" + +```json +{ + "action": "predefined", + "rcode": "", + "answer": [], + "ns": [], + "extra": [] +} +``` + +`predefined` responds with predefined DNS records. + +#### rcode + +The response code. + +| Value | Value in the legacy rcode server | Description | +|------------|----------------------------------|-----------------| +| `NOERROR` | `success` | Ok | +| `FORMERR` | `format_error` | Bad request | +| `SERVFAIL` | `server_failure` | Server failure | +| `NXDOMAIN` | `name_error` | Not found | +| `NOTIMP` | `not_implemented` | Not implemented | +| `REFUSED` | `refused` | Refused | + +`NOERROR` will be used by default. + +#### answer + +List of text DNS record to respond as answers. + +Examples: + +| Record Type | Example | +|-------------|-------------------------------| +| `A` | `localhost. IN A 127.0.0.1` | +| `AAAA` | `localhost. IN AAAA ::1` | +| `TXT` | `localhost. IN TXT \"Hello\"` | + +#### ns + +List of text DNS record to respond as name servers. + +#### extra + +List of text DNS record to respond as extra records. diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md new file mode 100644 index 00000000..24179977 --- /dev/null +++ b/docs/configuration/dns/rule_action.zh.md @@ -0,0 +1,229 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [strategy](#strategy) + :material-plus: [evaluate](#evaluate) + :material-plus: [respond](#respond) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [strategy](#strategy) + :material-plus: [predefined](#predefined) + +!!! question "自 sing-box 1.11.0 起" + +### route + +```json +{ + "action": "route", // 默认 + "server": "", + "strategy": "", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`route` 继承了将 DNS 请求 路由到指定服务器的经典规则动作。 + +#### server + +==必填== + +目标 DNS 服务器的标签。 + +#### strategy + +!!! question "自 sing-box 1.12.0 起" + +!!! failure "已在 sing-box 1.14.0 废弃" + + `strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。 + +为此查询设置域名策略。 + +可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 + +#### disable_cache + +在此查询中禁用缓存。 + +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + +#### rewrite_ttl + +重写 DNS 回应中的 TTL。 + +#### client_subnet + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +将覆盖 `dns.client_subnet`. + +### evaluate + +!!! question "自 sing-box 1.14.0 起" + +```json +{ + "action": "evaluate", + "server": "", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。 + +仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。 +使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则, +需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件, +因为匹配发生在动作执行之前。 + +#### server + +==必填== + +目标 DNS 服务器的标签。 + +#### disable_cache + +在此查询中禁用缓存。 + +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + +#### rewrite_ttl + +重写 DNS 回应中的 TTL。 + +#### client_subnet + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +将覆盖 `dns.client_subnet`. + +### respond + +!!! question "自 sing-box 1.14.0 起" + +```json +{ + "action": "respond" +} +``` + +`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。 + +此动作不会发起新的 DNS 查询,也没有额外选项。 + +只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。 + +### route-options + +```json +{ + "action": "route-options", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`route-options` 为路由设置选项。 + +### reject + +```json +{ + "action": "reject", + "method": "", + "no_drop": false +} +``` + +`reject` 拒绝 DNS 请求。 + +#### method + +- `default`: 返回 REFUSED。 +- `drop`: 丢弃请求。 + +默认使用 `default`。 + +#### no_drop + +如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 + +当 `method` 设为 `drop` 时不可用。 + +### predefined + +!!! question "自 sing-box 1.12.0 起" + +```json +{ + "action": "predefined", + "rcode": "", + "answer": [], + "ns": [], + "extra": [] +} +``` + +`predefined` 以预定义的 DNS 记录响应。 + +#### rcode + +响应码。 + +| 值 | 旧 rcode DNS 服务器中的值 | 描述 | +|------------|--------------------|-----------------| +| `NOERROR` | `success` | Ok | +| `FORMERR` | `format_error` | Bad request | +| `SERVFAIL` | `server_failure` | Server failure | +| `NXDOMAIN` | `name_error` | Not found | +| `NOTIMP` | `not_implemented` | Not implemented | +| `REFUSED` | `refused` | Refused | + +默认使用 `NOERROR`。 + +#### answer + +用于作为回答响应的文本 DNS 记录列表。 + +例子: + +| 记录类型 | 例子 | +|--------|-------------------------------| +| `A` | `localhost. IN A 127.0.0.1` | +| `AAAA` | `localhost. IN AAAA ::1` | +| `TXT` | `localhost. IN TXT \"Hello\"` | + +#### ns + +用于作为名称服务器响应的文本 DNS 记录列表。 + +#### extra + +用于作为额外记录响应的文本 DNS 记录列表。 diff --git a/docs/configuration/dns/server/dhcp.md b/docs/configuration/dns/server/dhcp.md new file mode 100644 index 00000000..b26da2a5 --- /dev/null +++ b/docs/configuration/dns/server/dhcp.md @@ -0,0 +1,38 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# DHCP + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "dhcp", + "tag": "", + + "interface": "", + + // Dial Fields + } + ] + } +} +``` + +### Fields + +#### interface + +Interface name to listen on. + +Tge default interface will be used by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/dhcp.zh.md b/docs/configuration/dns/server/dhcp.zh.md new file mode 100644 index 00000000..2a67a7a3 --- /dev/null +++ b/docs/configuration/dns/server/dhcp.zh.md @@ -0,0 +1,38 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# DHCP + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "dhcp", + "tag": "", + + "interface": "", + + // 拨号字段 + } + ] + } +} +``` + +### 字段 + +#### interface + +要监听的网络接口名称。 + +默认使用默认接口。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/fakeip.md b/docs/configuration/dns/server/fakeip.md new file mode 100644 index 00000000..6d2bba43 --- /dev/null +++ b/docs/configuration/dns/server/fakeip.md @@ -0,0 +1,35 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# Fake IP + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "fakeip", + "tag": "", + + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + ] + } +} +``` + +### Fields + +#### inet4_range + +IPv4 address range for FakeIP. + +#### inet6_address + +IPv6 address range for FakeIP. diff --git a/docs/configuration/dns/server/fakeip.zh.md b/docs/configuration/dns/server/fakeip.zh.md new file mode 100644 index 00000000..06dbdff0 --- /dev/null +++ b/docs/configuration/dns/server/fakeip.zh.md @@ -0,0 +1,35 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# Fake IP + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "fakeip", + "tag": "", + + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + ] + } +} +``` + +### 字段 + +#### inet4_range + +FakeIP 的 IPv4 地址范围。 + +#### inet6_range + +FakeIP 的 IPv6 地址范围。 \ No newline at end of file diff --git a/docs/configuration/dns/server/hosts.md b/docs/configuration/dns/server/hosts.md new file mode 100644 index 00000000..da76f619 --- /dev/null +++ b/docs/configuration/dns/server/hosts.md @@ -0,0 +1,127 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# Hosts + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "hosts", + "tag": "", + + "path": [], + "predefined": {} + } + ] + } +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### path + +List of paths to hosts files. + +`/etc/hosts` is used by default. + +`C:\Windows\System32\Drivers\etc\hosts` is used by default on Windows. + +Example: + +```json +{ + // "path": "/etc/hosts" + + "path": [ + "/etc/hosts", + "$HOME/.hosts" + ] +} +``` + +#### predefined + +Predefined hosts. + +Example: + +```json +{ + "predefined": { + "www.google.com": "127.0.0.1", + "localhost": [ + "127.0.0.1", + "::1" + ] + } +} +``` + +### Examples + +=== "Use hosts if available" + + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "hosts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] + } + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "hosts" + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/hosts.zh.md b/docs/configuration/dns/server/hosts.zh.md new file mode 100644 index 00000000..3384adc7 --- /dev/null +++ b/docs/configuration/dns/server/hosts.zh.md @@ -0,0 +1,127 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# Hosts + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "hosts", + "tag": "", + + "path": [], + "predefined": {} + } + ] + } +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 字段 + +#### path + +hosts 文件路径列表。 + +默认使用 `/etc/hosts`。 + +在 Windows 上默认使用 `C:\Windows\System32\Drivers\etc\hosts`。 + +示例: + +```json +{ + // "path": "/etc/hosts" + + "path": [ + "/etc/hosts", + "$HOME/.hosts" + ] +} +``` + +#### predefined + +预定义的 hosts。 + +示例: + +```json +{ + "predefined": { + "www.google.com": "127.0.0.1", + "localhost": [ + "127.0.0.1", + "::1" + ] + } +} +``` + +### 示例 + +=== "如果可用则使用 hosts" + + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "hosts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] + } + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "hosts" + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/http3.md b/docs/configuration/dns/server/http3.md new file mode 100644 index 00000000..dd81ba2d --- /dev/null +++ b/docs/configuration/dns/server/http3.md @@ -0,0 +1,71 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# DNS over HTTP3 (DoH3) + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "h3", + "tag": "", + + "server": "", + "server_port": 443, + + "path": "", + "headers": {}, + + "tls": {}, + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy H3 server" + + * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. + +### Fields + +#### server + +==Required== + +The address of the DNS server. + +If domain name is used, `domain_resolver` must also be set to resolve IP address. + +#### server_port + +The port of the DNS server. + +`443` will be used by default. + +#### path + +The path of the DNS server. + +`/dns-query` will be used by default. + +#### headers + +Additional headers to be sent to the DNS server. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/http3.zh.md b/docs/configuration/dns/server/http3.zh.md new file mode 100644 index 00000000..1032fedb --- /dev/null +++ b/docs/configuration/dns/server/http3.zh.md @@ -0,0 +1,71 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# DNS over HTTP3 (DoH3) + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "h3", + "tag": "", + + "server": "", + "server_port": 443, + + "path": "", + "headers": {}, + + "tls": {}, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版 H3 服务器的区别" + + * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 + +### 字段 + +#### server + +==必填== + +DNS 服务器的地址。 + +如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 + +#### server_port + +DNS 服务器的端口。 + +默认使用 `443`。 + +#### path + +DNS 服务器的路径。 + +默认使用 `/dns-query`。 + +#### headers + +发送到 DNS 服务器的额外标头。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/https.md b/docs/configuration/dns/server/https.md new file mode 100644 index 00000000..46e69a55 --- /dev/null +++ b/docs/configuration/dns/server/https.md @@ -0,0 +1,71 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# DNS over HTTPS (DoH) + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "https", + "tag": "", + + "server": "", + "server_port": 443, + + "path": "", + "headers": {}, + + "tls": {}, + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy HTTPS server" + + * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. + +### Fields + +#### server + +==Required== + +The address of the DNS server. + +If domain name is used, `domain_resolver` must also be set to resolve IP address. + +#### server_port + +The port of the DNS server. + +`443` will be used by default. + +#### path + +The path of the DNS server. + +`/dns-query` will be used by default. + +#### headers + +Additional headers to be sent to the DNS server. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/https.zh.md b/docs/configuration/dns/server/https.zh.md new file mode 100644 index 00000000..7aa73c3f --- /dev/null +++ b/docs/configuration/dns/server/https.zh.md @@ -0,0 +1,71 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# DNS over HTTPS (DoH) + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "https", + "tag": "", + + "server": "", + "server_port": 443, + + "path": "", + "headers": {}, + + "tls": {}, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版 HTTPS 服务器的区别" + + * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 + +### 字段 + +#### server + +==必填== + +DNS 服务器的地址。 + +如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 + +#### server_port + +DNS 服务器的端口。 + +默认使用 `443`。 + +#### path + +DNS 服务器的路径。 + +默认使用 `/dns-query`。 + +#### headers + +发送到 DNS 服务器的额外标头。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/index.md b/docs/configuration/dns/server/index.md new file mode 100644 index 00000000..b610cf5b --- /dev/null +++ b/docs/configuration/dns/server/index.md @@ -0,0 +1,48 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [type](#type) + +# DNS Server + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "", + "tag": "" + } + ] + } +} +``` + +#### type + +The type of the DNS server. + +| Type | Format | +|-----------------|---------------------------| +| empty (default) | :material-note-remove: [Legacy](./legacy/) | +| `local` | [Local](./local/) | +| `hosts` | [Hosts](./hosts/) | +| `tcp` | [TCP](./tcp/) | +| `udp` | [UDP](./udp/) | +| `tls` | [TLS](./tls/) | +| `quic` | [QUIC](./quic/) | +| `https` | [HTTPS](./https/) | +| `h3` | [HTTP/3](./http3/) | +| `dhcp` | [DHCP](./dhcp/) | +| `fakeip` | [Fake IP](./fakeip/) | +| `tailscale` | [Tailscale](./tailscale/) | +| `resolved` | [Resolved](./resolved/) | + +#### tag + +The tag of the DNS server. diff --git a/docs/configuration/dns/server/index.zh.md b/docs/configuration/dns/server/index.zh.md new file mode 100644 index 00000000..d1a4dc3c --- /dev/null +++ b/docs/configuration/dns/server/index.zh.md @@ -0,0 +1,48 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [type](#type) + +# DNS Server + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "", + "tag": "" + } + ] + } +} +``` + +#### type + +DNS 服务器的类型。 + +| 类型 | 格式 | +|-----------------|---------------------------| +| empty (default) | :material-note-remove: [Legacy](./legacy/) | +| `local` | [Local](./local/) | +| `hosts` | [Hosts](./hosts/) | +| `tcp` | [TCP](./tcp/) | +| `udp` | [UDP](./udp/) | +| `tls` | [TLS](./tls/) | +| `quic` | [QUIC](./quic/) | +| `https` | [HTTPS](./https/) | +| `h3` | [HTTP/3](./http3/) | +| `dhcp` | [DHCP](./dhcp/) | +| `fakeip` | [Fake IP](./fakeip/) | +| `tailscale` | [Tailscale](./tailscale/) | +| `resolved` | [Resolved](./resolved/) | + +#### tag + +DNS 服务器的标签。 diff --git a/docs/configuration/dns/server/legacy.md b/docs/configuration/dns/server/legacy.md new file mode 100644 index 00000000..e27b19cb --- /dev/null +++ b/docs/configuration/dns/server/legacy.md @@ -0,0 +1,113 @@ +--- +icon: material/note-remove +--- + +!!! failure "Removed in sing-box 1.14.0" + + Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats). + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [client_subnet](#client_subnet) + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "tag": "", + "address": "", + "address_resolver": "", + "address_strategy": "", + "strategy": "", + "detour": "", + "client_subnet": "" + } + ] + } +} +``` + +### Fields + +#### tag + +The tag of the dns server. + +#### address + +==Required== + +The address of the dns server. + +| Protocol | Format | +|--------------------------------------|-------------------------------| +| `System` | `local` | +| `TCP` | `tcp://1.0.0.1` | +| `UDP` | `8.8.8.8` `udp://8.8.4.4` | +| `TLS` | `tls://dns.google` | +| `HTTPS` | `https://1.1.1.1/dns-query` | +| `QUIC` | `quic://dns.adguard.com` | +| `HTTP3` | `h3://8.8.8.8/dns-query` | +| `RCode` | `rcode://refused` | +| `DHCP` | `dhcp://auto` or `dhcp://en0` | +| [FakeIP](/configuration/dns/fakeip/) | `fakeip` | + +!!! warning "" + + To ensure that Android system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time. + +!!! info "" + + the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option. + +| RCode | Description | +|-------------------|-----------------------| +| `success` | `No error` | +| `format_error` | `Format error` | +| `server_failure` | `Server failure` | +| `name_error` | `Non-existent domain` | +| `not_implemented` | `Not implemented` | +| `refused` | `Query refused` | + +#### address_resolver + +==Required if address contains domain== + +Tag of a another server to resolve the domain name in the address. + +#### address_strategy + +The domain strategy for resolving the domain name in the address. + +One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. + +`dns.strategy` will be used if empty. + +#### strategy + +Default domain strategy for resolving the domain names. + +One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. + +Take no effect if overridden by other settings. + +#### detour + +Tag of an outbound for connecting to the dns server. + +Default outbound will be used if empty. + +#### client_subnet + +!!! question "Since sing-box 1.9.0" + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Can be overridden by `rules.[].client_subnet`. + +Will override `dns.client_subnet`. diff --git a/docs/configuration/dns/server/legacy.zh.md b/docs/configuration/dns/server/legacy.zh.md new file mode 100644 index 00000000..2ad36839 --- /dev/null +++ b/docs/configuration/dns/server/legacy.zh.md @@ -0,0 +1,113 @@ +--- +icon: material/note-remove +--- + +!!! failure "已在 sing-box 1.14.0 移除" + + 旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [client_subnet](#client_subnet) + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "tag": "", + "address": "", + "address_resolver": "", + "address_strategy": "", + "strategy": "", + "detour": "", + "client_subnet": "" + } + ] + } +} +``` + +### 字段 + +#### tag + +DNS 服务器的标签。 + +#### address + +==必填== + +DNS 服务器的地址。 + +| 协议 | 格式 | +|--------------------------------------|------------------------------| +| `System` | `local` | +| `TCP` | `tcp://1.0.0.1` | +| `UDP` | `8.8.8.8` `udp://8.8.4.4` | +| `TLS` | `tls://dns.google` | +| `HTTPS` | `https://1.1.1.1/dns-query` | +| `QUIC` | `quic://dns.adguard.com` | +| `HTTP3` | `h3://8.8.8.8/dns-query` | +| `RCode` | `rcode://refused` | +| `DHCP` | `dhcp://auto` 或 `dhcp://en0` | +| [FakeIP](/zh/configuration/dns/fakeip/) | `fakeip` | + +!!! warning "" + + 为了确保 Android 系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。 + +!!! info "" + + RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。 + +| RCode | 描述 | +|-------------------|----------| +| `success` | `无错误` | +| `format_error` | `请求格式错误` | +| `server_failure` | `服务器出错` | +| `name_error` | `域名不存在` | +| `not_implemented` | `功能未实现` | +| `refused` | `请求被拒绝` | + +#### address_resolver + +==如果服务器地址包括域名则必须== + +用于解析本 DNS 服务器的域名的另一个 DNS 服务器的标签。 + +#### address_strategy + +用于解析本 DNS 服务器的域名的策略。 + +可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 + +默认使用 `dns.strategy`。 + +#### strategy + +默认解析策略。 + +可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 + +如果被其他设置覆盖则不生效。 + +#### detour + +用于连接到 DNS 服务器的出站的标签。 + +如果为空,将使用默认出站。 + +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +可以被 `rules.[].client_subnet` 覆盖。 + +将覆盖 `dns.client_subnet`。 diff --git a/docs/configuration/dns/server/local.md b/docs/configuration/dns/server/local.md new file mode 100644 index 00000000..aa7f095a --- /dev/null +++ b/docs/configuration/dns/server/local.md @@ -0,0 +1,61 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [prefer_go](#prefer_go) + +!!! question "Since sing-box 1.12.0" + +# Local + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "local", + "tag": "", + "prefer_go": false + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy local server" + + * The old legacy local server only handles IP requests; the new one handles all types of requests and supports concurrent for IP requests. + * The old local server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + +### Fields + +#### prefer_go + +!!! question "Since sing-box 1.13.0" + +When enabled, `local` DNS server will resolve DNS by dialing itself whenever possible. + +Specifically, it disables following behaviors which was added as features in sing-box 1.13.0: + +1. On Apple platforms: Attempt to resolve A/AAAA requests using `getaddrinfo` in NetworkExtension. +2. On Linux: Resolve through `systemd-resolvd`'s DBus interface when available. + +As a sole exception, it cannot disable the following behavior: + +1. In the Android graphical client, +`local` will always resolve DNS through the platform interface, +as there is no other way to obtain upstream DNS servers; +On devices running Android versions lower than 10, this interface can only resolve A/AAAA requests. + +2. On macOS, `local` will try DHCP first in Network Extension, since DHCP respects DIal Fields, +it will not be disabled by `prefer_go`. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/local.zh.md b/docs/configuration/dns/server/local.zh.md new file mode 100644 index 00000000..50ac05ac --- /dev/null +++ b/docs/configuration/dns/server/local.zh.md @@ -0,0 +1,61 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [prefer_go](#prefer_go) + +!!! question "自 sing-box 1.12.0 起" + +# Local + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "local", + "tag": "", + "prefer_go": false, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版本地服务器的区别" + + * 旧的传统本地服务器只处理 IP 请求;新的服务器处理所有类型的请求,并支持 IP 请求的并发处理。 + * 旧的本地服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + +### 字段 + +#### prefer_go + +!!! question "自 sing-box 1.13.0 起" + +启用后,`local` DNS 服务器将尽可能通过拨号自身来解析 DNS。 + +具体来说,它禁用了在 sing-box 1.13.0 中作为功能添加的以下行为: + +1. 在 Apple 平台上:尝试在 NetworkExtension 中使用 `getaddrinfo` 解析 A/AAAA 请求。 +2. 在 Linux 上:当可用时通过 `systemd-resolvd` 的 DBus 接口进行解析。 + +作为唯一的例外,它无法禁用以下行为: + +1. 在 Android 图形客户端中, +`local` 将始终通过平台接口解析 DNS, +因为没有其他方法来获取上游 DNS 服务器; +在运行 Android 10 以下版本的设备上,此接口只能解析 A/AAAA 请求。 + +2. 在 macOS 上,`local` 会在 Network Extension 中首先尝试 DHCP,由于 DHCP 遵循拨号字段, +它不会被 `prefer_go` 禁用。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/quic.md b/docs/configuration/dns/server/quic.md new file mode 100644 index 00000000..096264fe --- /dev/null +++ b/docs/configuration/dns/server/quic.md @@ -0,0 +1,58 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# DNS over QUIC (DoQ) + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "quic", + "tag": "", + + "server": "", + "server_port": 853, + + "tls": {}, + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy QUIC server" + + * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. + +### Fields + +#### server + +==Required== + +The address of the DNS server. + +If domain name is used, `domain_resolver` must also be set to resolve IP address. + +#### server_port + +The port of the DNS server. + +`853` will be used by default. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/quic.zh.md b/docs/configuration/dns/server/quic.zh.md new file mode 100644 index 00000000..c18c18ed --- /dev/null +++ b/docs/configuration/dns/server/quic.zh.md @@ -0,0 +1,58 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# DNS over QUIC (DoQ) + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "quic", + "tag": "", + + "server": "", + "server_port": 853, + + "tls": {}, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版 QUIC 服务器的区别" + + * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 + +### 字段 + +#### server + +==必填== + +DNS 服务器的地址。 + +如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 + +#### server_port + +DNS 服务器的端口。 + +默认使用 `853`。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/resolved.md b/docs/configuration/dns/server/resolved.md new file mode 100644 index 00000000..75835c6b --- /dev/null +++ b/docs/configuration/dns/server/resolved.md @@ -0,0 +1,117 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# Resolved + +```json +{ + "dns": { + "servers": [ + { + "type": "resolved", + "tag": "", + + "service": "resolved", + "accept_default_resolvers": false + } + ] + } +} +``` + + +### Fields + +#### service + +==Required== + +The tag of the [Resolved Service](/configuration/service/resolved). + +#### accept_default_resolvers + +Indicates whether the default DNS resolvers should be accepted for fallback queries in addition to matching domains. + +Specifically, default DNS resolvers are DNS servers that have `SetLinkDefaultRoute` or `SetLinkDomains ~.` set. + +If not enabled, `NXDOMAIN` will be returned for requests that do not match search or match domains. + +### Examples + +=== "Split DNS only" + + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "resolved" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] + } + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "resolved" + } + ] + } + } + ``` + +=== "Use as global DNS" + + ```json + { + "dns": { + "servers": [ + { + "type": "resolved", + "service": "resolved", + "accept_default_resolvers": true + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/resolved.zh.md b/docs/configuration/dns/server/resolved.zh.md new file mode 100644 index 00000000..8747e831 --- /dev/null +++ b/docs/configuration/dns/server/resolved.zh.md @@ -0,0 +1,116 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# Resolved + +```json +{ + "dns": { + "servers": [ + { + "type": "resolved", + "tag": "", + + "service": "resolved", + "accept_default_resolvers": false + } + ] + } +} +``` + +### 字段 + +#### service + +==必填== + +[Resolved 服务](/zh/configuration/service/resolved) 的标签。 + +#### accept_default_resolvers + +指示是否除了匹配域名外,还应接受默认 DNS 解析器以进行回退查询。 + +具体来说,默认 DNS 解析器是设置了 `SetLinkDefaultRoute` 或 `SetLinkDomains ~.` 的 DNS 服务器。 + +如果未启用,对于不匹配搜索域或匹配域的请求,将返回 `NXDOMAIN`。 + +### 示例 + +=== "仅分割 DNS" + + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "resolved" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] + } + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "resolved" + } + ] + } + } + ``` + +=== "用作全局 DNS" + + ```json + { + "dns": { + "servers": [ + { + "type": "resolved", + "service": "resolved", + "accept_default_resolvers": true + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/tailscale.md b/docs/configuration/dns/server/tailscale.md new file mode 100644 index 00000000..2677f2b8 --- /dev/null +++ b/docs/configuration/dns/server/tailscale.md @@ -0,0 +1,116 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# Tailscale + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "tailscale", + "tag": "", + + "endpoint": "ts-ep", + "accept_default_resolvers": false + } + ] + } +} +``` + +### Fields + +#### endpoint + +==Required== + +The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale). + +#### accept_default_resolvers + +Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。 + +if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries. + +### Examples + +=== "MagicDNS only" + + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "ts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] + } + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "ts" + } + ] + } + } + ``` + +=== "Use as global DNS" + + ```json + { + "dns": { + "servers": [ + { + "type": "tailscale", + "endpoint": "ts-ep", + "accept_default_resolvers": true + } + ] + } + } + ``` diff --git a/docs/configuration/dns/server/tailscale.zh.md b/docs/configuration/dns/server/tailscale.zh.md new file mode 100644 index 00000000..10d84038 --- /dev/null +++ b/docs/configuration/dns/server/tailscale.zh.md @@ -0,0 +1,116 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# Tailscale + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "tailscale", + "tag": "", + + "endpoint": "ts-ep", + "accept_default_resolvers": false + } + ] + } +} +``` + +### 字段 + +#### endpoint + +==必填== + +[Tailscale 端点](/zh/configuration/endpoint/tailscale) 的标签。 + +#### accept_default_resolvers + +指示是否除了 MagicDNS 外,还应接受默认 DNS 解析器以进行回退查询。 + +如果未启用,对于非 Tailscale 域名查询将返回 `NXDOMAIN`。 + +### 示例 + +=== "仅 MagicDNS" + + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "ts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] + } + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "ts" + } + ] + } + } + ``` + +=== "用作全局 DNS" + + ```json + { + "dns": { + "servers": [ + { + "type": "tailscale", + "endpoint": "ts-ep", + "accept_default_resolvers": true + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/tcp.md b/docs/configuration/dns/server/tcp.md new file mode 100644 index 00000000..c0a4ceaf --- /dev/null +++ b/docs/configuration/dns/server/tcp.md @@ -0,0 +1,52 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# TCP + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "tcp", + "tag": "", + + "server": "", + "server_port": 53, + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy TCP server" + + * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. + +### Fields + +#### server + +==Required== + +The address of the DNS server. + +If domain name is used, `domain_resolver` must also be set to resolve IP address. + +#### server_port + +The port of the DNS server. + +`53` will be used by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/tcp.zh.md b/docs/configuration/dns/server/tcp.zh.md new file mode 100644 index 00000000..6f439bdf --- /dev/null +++ b/docs/configuration/dns/server/tcp.zh.md @@ -0,0 +1,52 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# TCP + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "tcp", + "tag": "", + + "server": "", + "server_port": 53, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版 TCP 服务器的区别" + + * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 + +### 字段 + +#### server + +==必填== + +DNS 服务器的地址。 + +如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 + +#### server_port + +DNS 服务器的端口。 + +默认使用 `53`。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/tls.md b/docs/configuration/dns/server/tls.md new file mode 100644 index 00000000..0e3be226 --- /dev/null +++ b/docs/configuration/dns/server/tls.md @@ -0,0 +1,58 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# DNS over TLS (DoT) + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "tls", + "tag": "", + + "server": "", + "server_port": 853, + + "tls": {}, + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy TLS server" + + * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. + +### Fields + +#### server + +==Required== + +The address of the DNS server. + +If domain name is used, `domain_resolver` must also be set to resolve IP address. + +#### server_port + +The port of the DNS server. + +`853` will be used by default. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/tls.zh.md b/docs/configuration/dns/server/tls.zh.md new file mode 100644 index 00000000..afd1111a --- /dev/null +++ b/docs/configuration/dns/server/tls.zh.md @@ -0,0 +1,58 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# DNS over TLS (DoT) + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "tls", + "tag": "", + + "server": "", + "server_port": 853, + + "tls": {}, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版 TLS 服务器的区别" + + * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 + +### 字段 + +#### server + +==必填== + +DNS 服务器的地址。 + +如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 + +#### server_port + +DNS 服务器的端口。 + +默认使用 `853`。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/dns/server/udp.md b/docs/configuration/dns/server/udp.md new file mode 100644 index 00000000..c551d3c9 --- /dev/null +++ b/docs/configuration/dns/server/udp.md @@ -0,0 +1,52 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# UDP + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "udp", + "tag": "", + + "server": "", + "server_port": 53, + + // Dial Fields + } + ] + } +} +``` + +!!! info "Difference from legacy UDP server" + + * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. + * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. + +### Fields + +#### server + +==Required== + +The address of the DNS server. + +If domain name is used, `domain_resolver` must also be set to resolve IP address. + +#### server_port + +The port of the DNS server. + +`53` will be used by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/udp.zh.md b/docs/configuration/dns/server/udp.zh.md new file mode 100644 index 00000000..63feedd8 --- /dev/null +++ b/docs/configuration/dns/server/udp.zh.md @@ -0,0 +1,52 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# UDP + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "udp", + "tag": "", + + "server": "", + "server_port": 53, + + // 拨号字段 + } + ] + } +} +``` + +!!! info "与旧版 UDP 服务器的区别" + + * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 + * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 + +### 字段 + +#### server + +==必填== + +DNS 服务器的地址。 + +如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 + +#### server_port + +DNS 服务器的端口。 + +默认使用 `53`。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/endpoint/index.md b/docs/configuration/endpoint/index.md new file mode 100644 index 00000000..b409a783 --- /dev/null +++ b/docs/configuration/endpoint/index.md @@ -0,0 +1,29 @@ +!!! question "Since sing-box 1.11.0" + +# Endpoint + +An endpoint is a protocol with inbound and outbound behavior. + +### Structure + +```json +{ + "endpoints": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | +|-------------|---------------------------| +| `wireguard` | [WireGuard](./wireguard/) | +| `tailscale` | [Tailscale](./tailscale/) | + +#### tag + +The tag of the endpoint. diff --git a/docs/configuration/endpoint/index.zh.md b/docs/configuration/endpoint/index.zh.md new file mode 100644 index 00000000..f7e71b75 --- /dev/null +++ b/docs/configuration/endpoint/index.zh.md @@ -0,0 +1,29 @@ +!!! question "自 sing-box 1.11.0 起" + +# 端点 + +端点是具有入站和出站行为的协议。 + +### 结构 + +```json +{ + "endpoints": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | +|-------------|---------------------------| +| `wireguard` | [WireGuard](./wireguard/) | +| `tailscale` | [Tailscale](./tailscale/) | + +#### tag + +端点的标签。 diff --git a/docs/configuration/endpoint/tailscale.md b/docs/configuration/endpoint/tailscale.md new file mode 100644 index 00000000..6cf10e2b --- /dev/null +++ b/docs/configuration/endpoint/tailscale.md @@ -0,0 +1,157 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [relay_server_port](#relay_server_port) + :material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints) + :material-plus: [system_interface](#system_interface) + :material-plus: [system_interface_name](#system_interface_name) + :material-plus: [system_interface_mtu](#system_interface_mtu) + :material-plus: [advertise_tags](#advertise_tags) + +!!! question "Since sing-box 1.12.0" + +### Structure + +```json +{ + "type": "tailscale", + "tag": "ts-ep", + "state_directory": "", + "auth_key": "", + "control_url": "", + "ephemeral": false, + "hostname": "", + "accept_routes": false, + "exit_node": "", + "exit_node_allow_lan_access": false, + "advertise_routes": [], + "advertise_exit_node": false, + "advertise_tags": [], + "relay_server_port": 0, + "relay_server_static_endpoints": [], + "system_interface": false, + "system_interface_name": "", + "system_interface_mtu": 0, + "udp_timeout": "5m", + + ... // Dial Fields +} +``` + +### Fields + +#### state_directory + +The directory where the Tailscale state is stored. + +`tailscale` is used by default. + +Example: `$HOME/.tailscale` + +#### auth_key + +!!! note + + Auth key is not required. By default, sing-box will log the login URL (or popup a notification on graphical clients). + +The auth key to create the node. If the node is already created (from state previously stored), then this field is not +used. + +#### control_url + +The coordination server URL. + +`https://controlplane.tailscale.com` is used by default. + +#### ephemeral + +Indicates whether the instance should register as an Ephemeral node (https://tailscale.com/s/ephemeral-nodes). + +#### hostname + +The hostname of the node. + +System hostname is used by default. + +Example: `localhost` + +#### accept_routes + +Indicates whether the node should accept routes advertised by other nodes. + +#### exit_node + +The exit node name or IP address to use. + +#### exit_node_allow_lan_access + +!!! note + + When the exit node does not have a corresponding advertised route, private traffics cannot be routed to the exit node even if `exit_node_allow_lan_access is` set. + +Indicates whether locally accessible subnets should be routed directly or via the exit node. + +#### advertise_routes + +CIDR prefixes to advertise into the Tailscale network as reachable through the current node. + +Example: `["192.168.1.1/24"]` + +#### advertise_exit_node + +Indicates whether the node should advertise itself as an exit node. + +#### advertise_tags + +!!! question "Since sing-box 1.13.0" + +Tags to advertise for this node, for ACL enforcement purposes. + +Example: `["tag:server"]` + +#### relay_server_port + +!!! question "Since sing-box 1.13.0" + +The port to listen on for incoming relay connections from other Tailscale nodes. + +#### relay_server_static_endpoints + +!!! question "Since sing-box 1.13.0" + +Static endpoints to advertise for the relay server. + +#### system_interface + +!!! question "Since sing-box 1.13.0" + +Create a system TUN interface for Tailscale. + +#### system_interface_name + +!!! question "Since sing-box 1.13.0" + +Custom TUN interface name. By default, `tailscale` (or `utun` on macOS) will be used. + +#### system_interface_mtu + +!!! question "Since sing-box 1.13.0" + +Override the TUN MTU. By default, Tailscale's own MTU is used. + +#### udp_timeout + +UDP NAT expiration time. + +`5m` will be used by default. + +### Dial Fields + +!!! note + + Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections. + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/endpoint/tailscale.zh.md b/docs/configuration/endpoint/tailscale.zh.md new file mode 100644 index 00000000..f881dd67 --- /dev/null +++ b/docs/configuration/endpoint/tailscale.zh.md @@ -0,0 +1,156 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [relay_server_port](#relay_server_port) + :material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints) + :material-plus: [system_interface](#system_interface) + :material-plus: [system_interface_name](#system_interface_name) + :material-plus: [system_interface_mtu](#system_interface_mtu) + :material-plus: [advertise_tags](#advertise_tags) + +!!! question "自 sing-box 1.12.0 起" + +### 结构 + +```json +{ + "type": "tailscale", + "tag": "ts-ep", + "state_directory": "", + "auth_key": "", + "control_url": "", + "ephemeral": false, + "hostname": "", + "accept_routes": false, + "exit_node": "", + "exit_node_allow_lan_access": false, + "advertise_routes": [], + "advertise_exit_node": false, + "advertise_tags": [], + "relay_server_port": 0, + "relay_server_static_endpoints": [], + "system_interface": false, + "system_interface_name": "", + "system_interface_mtu": 0, + "udp_timeout": "5m", + + ... // 拨号字段 +} +``` + +### 字段 + +#### state_directory + +存储 Tailscale 状态的目录。 + +默认使用 `tailscale`。 + +示例:`$HOME/.tailscale` + +#### auth_key + +!!! note + + 认证密钥不是必需的。默认情况下,sing-box 将记录登录 URL(或在图形客户端上弹出通知)。 + +用于创建节点的认证密钥。如果节点已经创建(从之前存储的状态),则不使用此字段。 + +#### control_url + +协调服务器 URL。 + +默认使用 `https://controlplane.tailscale.com`。 + +#### ephemeral + +指示实例是否应注册为临时节点 (https://tailscale.com/s/ephemeral-nodes)。 + +#### hostname + +节点的主机名。 + +默认使用系统主机名。 + +示例:`localhost` + +#### accept_routes + +指示节点是否应接受其他节点通告的路由。 + +#### exit_node + +要使用的出口节点名称或 IP 地址。 + +#### exit_node_allow_lan_access + +!!! note + + 当出口节点没有相应的通告路由时,即使设置了 `exit_node_allow_lan_access`,私有流量也无法路由到出口节点。 + +指示本地可访问的子网应该直接路由还是通过出口节点路由。 + +#### advertise_routes + +通告到 Tailscale 网络的 CIDR 前缀,作为可通过当前节点访问的路由。 + +示例:`["192.168.1.1/24"]` + +#### advertise_exit_node + +指示节点是否应将自己通告为出口节点。 + +#### advertise_tags + +!!! question "自 sing-box 1.13.0 起" + +为此节点通告的标签,用于 ACL 执行。 + +示例:`["tag:server"]` + +#### relay_server_port + +!!! question "自 sing-box 1.13.0 起" + +监听来自其他 Tailscale 节点的中继连接的端口。 + +#### relay_server_static_endpoints + +!!! question "自 sing-box 1.13.0 起" + +为中继服务器通告的静态端点。 + +#### system_interface + +!!! question "自 sing-box 1.13.0 起" + +为 Tailscale 创建系统 TUN 接口。 + +#### system_interface_name + +!!! question "自 sing-box 1.13.0 起" + +自定义 TUN 接口名。默认使用 `tailscale`(macOS 上为 `utun`)。 + +#### system_interface_mtu + +!!! question "自 sing-box 1.13.0 起" + +覆盖 TUN 的 MTU。默认使用 Tailscale 自己的 MTU。 + +#### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +### 拨号字段 + +!!! note + + Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 diff --git a/docs/configuration/endpoint/wireguard.md b/docs/configuration/endpoint/wireguard.md new file mode 100644 index 00000000..dc3b8228 --- /dev/null +++ b/docs/configuration/endpoint/wireguard.md @@ -0,0 +1,129 @@ +!!! question "Since sing-box 1.11.0" + +### Structure + +```json +{ + "type": "wireguard", + "tag": "wg-ep", + + "system": false, + "name": "", + "mtu": 1408, + "address": [], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [], + "persistent_keepalive_interval": 0, + "reserved": [0, 0, 0] + } + ], + "udp_timeout": "", + "workers": 0, + + ... // Dial Fields +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### system + +Use system interface. + +Requires privilege and cannot conflict with exists system interfaces. + +#### name + +Custom interface name for system interface. + +#### mtu + +WireGuard MTU. + +`1408` will be used by default. + +#### address + +==Required== + +List of IP (v4 or v6) address prefixes to be assigned to the interface. + +#### private_key + +==Required== + +WireGuard requires base64-encoded public and private keys. These can be generated using the wg(8) utility: + +```shell +wg genkey +echo "private key" || wg pubkey +``` + +or `sing-box generate wg-keypair`. + +#### peers + +==Required== + +List of WireGuard peers. + +#### peers.address + +WireGuard peer address. + +#### peers.port + +WireGuard peer port. + +#### peers.public_key + +==Required== + +WireGuard peer public key. + +#### peers.pre_shared_key + +WireGuard peer pre-shared key. + +#### peers.allowed_ips + +==Required== + +WireGuard allowed IPs. + +#### peers.persistent_keepalive_interval + +WireGuard persistent keepalive interval, in seconds. + +Disabled by default. + +#### peers.reserved + +WireGuard reserved field bytes. + +#### udp_timeout + +UDP NAT expiration time. + +`5m` will be used by default. + +#### workers + +WireGuard worker count. + +CPU count is used by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/endpoint/wireguard.zh.md b/docs/configuration/endpoint/wireguard.zh.md new file mode 100644 index 00000000..1935135f --- /dev/null +++ b/docs/configuration/endpoint/wireguard.zh.md @@ -0,0 +1,131 @@ +!!! question "自 sing-box 1.11.0 起" + +### 结构 + +```json +{ + "type": "wireguard", + "tag": "wg-ep", + + "system": false, + "name": "", + "mtu": 1408, + "address": [], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [], + "persistent_keepalive_interval": 0, + "reserved": [0, 0, 0] + } + ], + "udp_timeout": "", + "workers": 0, + + ... // 拨号字段 +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 字段 + +#### system + +使用系统设备。 + +需要特权且不能与已有系统接口冲突。 + +#### name + +为系统接口自定义设备名称。 + +#### mtu + +WireGuard MTU。 + +默认使用 1408。 + +#### address + +==必填== + +接口的 IPv4/IPv6 地址或地址段的列表。 + +要分配给接口的 IP(v4 或 v6)地址段列表。 + +#### private_key + +==必填== + +WireGuard 需要 base64 编码的公钥和私钥。 这些可以使用 wg(8) 实用程序生成: + +```shell +wg genkey +echo "private key" || wg pubkey +``` + +或 `sing-box generate wg-keypair`. + +#### peers + +==必填== + +WireGuard 对等方的列表。 + +#### peers.address + +对等方的 IP 地址。 + +#### peers.port + +对等方的 WireGuard 端口。 + +#### peers.public_key + +==必填== + +对等方的 WireGuard 公钥。 + +#### peers.pre_shared_key + +对等方的预共享密钥。 + +#### peers.allowed_ips + +==必填== + +对等方的允许 IP 地址。 + +#### peers.persistent_keepalive_interval + +对等方的持久性保持活动间隔,以秒为单位。 + +默认禁用。 + +#### peers.reserved + +对等方的保留字段字节。 + +#### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +#### workers + +WireGuard worker 数量。 + +默认使用 CPU 数量。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 00000000..b93aa190 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,70 @@ +!!! question "Since sing-box 1.8.0" + +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [store_rdrc](#store_rdrc) + :material-plus: [store_dns](#store_dns) + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [store_rdrc](#store_rdrc) + :material-plus: [rdrc_timeout](#rdrc_timeout) + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false, + "store_rdrc": false, + "rdrc_timeout": "", + "store_dns": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in the cache file + +If not empty, configuration specified data will use a separate store keyed by it. + +#### store_fakeip + +Store fakeip in the cache file + +#### store_rdrc + +!!! failure "Deprecated in sing-box 1.14.0" + + `store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc). + +Store rejected DNS response cache in the cache file + +The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) +will be cached until expiration. + +#### rdrc_timeout + +Timeout of rejected DNS response cache. + +`7d` is used by default. + +#### store_dns + +!!! question "Since sing-box 1.14.0" + +Store DNS cache in the cache file. diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md new file mode 100644 index 00000000..5382f3a1 --- /dev/null +++ b/docs/configuration/experimental/cache-file.zh.md @@ -0,0 +1,67 @@ +!!! question "自 sing-box 1.8.0 起" + +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [store_rdrc](#store_rdrc) + :material-plus: [store_dns](#store_dns) + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [store_rdrc](#store_rdrc) + :material-plus: [rdrc_timeout](#rdrc_timeout) + +### 结构 + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false, + "store_rdrc": false, + "rdrc_timeout": "", + "store_dns": false +} +``` + +### 字段 + +#### enabled + +启用缓存文件。 + +#### path + +缓存文件路径,默认使用`cache.db`。 + +#### cache_id + +缓存文件中的标识符。 + +如果不为空,配置特定的数据将使用由其键控的单独存储。 + +#### store_fakeip + +将 fakeip 存储在缓存文件中。 + +#### store_rdrc + +!!! failure "已在 sing-box 1.14.0 废弃" + + `store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。 + +将拒绝的 DNS 响应缓存存储在缓存文件中。 + +[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。 + +#### rdrc_timeout + +拒绝的 DNS 响应缓存超时。 + +默认使用 `7d`。 + +#### store_dns + +!!! question "自 sing-box 1.14.0 起" + +将 DNS 缓存存储在缓存文件中。 diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 00000000..a8908940 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,166 @@ +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [access_control_allow_origin](#access_control_allow_origin) + :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + +### Structure + +=== "Structure" + + ```json + { + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" + } + ``` + +=== "Example (online)" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "external_controller": "127.0.0.1:9090", + "access_control_allow_origin": [ + "http://127.0.0.1", + "http://yacd.haishan.me" + ], + "access_control_allow_private_network": true + } + ``` + +=== "Example (download)" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard" + // "external_ui_download_detour": "direct" + } + ``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### access_control_allow_origin + +!!! question "Since sing-box 1.10.0" + +CORS allowed origins, `*` will be used if empty. + +To access the Clash API on a private network from a public website, you must explicitly specify it in `access_control_allow_origin` instead of using `*`. + +#### access_control_allow_private_network + +!!! question "Since sing-box 1.10.0" + +Allow access from private network. + +To access the Clash API on a private network from a public website, `access_control_allow_private_network` must be enabled. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.zh.md b/docs/configuration/experimental/clash-api.zh.md new file mode 100644 index 00000000..f86b2cac --- /dev/null +++ b/docs/configuration/experimental/clash-api.zh.md @@ -0,0 +1,164 @@ +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [access_control_allow_origin](#access_control_allow_origin) + :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) + +!!! quote "sing-box 1.8.0 中的更改" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + +### 结构 + +=== "结构" + + ```json + { + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" + } + ``` + +=== "示例 (在线)" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "external_controller": "127.0.0.1:9090", + "access_control_allow_origin": [ + "http://127.0.0.1", + "http://yacd.haishan.me" + ], + "access_control_allow_private_network": true + } + ``` + +=== "示例 (下载)" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard" + // "external_ui_download_detour": "direct" + } + ``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### Fields + +#### external_controller + +RESTful web API 监听地址。如果为空,则禁用 Clash API。 + +#### external_ui + +到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 + +#### external_ui_download_url + +静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 + +默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 + +#### external_ui_download_detour + +用于下载静态网页资源的出站的标签。 + +如果为空,将使用默认出站。 + +#### secret + +RESTful API 的密钥(可选) +通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 +如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 + +#### default_mode + +Clash 中的默认模式,默认使用 `Rule`。 + +此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 + +#### access_control_allow_origin + +!!! question "自 sing-box 1.10.0 起" + +允许的 CORS 来源,默认使用 `*`。 + +要从公共网站访问私有网络上的 Clash API,必须在 `access_control_allow_origin` 中明确指定它而不是使用 `*`。 + +#### access_control_allow_private_network + +!!! question "自 sing-box 1.10.0 起" + +允许从私有网络访问。 + +要从公共网站访问私有网络上的 Clash API,必须启用 `access_control_allow_private_network`。 + +#### store_mode + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_mode` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 + +将 Clash 模式存储在缓存文件中。 + +#### store_selected + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_selected` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 + +!!! note "" + + 必须为目标出站设置标签。 + +将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 + +#### store_fakeip + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_selected` 已在 Clash API 中废弃,且已迁移到 `cache_file.store_fakeip`。 + +将 fakeip 存储在缓存文件中。 + +#### cache_file + +!!! failure "已在 sing-box 1.8.0 废弃" + + `cache_file` 已在 Clash API 中废弃,且已迁移到 `cache_file.enabled` 和 `cache_file.path`。 + +缓存文件路径,默认使用`cache.db`。 + +#### cache_id + +!!! failure "已在 sing-box 1.8.0 废弃" + + `cache_id` 已在 Clash API 中废弃,且已迁移到 `cache_file.cache_id`。 + +缓存 ID。 + +如果不为空,配置特定的数据将使用由其键控的单独存储。 diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md new file mode 100644 index 00000000..a1a515cf --- /dev/null +++ b/docs/configuration/experimental/index.md @@ -0,0 +1,26 @@ +# Experimental + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + +### Structure + +```json +{ + "experimental": { + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} + } +} +``` + +### Fields + +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file/) | +| `clash_api` | [Clash API](./clash-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md new file mode 100644 index 00000000..01246c44 --- /dev/null +++ b/docs/configuration/experimental/index.zh.md @@ -0,0 +1,26 @@ +# 实验性 + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + +### 结构 + +```json +{ + "experimental": { + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} + } +} +``` + +### 字段 + +| 键 | 格式 | +|--------------|--------------------------| +| `cache_file` | [缓存文件](./cache-file/) | +| `clash_api` | [Clash API](./clash-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 00000000..4e23dea9 --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +!!! quote "" + + V2Ray API is not included by default, see [Installation](/installation/build-from-source/#build-tags). + +### Structure + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.zh.md b/docs/configuration/experimental/v2ray-api.zh.md new file mode 100644 index 00000000..87d5c95d --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.zh.md @@ -0,0 +1,50 @@ +!!! quote "" + + 默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#构建标记)。 + +### 结构 + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### 字段 + +#### listen + +gRPC API 监听地址。如果为空,则禁用 V2Ray API。 + +#### stats + +流量统计服务设置。 + +#### stats.enabled + +启用统计服务。 + +#### stats.inbounds + +统计流量的入站列表。 + +#### stats.outbounds + +统计流量的出站列表。 + +#### stats.users + +统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/inbound/anytls.md b/docs/configuration/inbound/anytls.md new file mode 100644 index 00000000..f3780119 --- /dev/null +++ b/docs/configuration/inbound/anytls.md @@ -0,0 +1,61 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +### Structure + +```json +{ + "type": "anytls", + "tag": "anytls-in", + + ... // Listen Fields + + "users": [ + { + "name": "sekai", + "password": "8JCsPssfgS8tiRwiMlhARg==" + } + ], + "padding_scheme": [], + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +==Required== + +AnyTLS users. + +#### padding_scheme + +AnyTLS padding scheme line array. + +Default padding scheme: + +```json +[ + "stop=8", + "0=30-30", + "1=100-400", + "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", + "7=500-1000" +] +``` + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). diff --git a/docs/configuration/inbound/anytls.zh.md b/docs/configuration/inbound/anytls.zh.md new file mode 100644 index 00000000..8c3d1daf --- /dev/null +++ b/docs/configuration/inbound/anytls.zh.md @@ -0,0 +1,61 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +### 结构 + +```json +{ + "type": "anytls", + "tag": "anytls-in", + + ... // 监听字段 + + "users": [ + { + "name": "sekai", + "password": "8JCsPssfgS8tiRwiMlhARg==" + } + ], + "padding_scheme": [], + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +==必填== + +AnyTLS 用户。 + +#### padding_scheme + +AnyTLS 填充方案行数组。 + +默认填充方案: + +```json +[ + "stop=8", + "0=30-30", + "1=100-400", + "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", + "7=500-1000" +] +``` + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 diff --git a/docs/configuration/inbound/cloudflared.md b/docs/configuration/inbound/cloudflared.md new file mode 100644 index 00000000..e91d73e0 --- /dev/null +++ b/docs/configuration/inbound/cloudflared.md @@ -0,0 +1,89 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +`cloudflared` inbound runs an embedded Cloudflare Tunnel client and routes all +incoming tunnel traffic (TCP, UDP, ICMP) through sing-box's routing engine. + +### Structure + +```json +{ + "type": "cloudflared", + "tag": "", + + "token": "", + "ha_connections": 0, + "protocol": "", + "post_quantum": false, + "edge_ip_version": 0, + "datagram_version": "", + "grace_period": "", + "region": "", + "control_dialer": { + ... // Dial Fields + }, + "tunnel_dialer": { + ... // Dial Fields + } +} +``` + +### Fields + +#### token + +==Required== + +Base64-encoded tunnel token from the Cloudflare Zero Trust dashboard +(`Networks → Tunnels → Install connector`). + +#### ha_connections + +Number of high-availability connections to the Cloudflare edge. + +Capped by the number of discovered edge addresses. + +#### protocol + +Transport protocol for edge connections. + +One of `quic` `http2`. + +#### post_quantum + +Enable post-quantum key exchange on the control connection. + +#### edge_ip_version + +IP version used when connecting to the Cloudflare edge. + +One of `0` (automatic) `4` `6`. + +#### datagram_version + +Datagram protocol version used for UDP proxying over QUIC. + +One of `v2` `v3`. Only meaningful when `protocol` is `quic`. + +#### grace_period + +Graceful shutdown window for in-flight edge connections. + +#### region + +Cloudflare edge region selector. + +Conflict with endpoints embedded in `token`. + +#### control_dialer + +[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the +Cloudflare control plane. + +#### tunnel_dialer + +[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the +Cloudflare edge data plane. diff --git a/docs/configuration/inbound/cloudflared.zh.md b/docs/configuration/inbound/cloudflared.zh.md new file mode 100644 index 00000000..65aa7dcf --- /dev/null +++ b/docs/configuration/inbound/cloudflared.zh.md @@ -0,0 +1,89 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +`cloudflared` 入站运行一个内嵌的 Cloudflare Tunnel 客户端,并将所有传入的隧道流量 +(TCP、UDP、ICMP)通过 sing-box 的路由引擎转发。 + +### 结构 + +```json +{ + "type": "cloudflared", + "tag": "", + + "token": "", + "ha_connections": 0, + "protocol": "", + "post_quantum": false, + "edge_ip_version": 0, + "datagram_version": "", + "grace_period": "", + "region": "", + "control_dialer": { + ... // 拨号字段 + }, + "tunnel_dialer": { + ... // 拨号字段 + } +} +``` + +### 字段 + +#### token + +==必填== + +来自 Cloudflare Zero Trust 仪表板的 Base64 编码隧道令牌 +(`Networks → Tunnels → Install connector`)。 + +#### ha_connections + +到 Cloudflare edge 的高可用连接数。 + +上限为已发现的 edge 地址数量。 + +#### protocol + +edge 连接使用的传输协议。 + +`quic` `http2` 之一。 + +#### post_quantum + +在控制连接上启用后量子密钥交换。 + +#### edge_ip_version + +连接 Cloudflare edge 时使用的 IP 版本。 + +`0`(自动)`4` `6` 之一。 + +#### datagram_version + +通过 QUIC 进行 UDP 代理时使用的数据报协议版本。 + +`v2` `v3` 之一。仅在 `protocol` 为 `quic` 时有效。 + +#### grace_period + +正在处理的 edge 连接的优雅关闭窗口。 + +#### region + +Cloudflare edge 区域选择器。 + +与 `token` 中嵌入的 endpoint 冲突。 + +#### control_dialer + +隧道客户端拨向 Cloudflare 控制面时使用的 +[拨号字段](/zh/configuration/shared/dial/)。 + +#### tunnel_dialer + +隧道客户端拨向 Cloudflare edge 数据面时使用的 +[拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/inbound/direct.md b/docs/configuration/inbound/direct.md new file mode 100644 index 00000000..6dc93578 --- /dev/null +++ b/docs/configuration/inbound/direct.md @@ -0,0 +1,36 @@ +`direct` inbound is a tunnel server. + +### Structure + +```json +{ + "type": "direct", + "tag": "direct-in", + + ... // Listen Fields + + "network": "udp", + "override_address": "1.0.0.1", + "override_port": 53 +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### network + +Listen network, one of `tcp` `udp`. + +Both if empty. + +#### override_address + +Override the connection destination address. + +#### override_port + +Override the connection destination port. \ No newline at end of file diff --git a/docs/configuration/inbound/direct.zh.md b/docs/configuration/inbound/direct.zh.md new file mode 100644 index 00000000..a3861a53 --- /dev/null +++ b/docs/configuration/inbound/direct.zh.md @@ -0,0 +1,37 @@ +`direct` 入站是一个隧道服务器。 + +### 结构 + +```json +{ + "type": "direct", + "tag": "direct-in", + + ... // 监听字段 + + "network": "udp", + "override_address": "1.0.0.1", + "override_port": 53 +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### network + +监听的网络协议,`tcp` `udp` 之一。 + +默认所有。 + +#### override_address + +覆盖连接目标地址。 + +#### override_port + +覆盖连接目标端口。 + diff --git a/docs/configuration/inbound/http.md b/docs/configuration/inbound/http.md new file mode 100644 index 00000000..00343e22 --- /dev/null +++ b/docs/configuration/inbound/http.md @@ -0,0 +1,47 @@ +### Structure + +```json +{ + "type": "http", + "tag": "http-in", + + ... // Listen Fields + + "users": [ + { + "username": "admin", + "password": "admin" + } + ], + "tls": {}, + "set_system_proxy": false +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### users + +HTTP users. + +No authentication required if empty. + +#### set_system_proxy + +!!! quote "" + + Only supported on Linux, Android, Windows, and macOS. + +!!! warning "" + + To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead. + +Automatically set system proxy configuration when start and clean up when stop. diff --git a/docs/configuration/inbound/http.zh.md b/docs/configuration/inbound/http.zh.md new file mode 100644 index 00000000..e1dd876b --- /dev/null +++ b/docs/configuration/inbound/http.zh.md @@ -0,0 +1,47 @@ +### 结构 + +```json +{ + "type": "http", + "tag": "http-in", + + ... // 监听字段 + + "users": [ + { + "username": "admin", + "password": "admin" + } + ], + "tls": {}, + "set_system_proxy": false +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +#### users + +HTTP 用户 + +如果为空则不需要验证。 + +#### set_system_proxy + +!!! quote "" + + 仅支持 Linux、Android、Windows 和 macOS。 + +!!! warning "" + + 要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。 + +启动时自动设置系统代理,停止时自动清理。 \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md new file mode 100644 index 00000000..4725aafc --- /dev/null +++ b/docs/configuration/inbound/hysteria.md @@ -0,0 +1,107 @@ +### Structure + +```json +{ + "type": "hysteria", + "tag": "hysteria-in", + + ... // Listen Fields + + "up": "100 Mbps", + "up_mbps": 100, + "down": "100 Mbps", + "down_mbps": 100, + "obfs": "fuck me till the daylight", + + "users": [ + { + "name": "sekai", + "auth": "", + "auth_str": "password" + } + ], + + "recv_window_conn": 0, + "recv_window_client": 0, + "max_conn_client": 0, + "disable_mtu_discovery": false, + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### up, down + +==Required== + +Format: `[Integer] [Unit]` e.g. `100 Mbps, 640 KBps, 2 Gbps` + +Supported units (case sensitive, b = bits, B = bytes, 8b=1B): + + bps (bits per second) + Bps (bytes per second) + Kbps (kilobits per second) + KBps (kilobytes per second) + Mbps (megabits per second) + MBps (megabytes per second) + Gbps (gigabits per second) + GBps (gigabytes per second) + Tbps (terabits per second) + TBps (terabytes per second) + +#### up_mbps, down_mbps + +==Required== + +`up, down` in Mbps. + +#### obfs + +Obfuscated password. + +#### users + +Hysteria users + +#### users.auth + +Authentication password, in base64. + +#### users.auth_str + +Authentication password. + +#### recv_window_conn + +The QUIC stream-level flow control window for receiving data. + +`15728640 (15 MB/s)` will be used if empty. + +#### recv_window_client + +The QUIC connection-level flow control window for receiving data. + +`67108864 (64 MB/s)` will be used if empty. + +#### max_conn_client + +The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open. + +`1024` will be used if empty. + +#### disable_mtu_discovery + +Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. + +Force enabled on for systems other than Linux and Windows (according to upstream). + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria.zh.md b/docs/configuration/inbound/hysteria.zh.md new file mode 100644 index 00000000..561d7102 --- /dev/null +++ b/docs/configuration/inbound/hysteria.zh.md @@ -0,0 +1,107 @@ +### 结构 + +```json +{ + "type": "hysteria", + "tag": "hysteria-in", + + ... // 监听字段 + + "up": "100 Mbps", + "up_mbps": 100, + "down": "100 Mbps", + "down_mbps": 100, + "obfs": "fuck me till the daylight", + + "users": [ + { + "name": "sekai", + "auth": "", + "auth_str": "password" + } + ], + + "recv_window_conn": 0, + "recv_window_client": 0, + "max_conn_client": 0, + "disable_mtu_discovery": false, + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### up, down + +==必填== + +格式: `[Integer] [Unit]` 例如: `100 Mbps, 640 KBps, 2 Gbps` + +支持的单位 (大小写敏感, b = bits, B = bytes, 8b=1B): + + bps (bits per second) + Bps (bytes per second) + Kbps (kilobits per second) + KBps (kilobytes per second) + Mbps (megabits per second) + MBps (megabytes per second) + Gbps (gigabits per second) + GBps (gigabytes per second) + Tbps (terabits per second) + TBps (terabytes per second) + +#### up_mbps, down_mbps + +==必填== + +以 Mbps 为单位的 `up, down`。 + +#### obfs + +混淆密码。 + +#### users + +Hysteria 用户 + +#### users.auth + +base64 编码的认证密码。 + +#### users.auth_str + +认证密码。 + +#### recv_window_conn + +用于接收数据的 QUIC 流级流控制窗口。 + +默认 `15728640 (15 MB/s)`。 + +#### recv_window_client + +用于接收数据的 QUIC 连接级流控制窗口。 + +默认 `67108864 (64 MB/s)`。 + +#### max_conn_client + +允许对等点打开的 QUIC 并发双向流的最大数量。 + +默认 `1024`。 + +#### disable_mtu_discovery + +禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。 + +强制为 Linux 和 Windows 以外的系统启用(根据上游)。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md new file mode 100644 index 00000000..8426be24 --- /dev/null +++ b/docs/configuration/inbound/hysteria2.md @@ -0,0 +1,159 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [bbr_profile](#bbr_profile) + +!!! quote "Changes in sing-box 1.11.0" + + :material-alert: [masquerade](#masquerade) + :material-alert: [ignore_client_bandwidth](#ignore_client_bandwidth) + +### Structure + +```json +{ + "type": "hysteria2", + "tag": "hy2-in", + + ... // Listen Fields + + "up_mbps": 100, + "down_mbps": 100, + "obfs": { + "type": "salamander", + "password": "cry_me_a_r1ver" + }, + "users": [ + { + "name": "tobyxdd", + "password": "goofy_ahh_password" + } + ], + "ignore_client_bandwidth": false, + "tls": {}, + "masquerade": "", // or {} + "bbr_profile": "", + "brutal_debug": false +} +``` + +!!! warning "Difference from official Hysteria2" + + The official program supports an authentication method called **userpass**, + which essentially uses a combination of `:` as the actual password, + while sing-box does not provide this alias. + To use sing-box with the official program, you need to fill in that combination as the actual password. + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### up_mbps, down_mbps + +Max bandwidth, in Mbps. + +Not limited if empty. + +Conflict with `ignore_client_bandwidth`. + +#### obfs.type + +QUIC traffic obfuscator type, only available with `salamander`. + +Disabled if empty. + +#### obfs.password + +QUIC traffic obfuscator password. + +#### users + +Hysteria2 users + +#### users.password + +Authentication password + +#### ignore_client_bandwidth + +*When `up_mbps` and `down_mbps` are not set*: + +Commands clients to use the BBR CC instead of Hysteria CC. + +*When `up_mbps` and `down_mbps` are set*: + +Deny clients to use the BBR CC. + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### masquerade + +HTTP3 server behavior (URL string configuration) when authentication fails. + +| Scheme | Example | Description | +|--------------|-------------------------|--------------------| +| `file` | `file:///var/www` | As a file server | +| `http/https` | `http://127.0.0.1:8080` | As a reverse proxy | + +Conflict with `masquerade.type`. + +A 404 page will be returned if masquerade is not configured. + +#### masquerade.type + +HTTP3 server behavior (Object configuration) when authentication fails. + +| Type | Description | Fields | +|----------|-----------------------------|-------------------------------------| +| `file` | As a file server | `directory` | +| `proxy` | As a reverse proxy | `url`, `rewrite_host` | +| `string` | Reply with a fixed response | `status_code`, `headers`, `content` | + +Conflict with `masquerade`. + +A 404 page will be returned if masquerade is not configured. + +#### masquerade.directory + +File server root directory. + +#### masquerade.url + +Reverse proxy target URL. + +#### masquerade.rewrite_host + +Rewrite the `Host` header to the target URL. + +#### masquerade.status_code + +Fixed response status code. + +#### masquerade.headers + +Fixed response headers. + +#### masquerade.content + +Fixed response content. + +#### bbr_profile + +!!! question "Since sing-box 1.14.0" + +BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`. + +`standard` is used by default. + +#### brutal_debug + +Enable debug information logging for Hysteria Brutal CC. diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md new file mode 100644 index 00000000..0c5e918e --- /dev/null +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -0,0 +1,156 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [bbr_profile](#bbr_profile) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-alert: [masquerade](#masquerade) + :material-alert: [ignore_client_bandwidth](#ignore_client_bandwidth) + +### 结构 + +```json +{ + "type": "hysteria2", + "tag": "hy2-in", + + ... // 监听字段 + + "up_mbps": 100, + "down_mbps": 100, + "obfs": { + "type": "salamander", + "password": "cry_me_a_r1ver" + }, + "users": [ + { + "name": "tobyxdd", + "password": "goofy_ahh_password" + } + ], + "ignore_client_bandwidth": false, + "tls": {}, + "masquerade": "", // 或 {} + "bbr_profile": "", + "brutal_debug": false +} +``` + +!!! warning "与官方 Hysteria2 的区别" + + 官方程序支持一种名为 **userpass** 的验证方式, + 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 + 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### up_mbps, down_mbps + +支持的速率,默认不限制。 + +与 `ignore_client_bandwidth` 冲突。 + +#### obfs.type + +QUIC 流量混淆器类型,仅可设为 `salamander`。 + +如果为空则禁用。 + +#### obfs.password + +QUIC 流量混淆器密码. + +#### users + +Hysteria 用户 + +#### users.password + +认证密码。 + +#### ignore_client_bandwidth + +*当 `up_mbps` 和 `down_mbps` 未设定时*: + +命令客户端使用 BBR 拥塞控制算法而不是 Hysteria CC。 + +*当 `up_mbps` 和 `down_mbps` 已设定时*: + +禁止客户端使用 BBR 拥塞控制算法。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +#### masquerade + +HTTP3 服务器认证失败时的行为 (URL 字符串配置)。 + +| Scheme | 示例 | 描述 | +|--------------|-------------------------|---------| +| `file` | `file:///var/www` | 作为文件服务器 | +| `http/https` | `http://127.0.0.1:8080` | 作为反向代理 | + +如果 masquerade 未配置,则返回 404 页。 + +与 `masquerade.type` 冲突。 + +#### masquerade.type + +HTTP3 服务器认证失败时的行为 (对象配置)。 + +| Type | 描述 | 字段 | +|----------|---------|-------------------------------------| +| `file` | 作为文件服务器 | `directory` | +| `proxy` | 作为反向代理 | `url`, `rewrite_host` | +| `string` | 返回固定响应 | `status_code`, `headers`, `content` | + +如果 masquerade 未配置,则返回 404 页。 + +与 `masquerade` 冲突。 + +#### masquerade.directory + +文件服务器根目录。 + +#### masquerade.url + +反向代理目标 URL。 + +#### masquerade.rewrite_host + +重写请求头中的 Host 字段到目标 URL。 + +#### masquerade.status_code + +固定响应状态码。 + +#### masquerade.headers + +固定响应头。 + +#### masquerade.content + +固定响应内容。 + +#### bbr_profile + +!!! question "自 sing-box 1.14.0 起" + +BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 + +默认使用 `standard`。 + +#### brutal_debug + +启用 Hysteria Brutal CC 的调试信息日志记录。 diff --git a/docs/configuration/inbound/index.md b/docs/configuration/inbound/index.md new file mode 100644 index 00000000..274a3780 --- /dev/null +++ b/docs/configuration/inbound/index.md @@ -0,0 +1,41 @@ +# Inbound + +### Structure + +```json +{ + "inbounds": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | Injectable | +|---------------|-------------------------------|------------------| +| `direct` | [Direct](./direct/) | :material-close: | +| `mixed` | [Mixed](./mixed/) | TCP | +| `socks` | [SOCKS](./socks/) | TCP | +| `http` | [HTTP](./http/) | TCP | +| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP | +| `vmess` | [VMess](./vmess/) | TCP | +| `trojan` | [Trojan](./trojan/) | TCP | +| `naive` | [Naive](./naive/) | :material-close: | +| `hysteria` | [Hysteria](./hysteria/) | :material-close: | +| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP | +| `tuic` | [TUIC](./tuic/) | :material-close: | +| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: | +| `vless` | [VLESS](./vless/) | TCP | +| `anytls` | [AnyTLS](./anytls/) | TCP | +| `tun` | [Tun](./tun/) | :material-close: | +| `redirect` | [Redirect](./redirect/) | :material-close: | +| `tproxy` | [TProxy](./tproxy/) | :material-close: | +| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: | + +#### tag + +The tag of the inbound. \ No newline at end of file diff --git a/docs/configuration/inbound/index.zh.md b/docs/configuration/inbound/index.zh.md new file mode 100644 index 00000000..99f8df3b --- /dev/null +++ b/docs/configuration/inbound/index.zh.md @@ -0,0 +1,41 @@ +# 入站 + +### 结构 + +```json +{ + "inbounds": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | 注入支持 | +|---------------|-------------------------------|------------------| +| `direct` | [Direct](./direct/) | :material-close: | +| `mixed` | [Mixed](./mixed/) | TCP | +| `socks` | [SOCKS](./socks/) | TCP | +| `http` | [HTTP](./http/) | TCP | +| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP | +| `vmess` | [VMess](./vmess/) | TCP | +| `trojan` | [Trojan](./trojan/) | TCP | +| `naive` | [Naive](./naive/) | :material-close: | +| `hysteria` | [Hysteria](./hysteria/) | :material-close: | +| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP | +| `tuic` | [TUIC](./tuic/) | :material-close: | +| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: | +| `vless` | [VLESS](./vless/) | TCP | +| `anytls` | [AnyTLS](./anytls/) | TCP | +| `tun` | [Tun](./tun/) | :material-close: | +| `redirect` | [Redirect](./redirect/) | :material-close: | +| `tproxy` | [TProxy](./tproxy/) | :material-close: | +| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: | + +#### tag + +入站的标签。 \ No newline at end of file diff --git a/docs/configuration/inbound/mixed.md b/docs/configuration/inbound/mixed.md new file mode 100644 index 00000000..e9deec75 --- /dev/null +++ b/docs/configuration/inbound/mixed.md @@ -0,0 +1,44 @@ +`mixed` inbound is a socks4, socks4a, socks5 and http server. + +### Structure + +```json +{ + "type": "mixed", + "tag": "mixed-in", + + ... // Listen Fields + + "users": [ + { + "username": "admin", + "password": "admin" + } + ], + "set_system_proxy": false +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +SOCKS and HTTP users. + +No authentication required if empty. + +#### set_system_proxy + +!!! quote "" + + Only supported on Linux, Android, Windows, and macOS. + +!!! warning "" + + To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead. + +Automatically set system proxy configuration when start and clean up when stop. diff --git a/docs/configuration/inbound/mixed.zh.md b/docs/configuration/inbound/mixed.zh.md new file mode 100644 index 00000000..448c66b4 --- /dev/null +++ b/docs/configuration/inbound/mixed.zh.md @@ -0,0 +1,44 @@ +`mixed` 入站是一个 socks4, socks4a, socks5 和 http 服务器. + +### 结构 + +```json +{ + "type": "mixed", + "tag": "mixed-in", + + ... // 监听字段 + + "users": [ + { + "username": "admin", + "password": "admin" + } + ], + "set_system_proxy": false +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +SOCKS 和 HTTP 用户 + +如果为空则不需要验证。 + +#### set_system_proxy + +!!! quote "" + + 仅支持 Linux、Android、Windows 和 macOS。 + +!!! warning "" + + 要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。 + +启动时自动设置系统代理,停止时自动清理。 \ No newline at end of file diff --git a/docs/configuration/inbound/naive.md b/docs/configuration/inbound/naive.md new file mode 100644 index 00000000..a360fa95 --- /dev/null +++ b/docs/configuration/inbound/naive.md @@ -0,0 +1,63 @@ +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [quic_congestion_control](#quic_congestion_control) + +### Structure + +```json +{ +"type": "naive", +"tag": "naive-in", +"network": "udp", +... +// Listen Fields + +"users": [ +{ +"username": "sekai", +"password": "password" +} +], +"quic_congestion_control": "", +"tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### network + +Listen network, one of `tcp` `udp`. + +Both if empty. + +#### users + +==Required== + +Naive users. + +#### quic_congestion_control + +!!! question "Since sing-box 1.13.0" + +QUIC congestion control algorithm. + +| Algorithm | Description | +|----------------|---------------------------------| +| `bbr` | BBR | +| `bbr_standard` | BBR (Standard version) | +| `bbr2` | BBRv2 | +| `bbr2_variant` | BBRv2 (An experimental variant) | +| `cubic` | CUBIC | +| `reno` | New Reno | + +`bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on). + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). \ No newline at end of file diff --git a/docs/configuration/inbound/naive.zh.md b/docs/configuration/inbound/naive.zh.md new file mode 100644 index 00000000..0984d310 --- /dev/null +++ b/docs/configuration/inbound/naive.zh.md @@ -0,0 +1,63 @@ +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [quic_congestion_control](#quic_congestion_control) + +### 结构 + +```json +{ +"type": "naive", +"tag": "naive-in", +"network": "udp", + +... // 监听字段 + +"users": [ +{ +"username": "sekai", +"password": "password" +} +], +"quic_congestion_control": "", +"tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### network + +监听的网络协议,`tcp` `udp` 之一。 + +默认所有。 + +#### users + +==必填== + +Naive 用户。 + +#### quic_congestion_control + +!!! question "Since sing-box 1.13.0" + +QUIC 拥塞控制算法。 + +| 算法 | 描述 | +|----------------|--------------------| +| `bbr` | BBR | +| `bbr_standard` | BBR (标准版) | +| `bbr2` | BBRv2 | +| `bbr2_variant` | BBRv2 (一种试验变体) | +| `cubic` | CUBIC | +| `reno` | New Reno | + +默认使用 `bbr`(NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/inbound/redirect.md b/docs/configuration/inbound/redirect.md new file mode 100644 index 00000000..50a5bacd --- /dev/null +++ b/docs/configuration/inbound/redirect.md @@ -0,0 +1,18 @@ +!!! quote "" + + Only supported on Linux and macOS. + +### Structure + +```json +{ + "type": "redirect", + "tag": "redirect-in", + + ... // Listen Fields +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/redirect.zh.md b/docs/configuration/inbound/redirect.zh.md new file mode 100644 index 00000000..a03049e5 --- /dev/null +++ b/docs/configuration/inbound/redirect.zh.md @@ -0,0 +1,17 @@ +!!! quote "" + + 仅支持 Linux 和 macOS。 + +### 结构 + +```json +{ + "type": "redirect", + "tag": "redirect-in", + + ... // 监听字段 +} +``` +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/shadowsocks.md b/docs/configuration/inbound/shadowsocks.md new file mode 100644 index 00000000..e5115857 --- /dev/null +++ b/docs/configuration/inbound/shadowsocks.md @@ -0,0 +1,96 @@ +### Structure + +```json +{ + "type": "shadowsocks", + "tag": "ss-in", + + ... // Listen Fields + + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "managed": false, + "multiplex": {} +} +``` + +### Multi-User Structure + +```json +{ + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "users": [ + { + "name": "sekai", + "password": "PCD2Z4o12bKUoFa3cC97Hw==" + } + ], + "multiplex": {} +} +``` + +### Relay Structure + +```json +{ + "type": "shadowsocks", + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "destinations": [ + { + "name": "test", + "server": "example.com", + "server_port": 8080, + "password": "PCD2Z4o12bKUoFa3cC97Hw==" + } + ], + "multiplex": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### network + +Listen network, one of `tcp` `udp`. + +Both if empty. + +#### method + +==Required== + +| Method | Key Length | +|-------------------------------|------------| +| 2022-blake3-aes-128-gcm | 16 | +| 2022-blake3-aes-256-gcm | 32 | +| 2022-blake3-chacha20-poly1305 | 32 | +| none | / | +| aes-128-gcm | / | +| aes-192-gcm | / | +| aes-256-gcm | / | +| chacha20-ietf-poly1305 | / | +| xchacha20-ietf-poly1305 | / | + +#### password + +==Required== + +| Method | Password Format | +|---------------|------------------------------------------------| +| none | / | +| 2022 methods | `sing-box generate rand --base64 ` | +| other methods | any string | + +#### managed + +Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user. + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. diff --git a/docs/configuration/inbound/shadowsocks.zh.md b/docs/configuration/inbound/shadowsocks.zh.md new file mode 100644 index 00000000..a991b0c3 --- /dev/null +++ b/docs/configuration/inbound/shadowsocks.zh.md @@ -0,0 +1,96 @@ +### 结构 + +```json +{ + "type": "shadowsocks", + "tag": "ss-in", + + ... // 监听字段 + + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "managed": false, + "multiplex": {} +} +``` + +### 多用户结构 + +```json +{ + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "users": [ + { + "name": "sekai", + "password": "PCD2Z4o12bKUoFa3cC97Hw==" + } + ], + "multiplex": {} +} +``` + +### 中转结构 + +```json +{ + "type": "shadowsocks", + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "destinations": [ + { + "name": "test", + "server": "example.com", + "server_port": 8080, + "password": "PCD2Z4o12bKUoFa3cC97Hw==" + } + ], + "multiplex": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### network + +监听的网络协议,`tcp` `udp` 之一。 + +默认所有。 + +#### method + +==必填== + +| 方法 | 密钥长度 | +|-------------------------------|------| +| 2022-blake3-aes-128-gcm | 16 | +| 2022-blake3-aes-256-gcm | 32 | +| 2022-blake3-chacha20-poly1305 | 32 | +| none | / | +| aes-128-gcm | / | +| aes-192-gcm | / | +| aes-256-gcm | / | +| chacha20-ietf-poly1305 | / | +| xchacha20-ietf-poly1305 | / | + +#### password + +==必填== + +| 方法 | 密码格式 | +|---------------|------------------------------------------| +| none | / | +| 2022 methods | `sing-box generate rand --base64 <密钥长度>` | +| other methods | 任意字符串 | + +#### managed + +默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。 + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 diff --git a/docs/configuration/inbound/shadowtls.md b/docs/configuration/inbound/shadowtls.md new file mode 100644 index 00000000..9dbf1dd5 --- /dev/null +++ b/docs/configuration/inbound/shadowtls.md @@ -0,0 +1,107 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [wildcard_sni](#wildcard_sni) + +### Structure + +```json +{ + "type": "shadowtls", + "tag": "st-in", + + ... // Listen Fields + + "version": 3, + "password": "fuck me till the daylight", + "users": [ + { + "name": "sekai", + "password": "8JCsPssfgS8tiRwiMlhARg==" + } + ], + "handshake": { + "server": "google.com", + "server_port": 443, + + ... // Dial Fields + }, + "handshake_for_server_name": { + "example.com": { + "server": "example.com", + "server_port": 443, + + ... // Dial Fields + } + }, + "strict_mode": false, + "wildcard_sni": "" +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### version + +ShadowTLS protocol version. + +| Value | Protocol Version | +|---------------|-----------------------------------------------------------------------------------------| +| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | +| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | +| `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | + +#### password + +ShadowTLS password. + +Only available in the ShadowTLS protocol 2. + +#### users + +ShadowTLS users. + +Only available in the ShadowTLS protocol 3. + +#### handshake + +==Required== + +When `wildcard_sni` is configured to `all`, the server address is optional. + +Handshake server address and [Dial Fields](/configuration/shared/dial/). + +#### handshake_for_server_name + +Handshake server address and [Dial Fields](/configuration/shared/dial/) for specific server name. + +Only available in the ShadowTLS protocol 2/3. + +#### strict_mode + +ShadowTLS strict mode. + +Only available in the ShadowTLS protocol 3. + +#### wildcard_sni + +!!! question "Since sing-box 1.12.0" + +ShadowTLS wildcard SNI mode. + +Available values are: + +* `off`: (default) Disabled. +* `authed`: Authenticated connections will have their destination overwritten to `(servername):443` +* `all`: All connections will have their destination overwritten to `(servername):443` + +Additionally, connections matching `handshake_for_server_name` are not affected. + +Only available in the ShadowTLS protocol 3. diff --git a/docs/configuration/inbound/shadowtls.zh.md b/docs/configuration/inbound/shadowtls.zh.md new file mode 100644 index 00000000..be860051 --- /dev/null +++ b/docs/configuration/inbound/shadowtls.zh.md @@ -0,0 +1,107 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [wildcard_sni](#wildcard_sni) + +### 结构 + +```json +{ + "type": "shadowtls", + "tag": "st-in", + + ... // 监听字段 + + "version": 3, + "password": "fuck me till the daylight", + "users": [ + { + "name": "sekai", + "password": "8JCsPssfgS8tiRwiMlhARg==" + } + ], + "handshake": { + "server": "google.com", + "server_port": 443, + + ... // 拨号字段 + }, + "handshake_for_server_name": { + "example.com": { + "server": "example.com", + "server_port": 443, + + ... // 拨号字段 + } + }, + "strict_mode": false, + "wildcard_sni": "" +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### version + +ShadowTLS 协议版本。 + +| 值 | 协议版本 | +|---------------|-----------------------------------------------------------------------------------------| +| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | +| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | +| `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | + +#### password + +ShadowTLS 密码。 + +仅在 ShadowTLS 协议版本 2 中可用。 + +#### users + +ShadowTLS 用户。 + +仅在 ShadowTLS 协议版本 3 中可用。 + +#### handshake + +==必填== + +握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 + +#### handshake_for_server_name + +==必填== + +对于特定服务器名称的握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 + +仅在 ShadowTLS 协议版本 2/3 中可用。 + +#### strict_mode + +ShadowTLS 严格模式。 + +仅在 ShadowTLS 协议版本 3 中可用。 + +#### wildcard_sni + +!!! question "自 sing-box 1.12.0 起" + +ShadowTLS 通配符 SNI 模式。 + +可用值: + +* `off`:(默认)禁用。 +* `authed`:已认证的连接的目标将被重写为 `(servername):443`。 +* `all`:所有连接的目标将被重写为 `(servername):443`。 + +此外,匹配 `handshake_for_server_name` 的连接不受影响。 + +仅在 ShadowTLS 协议 3 中可用。 diff --git a/docs/configuration/inbound/socks.md b/docs/configuration/inbound/socks.md new file mode 100644 index 00000000..4937f390 --- /dev/null +++ b/docs/configuration/inbound/socks.md @@ -0,0 +1,31 @@ +`socks` inbound is a socks4, socks4a, socks5 server. + +### Structure + +```json +{ + "type": "socks", + "tag": "socks-in", + + ... // Listen Fields + + "users": [ + { + "username": "admin", + "password": "admin" + } + ] +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +SOCKS users. + +No authentication required if empty. diff --git a/docs/configuration/inbound/socks.zh.md b/docs/configuration/inbound/socks.zh.md new file mode 100644 index 00000000..90314526 --- /dev/null +++ b/docs/configuration/inbound/socks.zh.md @@ -0,0 +1,31 @@ +`socks` 入站是一个 socks4, socks4a 和 socks5 服务器. + +### 结构 + +```json +{ + "type": "socks", + "tag": "socks-in", + + ... // 监听字段 + + "users": [ + { + "username": "admin", + "password": "admin" + } + ] +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +SOCKS 用户 + +如果为空则不需要验证。 diff --git a/docs/configuration/inbound/tproxy.md b/docs/configuration/inbound/tproxy.md new file mode 100644 index 00000000..42288537 --- /dev/null +++ b/docs/configuration/inbound/tproxy.md @@ -0,0 +1,28 @@ +!!! quote "" + + Only supported on Linux. + +### Structure + +```json +{ + "type": "tproxy", + "tag": "tproxy-in", + + ... // Listen Fields + + "network": "udp" +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### network + +Listen network, one of `tcp` `udp`. + +Both if empty. diff --git a/docs/configuration/inbound/tproxy.zh.md b/docs/configuration/inbound/tproxy.zh.md new file mode 100644 index 00000000..6e35ad5e --- /dev/null +++ b/docs/configuration/inbound/tproxy.zh.md @@ -0,0 +1,28 @@ +!!! quote "" + + 仅支持 Linux。 + +### 结构 + +```json +{ + "type": "tproxy", + "tag": "tproxy-in", + + ... // 监听字段 + + "network": "udp" +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### network + +监听的网络协议,`tcp` `udp` 之一。 + +默认所有。 diff --git a/docs/configuration/inbound/trojan.md b/docs/configuration/inbound/trojan.md new file mode 100644 index 00000000..e277236b --- /dev/null +++ b/docs/configuration/inbound/trojan.md @@ -0,0 +1,68 @@ +### Structure + +```json +{ + "type": "trojan", + "tag": "trojan-in", + + ... // Listen Fields + + "users": [ + { + "name": "sekai", + "password": "8JCsPssfgS8tiRwiMlhARg==" + } + ], + "tls": {}, + "fallback": { + "server": "127.0.0.1", + "server_port": 8080 + }, + "fallback_for_alpn": { + "http/1.1": { + "server": "127.0.0.1", + "server_port": 8081 + } + }, + "multiplex": {}, + "transport": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +==Required== + +Trojan users. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### fallback + +!!! failure "" + + There is no evidence that GFW detects and blocks Trojan servers based on HTTP responses, and opening the standard http/s port on the server is a much bigger signature. + +Fallback server configuration. Disabled if `fallback` and `fallback_for_alpn` are empty. + +#### fallback_for_alpn + +Fallback server configuration for specified ALPN. + +If not empty, TLS fallback requests with ALPN not in this table will be rejected. + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). diff --git a/docs/configuration/inbound/trojan.zh.md b/docs/configuration/inbound/trojan.zh.md new file mode 100644 index 00000000..d81b4c1d --- /dev/null +++ b/docs/configuration/inbound/trojan.zh.md @@ -0,0 +1,68 @@ +### 结构 + +```json +{ + "type": "trojan", + "tag": "trojan-in", + + ... // 监听字段 + + "users": [ + { + "name": "sekai", + "password": "8JCsPssfgS8tiRwiMlhARg==" + } + ], + "tls": {}, + "fallback": { + "server": "127.0.0.1", + "server_port": 8080 + }, + "fallback_for_alpn": { + "http/1.1": { + "server": "127.0.0.1", + "server_port": 8081 + } + }, + "multiplex": {}, + "transport": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +==必填== + +Trojan 用户。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +#### fallback + +!!! failure "" + + 没有证据表明 GFW 基于 HTTP 响应检测并阻止 Trojan 服务器,并且在服务器上打开标准 http/s 端口是一个更大的特征。 + +回退服务器配置。如果 `fallback` 和 `fallback_for_alpn` 为空,则禁用回退。 + +#### fallback_for_alpn + +为 ALPN 指定回退服务器配置。 + +如果不为空,ALPN 不在此列表中的 TLS 回退请求将被拒绝。 + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 \ No newline at end of file diff --git a/docs/configuration/inbound/tuic.md b/docs/configuration/inbound/tuic.md new file mode 100644 index 00000000..8a2d8c7e --- /dev/null +++ b/docs/configuration/inbound/tuic.md @@ -0,0 +1,78 @@ +### Structure + +```json +{ + "type": "tuic", + "tag": "tuic-in", + + ... // Listen Fields + + "users": [ + { + "name": "sekai", + "uuid": "059032A9-7D40-4A96-9BB1-36823D848068", + "password": "hello" + } + ], + "congestion_control": "cubic", + "auth_timeout": "3s", + "zero_rtt_handshake": false, + "heartbeat": "10s", + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +TUIC users + +#### users.uuid + +==Required== + +TUIC user uuid + +#### users.password + +TUIC user password + +#### congestion_control + +QUIC congestion control algorithm + +One of: `cubic`, `new_reno`, `bbr` + +`cubic` is used by default. + +#### auth_timeout + +How long the server should wait for the client to send the authentication command + +`3s` is used by default. + +#### zero_rtt_handshake + +Enable 0-RTT QUIC connection handshake on the client side +This is not impacting much on the performance, as the protocol is fully multiplexed + +!!! warning "" + Disabling this is highly recommended, as it is vulnerable to replay attacks. + See [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones) + +#### heartbeat + +Interval for sending heartbeat packets for keeping the connection alive + +`10s` is used by default. + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). \ No newline at end of file diff --git a/docs/configuration/inbound/tuic.zh.md b/docs/configuration/inbound/tuic.zh.md new file mode 100644 index 00000000..ae531635 --- /dev/null +++ b/docs/configuration/inbound/tuic.zh.md @@ -0,0 +1,78 @@ +### 结构 + +```json +{ + "type": "tuic", + "tag": "tuic-in", + + ... // 监听字段 + + "users": [ + { + "name": "sekai", + "uuid": "059032A9-7D40-4A96-9BB1-36823D848068", + "password": "hello" + } + ], + "congestion_control": "cubic", + "auth_timeout": "3s", + "zero_rtt_handshake": false, + "heartbeat": "10s", + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +TUIC 用户 + +#### users.uuid + +==必填== + +TUIC 用户 UUID + +#### users.password + +TUIC 用户密码 + +#### congestion_control + +QUIC 拥塞控制算法 + +可选值: `cubic`, `new_reno`, `bbr` + +默认使用 `cubic`。 + +#### auth_timeout + +服务器等待客户端发送认证命令的时间 + +默认使用 `3s`。 + +#### zero_rtt_handshake + +在客户端启用 0-RTT QUIC 连接握手 +这对性能影响不大,因为协议是完全复用的 + +!!! warning "" +强烈建议禁用此功能,因为它容易受到重放攻击。 +请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones) + +#### heartbeat + +发送心跳包以保持连接存活的时间间隔 + +默认使用 `10s`。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md new file mode 100644 index 00000000..6dae06e1 --- /dev/null +++ b/docs/configuration/inbound/tun.md @@ -0,0 +1,635 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [include_mac_address](#include_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + +!!! quote "Changes in sing-box 1.13.3" + + :material-alert: [strict_route](#strict_route) + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) + :material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue) + :material-plus: [exclude_mptcp](#exclude_mptcp) + :material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [loopback_address](#loopback_address) + +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-alert: [gso](#gso) + :material-alert-decagram: [route_address_set](#stack) + :material-alert-decagram: [route_exclude_address_set](#stack) + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [address](#address) + :material-delete-clock: [inet4_address](#inet4_address) + :material-delete-clock: [inet6_address](#inet6_address) + :material-plus: [route_address](#route_address) + :material-delete-clock: [inet4_route_address](#inet4_route_address) + :material-delete-clock: [inet6_route_address](#inet6_route_address) + :material-plus: [route_exclude_address](#route_address) + :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [iproute2_table_index](#iproute2_table_index) + :material-plus: [iproute2_rule_index](#iproute2_table_index) + :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) + :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) + :material-plus: [route_address_set](#route_address_set) + :material-plus: [route_exclude_address_set](#route_address_set) + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) + :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [gso](#gso) + :material-alert-decagram: [stack](#stack) + +!!! quote "" + + Only supported on Linux, Windows and macOS. + +### Structure + +```json +{ + "type": "tun", + "tag": "tun-in", + "interface_name": "tun0", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "mtu": 9000, + "auto_route": true, + "iproute2_table_index": 2022, + "iproute2_rule_index": 9000, + "auto_redirect": true, + "auto_redirect_input_mark": "0x2023", + "auto_redirect_output_mark": "0x2024", + "auto_redirect_reset_mark": "0x2025", + "auto_redirect_nfqueue": 100, + "auto_redirect_iproute2_fallback_rule_index": 32768, + "exclude_mptcp": false, + "loopback_address": [ + "10.7.0.1" + ], + "strict_route": true, + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], + "endpoint_independent_nat": false, + "udp_timeout": "5m", + "stack": "system", + "include_interface": [ + "lan0" + ], + "exclude_interface": [ + "lan1" + ], + "include_uid": [ + 0 + ], + "include_uid_range": [ + "1000:99999" + ], + "exclude_uid": [ + 1000 + ], + "exclude_uid_range": [ + "1000:99999" + ], + "include_android_user": [ + 0, + 10 + ], + "include_package": [ + "com.android.chrome" + ], + "exclude_package": [ + "com.android.captiveportallogin" + ], + "include_mac_address": [ + "00:11:22:33:44:55" + ], + "exclude_mac_address": [ + "66:77:88:99:aa:bb" + ], + "platform": { + "http_proxy": { + "enabled": false, + "server": "127.0.0.1", + "server_port": 8080, + "bypass_domain": [], + "match_domain": [] + } + }, + // Deprecated + "gso": false, + "inet4_address": [ + "172.19.0.1/30" + ], + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ], + ... + // Listen Fields +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +!!! warning "" + + If tun is running in non-privileged mode, addresses and MTU will not be configured automatically, please make sure the settings are accurate. + +### Fields + +#### interface_name + +Virtual device name, automatically selected if empty. + +#### address + +!!! question "Since sing-box 1.10.0" + +IPv4 and IPv6 prefix for the tun interface. + +#### inet4_address + +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_address` is merged to `address` and will be removed in sing-box 1.12.0. + +IPv4 prefix for the tun interface. + +#### inet6_address + +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_address` is merged to `address` and will be removed in sing-box 1.12.0. + +IPv6 prefix for the tun interface. + +#### mtu + +The maximum transmission unit. + +#### gso + +!!! failure "Deprecated in sing-box 1.11.0" + + GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works, and will be removed in sing-box 1.12.0. + +!!! question "Since sing-box 1.8.0" + +!!! quote "" + + Only supported on Linux with `auto_route` enabled. + +Enable generic segmentation offload. + +#### auto_route + +Set the default route to the Tun. + +!!! quote "" + + To avoid traffic loopback, set `route.auto_detect_interface` or `route.default_interface` or `outbound.bind_interface` + +!!! note "Use with Android VPN" + + 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" + +Linux iproute2 table index generated by `auto_route`. + +`2022` is used by default. + +#### iproute2_rule_index + +!!! question "Since sing-box 1.10.0" + +Linux iproute2 rule start index generated by `auto_route`. + +`9000` is used by default. + +#### auto_redirect + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with `auto_route` enabled. + +Improve TUN routing and performance using nftables. + +`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. + +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). + +`auto_redirect` also automatically inserts compatibility rules +into the OpenWrt fw4 table, i.e. +it will work on routers without any extra configuration. + +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 `auto_redirect`. + +`0x2023` is used by default. + +#### auto_redirect_output_mark + +!!! question "Since sing-box 1.10.0" + +Connection output mark used by `auto_redirect`. + +`0x2024` is used by default. + +#### auto_redirect_reset_mark + +!!! question "Since sing-box 1.13.0" + +Connection reset mark used by `auto_redirect` pre-matching. + +`0x2025` is used by default. + +#### auto_redirect_nfqueue + +!!! question "Since sing-box 1.13.0" + +NFQueue number used by `auto_redirect` pre-matching. + +`100` is used by default. + +#### auto_redirect_iproute2_fallback_rule_index + +!!! question "Since sing-box 1.12.18" + +Linux iproute2 fallback rule index generated by `auto_redirect`. + +This rule is checked after system default rules (32766: main, 32767: default), +routing traffic to the sing-box table only when no route is found in system tables. + +`32768` is used by default. + +#### exclude_mptcp + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + +MPTCP cannot be transparently proxied due to protocol limitations. + +Such traffic is usually created by Apple systems. + +When enabled, MPTCP connections will bypass sing-box and connect directly, otherwise, will be rejected to avoid errors by default. + +#### loopback_address + +!!! question "Since sing-box 1.12.0" + +Loopback addresses make TCP connections to the specified address connect to the source address. + +Setting option value to `10.7.0.1` achieves the same behavior as SideStore/StosVPN. + +When `auto_redirect` is enabled, the same behavior can be achieved for LAN devices (not just local) as a gateway. + +#### strict_route + +Enforce strict routing rules when `auto_route` is enabled: + +*In Linux*: + +* Let unsupported network unreachable +* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN. +* When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic: + * Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box. + * Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box. + +*In Windows*: + +* 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 Windows applications (such as VirtualBox) from working properly in certain situations. + +#### route_address + +!!! question "Since sing-box 1.10.0" + +Use custom routes instead of default when `auto_route` is enabled. + +#### inet4_route_address + +!!! failure "Deprecated in sing-box 1.10.0" + +`inet4_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address) +instead. + +Use custom routes instead of default when `auto_route` is enabled. + +#### inet6_route_address + +!!! failure "Deprecated in sing-box 1.10.0" + +`inet6_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address) +instead. + +Use custom routes instead of default when `auto_route` is enabled. + +#### route_exclude_address + +!!! question "Since sing-box 1.10.0" + +Exclude custom routes when `auto_route` is enabled. + +#### inet4_route_exclude_address + +!!! failure "Deprecated in sing-box 1.10.0" + +`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please +use [route_exclude_address](#route_exclude_address) instead. + +Exclude custom routes when `auto_route` is enabled. + +#### inet6_route_exclude_address + +!!! failure "Deprecated in sing-box 1.10.0" + +`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please +use [route_exclude_address](#route_exclude_address) instead. + +Exclude custom routes when `auto_route` is enabled. + +#### route_address_set + +=== "With `auto_redirect` enabled" + + !!! question "Since sing-box 1.10.0" + + !!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + + Add the destination IP CIDR rules in the specified rule-sets to the firewall. + Unmatched traffic will bypass the sing-box routes. + + Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + +=== "Without `auto_redirect` enabled" + + !!! question "Since sing-box 1.11.0" + + Add the destination IP CIDR rules in the specified rule-sets to routes, equivalent to adding to `route_address`. + Unmatched traffic will bypass the sing-box routes. + + Note that it **doesn't work on the Android graphical client** due to + the Android VpnService not being able to handle a large number of routes (DeadSystemException), + but otherwise it works fine on all command line clients and Apple platforms. + +#### route_exclude_address_set + +=== "With `auto_redirect` enabled" + + !!! question "Since sing-box 1.10.0" + + !!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + + Add the destination IP CIDR rules in the specified rule-sets to the firewall. + Matched traffic will bypass the sing-box routes. + +=== "Without `auto_redirect` enabled" + + !!! question "Since sing-box 1.11.0" + + Add the destination IP CIDR rules in the specified rule-sets to routes, equivalent to adding to `route_exclude_address`. + Matched traffic will bypass the sing-box routes. + + Note that it **doesn't work on the Android graphical client** due to + the Android VpnService not being able to handle a large number of routes (DeadSystemException), + but otherwise it works fine on all command line clients and Apple platforms. + +#### endpoint_independent_nat + +!!! info "" + + This item is only available on the gvisor stack, other stacks are endpoint-independent NAT by default. + +Enable endpoint-independent NAT. + +Performance may degrade slightly, so it is not recommended to enable on when it is not needed. + +#### udp_timeout + +UDP NAT expiration time. + +`5m` will be used by default. + +#### stack + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: The legacy LWIP stack has been deprecated and removed. + +TCP/IP stack. + +| Stack | Description | +|----------|-------------------------------------------------------------------------------------------------------| +| `system` | Perform L3 to L4 translation using the system network stack | +| `gvisor` | Perform L3 to L4 translation using [gVisor](https://github.com/google/gvisor)'s virtual network stack | +| `mixed` | Mixed `system` TCP stack and `gvisor` UDP stack | + +Defaults to the `mixed` stack if the gVisor build tag is enabled, otherwise defaults to the `system` stack. + +#### include_interface + +!!! quote "" + + Interface rules are only supported on Linux and require auto_route. + +Limit interfaces in route. Not limited by default. + +Conflict with `exclude_interface`. + +#### exclude_interface + +!!! warning "" + + When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`). + +Exclude interfaces in route. + +Conflict with `include_interface`. + +#### include_uid + +!!! quote "" + + UID rules are only supported on Linux and require auto_route. + +Limit users in route. Not limited by default. + +#### include_uid_range + +Limit users in route, but in range. + +#### exclude_uid + +Exclude users in route. + +#### exclude_uid_range + +Exclude users in route, but in range. + +#### include_android_user + +!!! quote "" + + Android user and package rules are only supported on Android and require auto_route. + +Limit android users in route. + +| Common user | ID | +|--------------|----| +| Main | 0 | +| Work Profile | 10 | + +#### include_package + +Limit android packages in route. + +#### exclude_package + +Exclude android packages in route. + +#### include_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `auto_route` and `auto_redirect` enabled. + +Limit MAC addresses in route. Not limited by default. + +Conflict with `exclude_mac_address`. + +#### exclude_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `auto_route` and `auto_redirect` enabled. + +Exclude MAC addresses in route. + +Conflict with `include_mac_address`. + +#### platform + +Platform-specific settings, provided by client applications. + +#### platform.http_proxy + +System HTTP proxy settings. + +#### platform.http_proxy.enabled + +Enable system HTTP proxy. + +#### platform.http_proxy.server + +==Required== + +HTTP proxy server address. + +#### platform.http_proxy.server_port + +==Required== + +HTTP proxy server port. + +#### platform.http_proxy.bypass_domain + +!!! note "" + + On Apple platforms, `bypass_domain` items matches hostname **suffixes**. + +Hostnames that bypass the HTTP proxy. + +#### platform.http_proxy.match_domain + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Hostnames that use the HTTP proxy. + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md new file mode 100644 index 00000000..a41e5ae9 --- /dev/null +++ b/docs/configuration/inbound/tun.zh.md @@ -0,0 +1,623 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [include_mac_address](#include_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + +!!! quote "sing-box 1.13.3 中的更改" + + :material-alert: [strict_route](#strict_route) + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) + :material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue) + :material-plus: [exclude_mptcp](#exclude_mptcp) + :material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [loopback_address](#loopback_address) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-alert: [gso](#gso) + :material-alert-decagram: [route_address_set](#stack) + :material-alert-decagram: [route_exclude_address_set](#stack) + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [address](#address) + :material-delete-clock: [inet4_address](#inet4_address) + :material-delete-clock: [inet6_address](#inet6_address) + :material-plus: [route_address](#route_address) + :material-delete-clock: [inet4_route_address](#inet4_route_address) + :material-delete-clock: [inet6_route_address](#inet6_route_address) + :material-plus: [route_exclude_address](#route_address) + :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [iproute2_table_index](#iproute2_table_index) + :material-plus: [iproute2_rule_index](#iproute2_table_index) + :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) + :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) + :material-plus: [route_address_set](#route_address_set) + :material-plus: [route_exclude_address_set](#route_address_set) + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) + :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [gso](#gso) + :material-alert-decagram: [stack](#stack) + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +### 结构 + +```json +{ + "type": "tun", + "tag": "tun-in", + "interface_name": "tun0", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "mtu": 9000, + "auto_route": true, + "iproute2_table_index": 2022, + "iproute2_rule_index": 9000, + "auto_redirect": true, + "auto_redirect_input_mark": "0x2023", + "auto_redirect_output_mark": "0x2024", + "auto_redirect_reset_mark": "0x2025", + "auto_redirect_nfqueue": 100, + "auto_redirect_iproute2_fallback_rule_index": 32768, + "exclude_mptcp": false, + "loopback_address": [ + "10.7.0.1" + ], + "strict_route": true, + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], + "endpoint_independent_nat": false, + "udp_timeout": "5m", + "stack": "system", + "include_interface": [ + "lan0" + ], + "exclude_interface": [ + "lan1" + ], + "include_uid": [ + 0 + ], + "include_uid_range": [ + "1000:99999" + ], + "exclude_uid": [ + 1000 + ], + "exclude_uid_range": [ + "1000:99999" + ], + "include_android_user": [ + 0, + 10 + ], + "include_package": [ + "com.android.chrome" + ], + "exclude_package": [ + "com.android.captiveportallogin" + ], + "include_mac_address": [ + "00:11:22:33:44:55" + ], + "exclude_mac_address": [ + "66:77:88:99:aa:bb" + ], + "platform": { + "http_proxy": { + "enabled": false, + "server": "127.0.0.1", + "server_port": 8080, + "bypass_domain": [], + "match_domain": [] + } + }, + + // 已弃用 + "gso": false, + "inet4_address": [ + "172.19.0.1/30" + ], + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ], + + ... // 监听字段 +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +!!! warning "" + + 如果 tun 在非特权模式下运行,地址和 MTU 将不会自动配置,请确保设置正确。 + +### Tun 字段 + +#### interface_name + +虚拟设备名称,默认自动选择。 + +#### address + +!!! question "自 sing-box 1.10.0 起" + +==必填== + +tun 接口的 IPv4 和 IPv6 前缀。 + +#### inet4_address + +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。 + +==必填== + +tun 接口的 IPv4 前缀。 + +#### inet6_address + +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。 + +tun 接口的 IPv6 前缀。 + +#### mtu + +最大传输单元。 + +#### gso + +!!! failure "已在 sing-box 1.11.0 废弃" + + GSO 对于透明代理场景没有优势,已废弃和不再生效,且将在 sing-box 1.12.0 中被移除。 + +!!! question "自 sing-box 1.8.0 起" + +!!! quote "" + + 仅支持 Linux。 + +启用通用分段卸载。 + +#### auto_route + +设置到 Tun 的默认路由。 + +!!! quote "" + + 为避免流量环回,请设置 `route.auto_detect_interface` 或 `route.default_interface` 或 `outbound.bind_interface`。 + +!!! note "与 Android VPN 一起使用" + + 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 起" + +`auto_route` 生成的 iproute2 路由表索引。 + +默认使用 `2022`。 + +#### iproute2_rule_index + +!!! question "自 sing-box 1.10.0 起" + +`auto_route` 生成的 iproute2 规则起始索引。 + +默认使用 `9000`。 + +#### auto_redirect + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `auto_route` 已启用。 + +通过使用 nftables 改善 TUN 路由和性能。 + +在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由、更高的性能(优于 tproxy),并避免了 TUN 和 Docker 桥接网络之间的冲突。 + +请注意,`auto_redirect` 也适用于 Android,但由于缺少 `nftables` 和 `ip6tables`,仅执行简单的 IPv4 TCP 转发。 +若要在 Android 上通过热点或中继器共享 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。 + +`auto_redirect` 还会自动将兼容性规则插入 OpenWrt 的 fw4 表中,即无需额外配置即可在路由器上工作。 + +与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + +#### auto_redirect_input_mark + +!!! question "自 sing-box 1.10.0 起" + +`auto_redirect` 使用的连接输入标记。 + +默认使用 `0x2023`。 + +#### auto_redirect_output_mark + +!!! question "自 sing-box 1.10.0 起" + +`auto_redirect` 使用的连接输出标记。 + +默认使用 `0x2024`。 + +#### auto_redirect_reset_mark + +!!! question "自 sing-box 1.13.0 起" + +`auto_redirect` 预匹配使用的连接重置标记。 + +默认使用 `0x2025`。 + +#### auto_redirect_nfqueue + +!!! question "自 sing-box 1.13.0 起" + +`auto_redirect` 预匹配使用的 NFQueue 编号。 + +默认使用 `100`。 + +#### auto_redirect_iproute2_fallback_rule_index + +!!! question "自 sing-box 1.12.18 起" + +`auto_redirect` 生成的 iproute2 回退规则索引。 + +此规则在系统默认规则(32766: main,32767: default)之后检查, +仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。 + +默认使用 `32768`。 + +#### exclude_mptcp + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + +由于协议限制,MPTCP 无法被透明代理。 + +此类流量通常由 Apple 系统创建。 + +启用时,MPTCP 连接将绕过 sing-box 直接连接,否则,将被拒绝以避免错误。 + +#### loopback_address + +!!! question "自 sing-box 1.12.0 起" + +环回地址是用于使指向指定地址的 TCP 连接连接到来源地址的。 + +将选项值设置为 `10.7.0.1` 可实现与 SideStore/StosVPN 相同的行为。 + +当启用 `auto_redirect` 时,可以作为网关为局域网设备(而不仅仅是本地)实现相同的行为。 + +#### strict_route + +当启用 `auto_route` 时,强制执行严格的路由规则: + +*在 Linux 中*: + +* 使不支持的网络不可达。 +* 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。 +* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量: + * 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。 + * 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。 + +*在 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 应用程序(如 VirtualBox)在某些情况下无法正常工作。 + +#### route_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的自定义路由。 + +#### inet4_route_address + +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。 + +启用 `auto_route` 时使用自定义路由而不是默认路由。 + +#### inet6_route_address + +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。 + +启用 `auto_route` 时使用自定义路由而不是默认路由。 + +#### route_exclude_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的排除自定义路由。 + +#### inet4_route_exclude_address + +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。 + +启用 `auto_route` 时排除自定义路由。 + +#### inet6_route_exclude_address + +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。 + +启用 `auto_route` 时排除自定义路由。 + +#### route_address_set + +=== "`auto_redirect` 已启用" + + !!! question "自 sing-box 1.10.0 起" + + !!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + + 将指定规则集中的目标 IP CIDR 规则添加到防火墙。 + 不匹配的流量将绕过 sing-box 路由。 + +=== "`auto_redirect` 未启用" + + !!! question "自 sing-box 1.11.0 起" + + 将指定规则集中的目标 IP CIDR 规则添加到路由,相当于添加到 `route_address`。 + 不匹配的流量将绕过 sing-box 路由。 + + 请注意,由于 Android VpnService 无法处理大量路由(DeadSystemException), + 因此它**在 Android 图形客户端上不起作用**,但除此之外,它在所有命令行客户端和 Apple 平台上都可以正常工作。 + +#### route_exclude_address_set + +=== "`auto_redirect` 已启用" + + !!! question "自 sing-box 1.10.0 起" + + !!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + + 将指定规则集中的目标 IP CIDR 规则添加到防火墙。 + 匹配的流量将绕过 sing-box 路由。 + + 与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + +=== "`auto_redirect` 未启用" + + !!! question "自 sing-box 1.11.0 起" + + 将指定规则集中的目标 IP CIDR 规则添加到路由,相当于添加到 `route_exclude_address`。 + 匹配的流量将绕过 sing-box 路由。 + + 请注意,由于 Android VpnService 无法处理大量路由(DeadSystemException), + 因此它**在 Android 图形客户端上不起作用**,但除此之外,它在所有命令行客户端和 Apple 平台上都可以正常工作。 + +#### endpoint_independent_nat + +启用独立于端点的 NAT。 + +性能可能会略有下降,所以不建议在不需要的时候开启。 + +#### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +#### stack + +!!! quote "sing-box 1.8.0 中的更改" + + :material-delete-alert: 旧的 LWIP 栈已被弃用并移除。 + +TCP/IP 栈。 + +| 栈 | 描述 | +|----------|-------------------------------------------------------------------------------------------------------| +| `system` | 基于系统网络栈执行 L3 到 L4 转换 | +| `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 | +| `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 | + +默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。 + +#### include_interface + +!!! quote "" + + 接口规则仅在 Linux 下被支持,并且需要 `auto_route`。 + +限制被路由的接口。默认不限制。 + +与 `exclude_interface` 冲突。 + +#### exclude_interface + +!!! warning "" + + 当 `strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan` 与 `pppoe-wan`)。 + +排除路由的接口。 + +与 `include_interface` 冲突。 + +#### include_uid + +!!! quote "" + + UID 规则仅在 Linux 下被支持,并且需要 `auto_route`。 + +限制被路由的用户。默认不限制。 + +#### include_uid_range + +限制被路由的用户范围。 + +#### exclude_uid + +排除路由的用户。 + +#### exclude_uid_range + +排除路由的用户范围。 + +#### include_android_user + +!!! quote "" + + Android 用户和应用规则仅在 Android 下被支持,并且需要 `auto_route`。 + +限制被路由的 Android 用户。 + +| 常用用户 | ID | +|------|----| +| 您 | 0 | +| 工作资料 | 10 | + +#### include_package + +限制被路由的 Android 应用包名。 + +#### exclude_package + +排除路由的 Android 应用包名。 + +#### include_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。 + +限制被路由的 MAC 地址。默认不限制。 + +与 `exclude_mac_address` 冲突。 + +#### exclude_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。 + +排除路由的 MAC 地址。 + +与 `include_mac_address` 冲突。 + +#### platform + +平台特定的设置,由客户端应用提供。 + +#### platform.http_proxy + +系统 HTTP 代理设置。 + +##### platform.http_proxy.enabled + +启用系统 HTTP 代理。 + +##### platform.http_proxy.server + +==必填== + +系统 HTTP 代理服务器地址。 + +##### platform.http_proxy.server_port + +==必填== + +系统 HTTP 代理服务器端口。 + +##### platform.http_proxy.bypass_domain + +!!! note "" + + 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. + +绕过代理的主机名列表。 + +##### platform.http_proxy.match_domain + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +代理的主机名列表。 + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/vless.md b/docs/configuration/inbound/vless.md new file mode 100644 index 00000000..93faf716 --- /dev/null +++ b/docs/configuration/inbound/vless.md @@ -0,0 +1,59 @@ +### Structure + +```json +{ + "type": "vless", + "tag": "vless-in", + + ... // Listen Fields + + "users": [ + { + "name": "sekai", + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "flow": "" + } + ], + "tls": {}, + "multiplex": {}, + "transport": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +==Required== + +VLESS users. + +#### users.uuid + +==Required== + +VLESS user id. + +#### users.flow + +VLESS Sub-protocol. + +Available values: + +* `xtls-rprx-vision` + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). diff --git a/docs/configuration/inbound/vless.zh.md b/docs/configuration/inbound/vless.zh.md new file mode 100644 index 00000000..2ce4785b --- /dev/null +++ b/docs/configuration/inbound/vless.zh.md @@ -0,0 +1,59 @@ +### 结构 + +```json +{ + "type": "vless", + "tag": "vless-in", + + ... // 监听字段 + + "users": [ + { + "name": "sekai", + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "flow": "" + } + ], + "tls": {}, + "multiplex": {}, + "transport": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +==必填== + +VLESS 用户。 + +#### users.uuid + +==必填== + +VLESS 用户 ID。 + +#### users.flow + +VLESS 子协议。 + +可用值: + +* `xtls-rprx-vision` + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 diff --git a/docs/configuration/inbound/vmess.md b/docs/configuration/inbound/vmess.md new file mode 100644 index 00000000..f38a6cae --- /dev/null +++ b/docs/configuration/inbound/vmess.md @@ -0,0 +1,54 @@ +### Structure + +```json +{ + "type": "vmess", + "tag": "vmess-in", + + ... // Listen Fields + + "users": [ + { + "name": "sekai", + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "alterId": 0 + } + ], + "tls": {}, + "multiplex": {}, + "transport": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### users + +==Required== + +VMess users. + +| Alter ID | Description | +|----------|-------------------------| +| 0 | Disable legacy protocol | +| > 0 | Enable legacy protocol | + +!!! warning "" + + Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). diff --git a/docs/configuration/inbound/vmess.zh.md b/docs/configuration/inbound/vmess.zh.md new file mode 100644 index 00000000..f741ed1b --- /dev/null +++ b/docs/configuration/inbound/vmess.zh.md @@ -0,0 +1,54 @@ +### 结构 + +```json +{ + "type": "vmess", + "tag": "vmess-in", + + ... // 监听字段 + + "users": [ + { + "name": "sekai", + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "alterId": 0 + } + ], + "tls": {}, + "multiplex": {}, + "transport": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### users + +==必填== + +VMess 用户。 + +| Alter ID | 描述 | +|----------|-------| +| 0 | 禁用旧协议 | +| > 0 | 启用旧协议 | + +!!! warning "" + + 提供旧协议支持(VMess MD5 身份验证)仅出于兼容性目的,不建议使用 alterId > 1。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 diff --git a/docs/configuration/index.md b/docs/configuration/index.md new file mode 100644 index 00000000..81cb8f38 --- /dev/null +++ b/docs/configuration/index.md @@ -0,0 +1,54 @@ +# Introduction + +sing-box uses JSON for configuration files. +### Structure + +```json +{ + "log": {}, + "dns": {}, + "ntp": {}, + "certificate": {}, + "certificate_providers": [], + "endpoints": [], + "inbounds": [], + "outbounds": [], + "route": {}, + "services": [], + "experimental": {} +} +``` + +### Fields + +| Key | Format | +|----------------|---------------------------------| +| `log` | [Log](./log/) | +| `dns` | [DNS](./dns/) | +| `ntp` | [NTP](./ntp/) | +| `certificate` | [Certificate](./certificate/) | +| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) | +| `endpoints` | [Endpoint](./endpoint/) | +| `inbounds` | [Inbound](./inbound/) | +| `outbounds` | [Outbound](./outbound/) | +| `route` | [Route](./route/) | +| `services` | [Service](./service/) | +| `experimental` | [Experimental](./experimental/) | + +### Check + +```bash +sing-box check +``` + +### Format + +```bash +sing-box format -w -c config.json -D config_directory +``` + +### Merge + +```bash +sing-box merge output.json -c config.json -D config_directory +``` diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md new file mode 100644 index 00000000..350db5d4 --- /dev/null +++ b/docs/configuration/index.zh.md @@ -0,0 +1,54 @@ +# 引言 + +sing-box 使用 JSON 作为配置文件格式。 +### 结构 + +```json +{ + "log": {}, + "dns": {}, + "ntp": {}, + "certificate": {}, + "certificate_providers": [], + "endpoints": [], + "inbounds": [], + "outbounds": [], + "route": {}, + "services": [], + "experimental": {} +} +``` + +### 字段 + +| Key | Format | +|----------------|------------------------| +| `log` | [日志](./log/) | +| `dns` | [DNS](./dns/) | +| `ntp` | [NTP](./ntp/) | +| `certificate` | [证书](./certificate/) | +| `certificate_providers` | [证书提供者](./shared/certificate-provider/) | +| `endpoints` | [端点](./endpoint/) | +| `inbounds` | [入站](./inbound/) | +| `outbounds` | [出站](./outbound/) | +| `route` | [路由](./route/) | +| `services` | [服务](./service/) | +| `experimental` | [实验性](./experimental/) | + +### 检查 + +```bash +sing-box check +``` + +### 格式化 + +```bash +sing-box format -w -c config.json -D config_directory +``` + +### 合并 + +```bash +sing-box merge output.json -c config.json -D config_directory +``` diff --git a/docs/configuration/log/index.md b/docs/configuration/log/index.md new file mode 100644 index 00000000..ff45e165 --- /dev/null +++ b/docs/configuration/log/index.md @@ -0,0 +1,33 @@ +# Log + +### Structure + +```json +{ + "log": { + "disabled": false, + "level": "info", + "output": "box.log", + "timestamp": true + } +} + +``` + +### Fields + +#### disabled + +Disable logging, no output after start. + +#### level + +Log level. One of: `trace` `debug` `info` `warn` `error` `fatal` `panic`. + +#### output + +Output file path. Will not write log to console after enable. + +#### timestamp + +Add time to each line. \ No newline at end of file diff --git a/docs/configuration/log/index.zh.md b/docs/configuration/log/index.zh.md new file mode 100644 index 00000000..faaa82e7 --- /dev/null +++ b/docs/configuration/log/index.zh.md @@ -0,0 +1,33 @@ +# 日志 + +### 结构 + +```json +{ + "log": { + "disabled": false, + "level": "info", + "output": "box.log", + "timestamp": true + } +} + +``` + +### 字段 + +#### disabled + +禁用日志,启动后不输出日志。 + +#### level + +日志等级,可选值:`trace` `debug` `info` `warn` `error` `fatal` `panic`。 + +#### output + +输出文件路径,启动后将不输出到控制台。 + +#### timestamp + +添加时间到每行。 \ No newline at end of file diff --git a/docs/configuration/ntp/index.md b/docs/configuration/ntp/index.md new file mode 100644 index 00000000..b95b9b1f --- /dev/null +++ b/docs/configuration/ntp/index.md @@ -0,0 +1,50 @@ +# NTP + +Built-in NTP client service. + +If enabled, it will provide time for protocols like TLS/Shadowsocks/VMess, which is useful for environments where time +synchronization is not possible. + +### Structure + +```json +{ + "ntp": { + "enabled": false, + "server": "time.apple.com", + "server_port": 123, + "interval": "30m", + + ... // Dial Fields + } +} + +``` + +### Fields + +#### enabled + +Enable NTP service. + +#### server + +==Required== + +NTP server address. + +#### server_port + +NTP server port. + +123 is used by default. + +#### interval + +Time synchronization interval. + +30 minutes is used by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. \ No newline at end of file diff --git a/docs/configuration/ntp/index.zh.md b/docs/configuration/ntp/index.zh.md new file mode 100644 index 00000000..cab1eb67 --- /dev/null +++ b/docs/configuration/ntp/index.zh.md @@ -0,0 +1,49 @@ +# NTP + +内建的 NTP 客户端服务。 + +如果启用,它将为像 TLS/Shadowsocks/VMess 这样的协议提供时间,这对于无法进行时间同步的环境很有用。 + +### 结构 + +```json +{ + "ntp": { + "enabled": false, + "server": "time.apple.com", + "server_port": 123, + "interval": "30m", + + ... // 拨号字段 + } +} + +``` + +### 字段 + +#### enabled + +启用 NTP 服务。 + +#### server + +==必填== + +NTP 服务器地址。 + +#### server_port + +NTP 服务器端口。 + +默认使用 123。 + +#### interval + +时间同步间隔。 + +默认使用 30 分钟。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/anytls.md b/docs/configuration/outbound/anytls.md new file mode 100644 index 00000000..4b3e0371 --- /dev/null +++ b/docs/configuration/outbound/anytls.md @@ -0,0 +1,66 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +### Structure + +```json +{ + "type": "anytls", + "tag": "anytls-out", + + "server": "127.0.0.1", + "server_port": 1080, + "password": "8JCsPssfgS8tiRwiMlhARg==", + "idle_session_check_interval": "30s", + "idle_session_timeout": "30s", + "min_idle_session": 5, + "tls": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### password + +==Required== + +The AnyTLS password. + +#### idle_session_check_interval + +Interval checking for idle sessions. Default: 30s. + +#### idle_session_timeout + +In the check, close sessions that have been idle for longer than this. Default: 30s. + +#### min_idle_session + +In the check, at least the first `n` idle sessions are kept open. Default value: `n`=0 + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/anytls.zh.md b/docs/configuration/outbound/anytls.zh.md new file mode 100644 index 00000000..c1f8999e --- /dev/null +++ b/docs/configuration/outbound/anytls.zh.md @@ -0,0 +1,66 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +### 结构 + +```json +{ + "type": "anytls", + "tag": "anytls-out", + + "server": "127.0.0.1", + "server_port": 1080, + "password": "8JCsPssfgS8tiRwiMlhARg==", + "idle_session_check_interval": "30s", + "idle_session_timeout": "30s", + "min_idle_session": 5, + "tls": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### password + +==必填== + +AnyTLS 密码。 + +#### idle_session_check_interval + +检查空闲会话的时间间隔。默认值:30秒。 + +#### idle_session_timeout + +在检查中,关闭闲置时间超过此值的会话。默认值:30秒。 + +#### min_idle_session + +在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/block.md b/docs/configuration/outbound/block.md new file mode 100644 index 00000000..6e232263 --- /dev/null +++ b/docs/configuration/outbound/block.md @@ -0,0 +1,16 @@ +--- +icon: material/delete-clock +--- + +### Structure + +```json +{ + "type": "block", + "tag": "block" +} +``` + +### Fields + +No fields. diff --git a/docs/configuration/outbound/block.zh.md b/docs/configuration/outbound/block.zh.md new file mode 100644 index 00000000..93227476 --- /dev/null +++ b/docs/configuration/outbound/block.zh.md @@ -0,0 +1,18 @@ +--- +icon: material/delete-clock +--- + +`block` 出站关闭所有传入请求。 + +### 结构 + +```json +{ + "type": "block", + "tag": "block" +} +``` + +### 字段 + +无字段。 diff --git a/docs/configuration/outbound/direct.md b/docs/configuration/outbound/direct.md new file mode 100644 index 00000000..3e28db8f --- /dev/null +++ b/docs/configuration/outbound/direct.md @@ -0,0 +1,48 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-clock: [override_address](#override_address) + :material-delete-clock: [override_port](#override_port) + +`direct` outbound send requests directly. + +### Structure + +```json +{ + "type": "direct", + "tag": "direct-out", + + "override_address": "1.0.0.1", + "override_port": 53, + + ... // Dial Fields +} +``` + +### Fields + +#### override_address + +!!! failure "Deprecated in sing-box 1.11.0" + + Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options). + +Override the connection destination address. + +#### override_port + +!!! failure "Deprecated in sing-box 1.11.0" + + Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options). + +Override the connection destination port. + +Protocol value can be `1` or `2`. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/direct.zh.md b/docs/configuration/outbound/direct.zh.md new file mode 100644 index 00000000..824a3529 --- /dev/null +++ b/docs/configuration/outbound/direct.zh.md @@ -0,0 +1,46 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-clock: [override_address](#override_address) + :material-delete-clock: [override_port](#override_port) + +`direct` 出站直接发送请求。 + +### 结构 + +```json +{ + "type": "direct", + "tag": "direct-out", + + "override_address": "1.0.0.1", + "override_port": 53, + + ... // 拨号字段 +} +``` + +### 字段 + +#### override_address + +!!! failure "已在 sing-box 1.11.0 废弃" + + 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 + +覆盖连接目标地址。 + +#### override_port + +!!! failure "已在 sing-box 1.11.0 废弃" + + 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 + +覆盖连接目标端口。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/dns.md b/docs/configuration/outbound/dns.md new file mode 100644 index 00000000..d7336041 --- /dev/null +++ b/docs/configuration/outbound/dns.md @@ -0,0 +1,26 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.11.0" + + Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions). + +`dns` outbound is a internal DNS server. + +### Structure + +```json +{ + "type": "dns", + "tag": "dns-out" +} +``` + +!!! note "" + + There are no outbound connections by the DNS outbound, all requests are handled internally. + +### Fields + +No fields. \ No newline at end of file diff --git a/docs/configuration/outbound/dns.zh.md b/docs/configuration/outbound/dns.zh.md new file mode 100644 index 00000000..592075b3 --- /dev/null +++ b/docs/configuration/outbound/dns.zh.md @@ -0,0 +1,26 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.11.0 废弃" + + 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作). + +`dns` 出站是一个内部 DNS 服务器。 + +### 结构 + +```json +{ + "type": "dns", + "tag": "dns-out" +} +``` + +!!! note "" + + DNS 出站没有出站连接,所有请求均在内部处理。 + +### 字段 + +无字段。 \ No newline at end of file diff --git a/docs/configuration/outbound/http.md b/docs/configuration/outbound/http.md new file mode 100644 index 00000000..0b9dfa23 --- /dev/null +++ b/docs/configuration/outbound/http.md @@ -0,0 +1,58 @@ +`http` outbound is a HTTP CONNECT proxy client. + +### Structure + +```json +{ + "type": "http", + "tag": "http-out", + + "server": "127.0.0.1", + "server_port": 1080, + "username": "sekai", + "password": "admin", + "path": "", + "headers": {}, + "tls": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### username + +Basic authorization username. + +#### password + +Basic authorization password. + +#### path + +Path of HTTP request. + +#### headers + +Extra headers of HTTP request. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/http.zh.md b/docs/configuration/outbound/http.zh.md new file mode 100644 index 00000000..55387a63 --- /dev/null +++ b/docs/configuration/outbound/http.zh.md @@ -0,0 +1,58 @@ +`http` 出站是一个 HTTP CONNECT 代理客户端 + +### 结构 + +```json +{ + "type": "http", + "tag": "http-out", + + "server": "127.0.0.1", + "server_port": 1080, + "username": "sekai", + "password": "admin", + "path": "", + "headers": {}, + "tls": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### username + +Basic 认证用户名。 + +#### password + +Basic 认证密码。 + +#### path + +HTTP 请求路径。 + +#### headers + +HTTP 请求的额外标头。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md new file mode 100644 index 00000000..b326e06d --- /dev/null +++ b/docs/configuration/outbound/hysteria.md @@ -0,0 +1,141 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [server_ports](#server_ports) + :material-plus: [hop_interval](#hop_interval) + +### Structure + +```json +{ + "type": "hysteria", + "tag": "hysteria-out", + + "server": "127.0.0.1", + "server_port": 1080, + "server_ports": [ + "2080:3000" + ], + "hop_interval": "", + "up": "100 Mbps", + "up_mbps": 100, + "down": "100 Mbps", + "down_mbps": 100, + "obfs": "fuck me till the daylight", + "auth": "", + "auth_str": "password", + "recv_window_conn": 0, + "recv_window": 0, + "disable_mtu_discovery": false, + "network": "tcp", + "tls": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### server_ports + +!!! question "Since sing-box 1.12.0" + +Server port range list. + +Conflicts with `server_port`. + +#### hop_interval + +!!! question "Since sing-box 1.12.0" + +Port hopping interval. + +`30s` is used by default. + +#### up, down + +==Required== + +Format: `[Integer] [Unit]` e.g. `100 Mbps, 640 KBps, 2 Gbps` + +Supported units (case sensitive, b = bits, B = bytes, 8b=1B): + + bps (bits per second) + Bps (bytes per second) + Kbps (kilobits per second) + KBps (kilobytes per second) + Mbps (megabits per second) + MBps (megabytes per second) + Gbps (gigabits per second) + GBps (gigabytes per second) + Tbps (terabits per second) + TBps (terabytes per second) + +#### up_mbps, down_mbps + +==Required== + +`up, down` in Mbps. + +#### obfs + +Obfuscated password. + +#### auth + +Authentication password, in base64. + +#### auth_str + +Authentication password. + +#### recv_window_conn + +The QUIC stream-level flow control window for receiving data. + +`15728640 (15 MB/s)` will be used if empty. + +#### recv_window + +The QUIC connection-level flow control window for receiving data. + +`67108864 (64 MB/s)` will be used if empty. + +#### disable_mtu_discovery + +Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. + +Force enabled on for systems other than Linux and Windows (according to upstream). + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/hysteria.zh.md b/docs/configuration/outbound/hysteria.zh.md new file mode 100644 index 00000000..ae1d3590 --- /dev/null +++ b/docs/configuration/outbound/hysteria.zh.md @@ -0,0 +1,142 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [server_ports](#server_ports) + :material-plus: [hop_interval](#hop_interval) + +### 结构 + +```json +{ + "type": "hysteria", + "tag": "hysteria-out", + + "server": "127.0.0.1", + "server_port": 1080, + "server_ports": [ + "2080:3000" + ], + "hop_interval": "", + "up": "100 Mbps", + "up_mbps": 100, + "down": "100 Mbps", + "down_mbps": 100, + "obfs": "fuck me till the daylight", + "auth": "", + "auth_str": "password", + "recv_window_conn": 0, + "recv_window": 0, + "disable_mtu_discovery": false, + "network": "tcp", + "tls": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### server_ports + +!!! question "自 sing-box 1.12.0 起" + +服务器端口范围列表。 + +与 `server_port` 冲突。 + +#### hop_interval + +!!! question "自 sing-box 1.12.0 起" + +端口跳跃间隔。 + +默认使用 `30s`。 + +#### up, down + +==必填== + +格式: `[Integer] [Unit]` 例如: `100 Mbps, 640 KBps, 2 Gbps` + +支持的单位 (大小写敏感, b = bits, B = bytes, 8b=1B): + + bps (bits per second) + Bps (bytes per second) + Kbps (kilobits per second) + KBps (kilobytes per second) + Mbps (megabits per second) + MBps (megabytes per second) + Gbps (gigabits per second) + GBps (gigabytes per second) + Tbps (terabits per second) + TBps (terabytes per second) + +#### up_mbps, down_mbps + +==必填== + +以 Mbps 为单位的 `up, down`。 + +#### obfs + +混淆密码。 + +#### auth + +base64 编码的认证密码。 + +#### auth_str + +认证密码。 + +#### recv_window_conn + +用于接收数据的 QUIC 流级流控制窗口。 + +默认 `15728640 (15 MB/s)`。 + +#### recv_window + +用于接收数据的 QUIC 连接级流控制窗口。 + +默认 `67108864 (64 MB/s)`。 + +#### disable_mtu_discovery + +禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。 + +强制为 Linux 和 Windows 以外的系统启用(根据上游)。 + +#### network + +启用的网络协议。 + +`tcp` 或 `udp`。 + +默认所有。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md new file mode 100644 index 00000000..a71dd1e0 --- /dev/null +++ b/docs/configuration/outbound/hysteria2.md @@ -0,0 +1,141 @@ +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [hop_interval_max](#hop_interval_max) + :material-plus: [bbr_profile](#bbr_profile) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [server_ports](#server_ports) + :material-plus: [hop_interval](#hop_interval) + +### Structure + +```json +{ + "type": "hysteria2", + "tag": "hy2-out", + + "server": "127.0.0.1", + "server_port": 1080, + "server_ports": [ + "2080:3000" + ], + "hop_interval": "", + "hop_interval_max": "", + "up_mbps": 100, + "down_mbps": 100, + "obfs": { + "type": "salamander", + "password": "cry_me_a_r1ver" + }, + "password": "goofy_ahh_password", + "network": "tcp", + "tls": {}, + "bbr_profile": "", + "brutal_debug": false, + + ... // Dial Fields +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +!!! warning "Difference from official Hysteria2" + + The official Hysteria2 supports an authentication method called **userpass**, + which essentially uses a combination of `:` as the actual password, + while sing-box does not provide this alias. + If you are planning to use sing-box with the official program, + please note that you will need to fill the combination as the password. + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +Ignored if `server_ports` is set. + +#### server_ports + +!!! question "Since sing-box 1.11.0" + +Server port range list. + +Conflicts with `server_port`. + +#### hop_interval + +!!! question "Since sing-box 1.11.0" + +Port hopping interval. + +`30s` is used by default. + +#### hop_interval_max + +!!! question "Since sing-box 1.14.0" + +Maximum port hopping interval, used for randomization. + +If set, the actual hop interval will be randomly chosen between `hop_interval` and `hop_interval_max`. + +#### up_mbps, down_mbps + +Max bandwidth, in Mbps. + +If empty, the BBR congestion control algorithm will be used instead of Hysteria CC. + +#### obfs.type + +QUIC traffic obfuscator type, only available with `salamander`. + +Disabled if empty. + +#### obfs.password + +QUIC traffic obfuscator password. + +#### password + +Authentication password. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +#### bbr_profile + +!!! question "Since sing-box 1.14.0" + +BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`. + +`standard` is used by default. + +#### brutal_debug + +Enable debug information logging for Hysteria Brutal CC. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md new file mode 100644 index 00000000..0fb17bbd --- /dev/null +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -0,0 +1,139 @@ +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [hop_interval_max](#hop_interval_max) + :material-plus: [bbr_profile](#bbr_profile) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [server_ports](#server_ports) + :material-plus: [hop_interval](#hop_interval) + +### 结构 + +```json +{ + "type": "hysteria2", + "tag": "hy2-out", + + "server": "127.0.0.1", + "server_port": 1080, + "server_ports": [ + "2080:3000" + ], + "hop_interval": "", + "hop_interval_max": "", + "up_mbps": 100, + "down_mbps": 100, + "obfs": { + "type": "salamander", + "password": "cry_me_a_r1ver" + }, + "password": "goofy_ahh_password", + "network": "tcp", + "tls": {}, + "bbr_profile": "", + "brutal_debug": false, + + ... // 拨号字段 +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +!!! warning "与官方 Hysteria2 的区别" + + 官方程序支持一种名为 **userpass** 的验证方式, + 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 + 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +如果设置了 `server_ports`,则忽略此项。 + +#### server_ports + +!!! question "自 sing-box 1.11.0 起" + +服务器端口范围列表。 + +与 `server_port` 冲突。 + +#### hop_interval + +!!! question "自 sing-box 1.11.0 起" + +端口跳跃间隔。 + +默认使用 `30s`。 + +#### hop_interval_max + +!!! question "自 sing-box 1.14.0 起" + +最大端口跳跃间隔,用于随机化。 + +如果设置,实际跳跃间隔将在 `hop_interval` 和 `hop_interval_max` 之间随机选择。 + +#### up_mbps, down_mbps + +最大带宽。 + +如果为空,将使用 BBR 拥塞控制算法而不是 Hysteria CC。 + +#### obfs.type + +QUIC 流量混淆器类型,仅可设为 `salamander`。 + +如果为空则禁用。 + +#### obfs.password + +QUIC 流量混淆器密码. + +#### password + +认证密码。 + +#### network + +启用的网络协议。 + +`tcp` 或 `udp`。 + +默认所有。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +#### bbr_profile + +!!! question "自 sing-box 1.14.0 起" + +BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 + +默认使用 `standard`。 + +#### brutal_debug + +启用 Hysteria Brutal CC 的调试信息日志记录。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/index.md b/docs/configuration/outbound/index.md new file mode 100644 index 00000000..47b8a96a --- /dev/null +++ b/docs/configuration/outbound/index.md @@ -0,0 +1,49 @@ +# Outbound + +### Structure + +```json +{ + "outbounds": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | +|----------------|--------------------------------| +| `direct` | [Direct](./direct/) | +| `block` | [Block](./block/) | +| `socks` | [SOCKS](./socks/) | +| `http` | [HTTP](./http/) | +| `shadowsocks` | [Shadowsocks](./shadowsocks/) | +| `vmess` | [VMess](./vmess/) | +| `trojan` | [Trojan](./trojan/) | +| `wireguard` | [Wireguard](./wireguard/) | +| `hysteria` | [Hysteria](./hysteria/) | +| `vless` | [VLESS](./vless/) | +| `shadowtls` | [ShadowTLS](./shadowtls/) | +| `tuic` | [TUIC](./tuic/) | +| `hysteria2` | [Hysteria2](./hysteria2/) | +| `anytls` | [AnyTLS](./anytls/) | +| `tor` | [Tor](./tor/) | +| `ssh` | [SSH](./ssh/) | +| `dns` | [DNS](./dns/) | +| `selector` | [Selector](./selector/) | +| `urltest` | [URLTest](./urltest/) | +| `naive` | [NaiveProxy](./naive/) | + +#### tag + +The tag of the outbound. + +### Features + +#### Outbounds that support IP connection + +* `WireGuard` diff --git a/docs/configuration/outbound/index.zh.md b/docs/configuration/outbound/index.zh.md new file mode 100644 index 00000000..a1c4a7ad --- /dev/null +++ b/docs/configuration/outbound/index.zh.md @@ -0,0 +1,49 @@ +# 出站 + +### 结构 + +```json +{ + "outbounds": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | +|----------------|--------------------------------| +| `direct` | [Direct](./direct/) | +| `block` | [Block](./block/) | +| `socks` | [SOCKS](./socks/) | +| `http` | [HTTP](./http/) | +| `shadowsocks` | [Shadowsocks](./shadowsocks/) | +| `vmess` | [VMess](./vmess/) | +| `trojan` | [Trojan](./trojan/) | +| `wireguard` | [Wireguard](./wireguard/) | +| `hysteria` | [Hysteria](./hysteria/) | +| `vless` | [VLESS](./vless/) | +| `shadowtls` | [ShadowTLS](./shadowtls/) | +| `tuic` | [TUIC](./tuic/) | +| `hysteria2` | [Hysteria2](./hysteria2/) | +| `anytls` | [AnyTLS](./anytls/) | +| `tor` | [Tor](./tor/) | +| `ssh` | [SSH](./ssh/) | +| `dns` | [DNS](./dns/) | +| `selector` | [Selector](./selector/) | +| `urltest` | [URLTest](./urltest/) | +| `naive` | [NaiveProxy](./naive/) | + +#### tag + +出站的标签。 + +### 特性 + +#### 支持 IP 连接的出站 + +* `WireGuard` diff --git a/docs/configuration/outbound/naive.md b/docs/configuration/outbound/naive.md new file mode 100644 index 00000000..5ed9d2b8 --- /dev/null +++ b/docs/configuration/outbound/naive.md @@ -0,0 +1,116 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.13.0" + +### Structure + +```json +{ + "type": "naive", + "tag": "naive-out", + + "server": "127.0.0.1", + "server_port": 443, + "username": "sekai", + "password": "password", + "insecure_concurrency": 0, + "extra_headers": {}, + "udp_over_tcp": false | {}, + "quic": false, + "quic_congestion_control": "", + "tls": {}, + + ... // Dial Fields +} +``` + +!!! warning "Platform Support" + + NaiveProxy outbound is only available on Apple platforms, Android, Windows and certain Linux builds. + + **Official Release Build Variants:** + + | Build Variant | Platforms | Description | + |---------------|-----------|-------------| + | (no suffix) | Linux amd64/arm64 | purego build, `libcronet.so` included | + | `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO build, dynamically linked with glibc, requires glibc >= 2.31 (loong64: >= 2.36) | + | `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO build, statically linked with musl | + | (no suffix) | Windows amd64/arm64 | purego build, `libcronet.dll` included | + + For Linux, choose the glibc or musl variant based on your distribution's libc type. + + **Runtime Requirements:** + + - **Linux purego**: `libcronet.so` must be in the same directory as the sing-box binary or in system library path + - **Windows**: `libcronet.dll` must be in the same directory as `sing-box.exe` or in a directory listed in `PATH` + + For self-built binaries, see [Build from source](/installation/build-from-source/#with_naive_outbound). + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### username + +Authentication username. + +#### password + +Authentication password. + +#### insecure_concurrency + +Number of concurrent tunnel connections. Multiple connections make the tunneling easier to detect through traffic analysis, which defeats the purpose of NaiveProxy's design to resist traffic analysis. + +#### extra_headers + +Extra headers to send in HTTP requests. + +#### udp_over_tcp + +UDP over TCP protocol settings. + +See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details. + +#### quic + +Use QUIC instead of HTTP/2. + +#### quic_congestion_control + +QUIC congestion control algorithm. + +| Algorithm | Description | +|-----------|-------------| +| `bbr` | BBR | +| `bbr2` | BBRv2 | +| `cubic` | CUBIC | +| `reno` | New Reno | + +`bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on). + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +Only `server_name`, `certificate`, `certificate_path` and `ech` are supported. + +Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis, and should not be used in production. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/naive.zh.md b/docs/configuration/outbound/naive.zh.md new file mode 100644 index 00000000..dbfd7fbf --- /dev/null +++ b/docs/configuration/outbound/naive.zh.md @@ -0,0 +1,116 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.13.0 起" + +### 结构 + +```json +{ + "type": "naive", + "tag": "naive-out", + + "server": "127.0.0.1", + "server_port": 443, + "username": "sekai", + "password": "password", + "insecure_concurrency": 0, + "extra_headers": {}, + "udp_over_tcp": false | {}, + "quic": false, + "quic_congestion_control": "", + "tls": {}, + + ... // 拨号字段 +} +``` + +!!! warning "平台支持" + + NaiveProxy 出站仅在 Apple 平台、Android、Windows 和特定 Linux 构建上可用。 + + **官方发布版本区别:** + + | 构建变体 | 平台 | 说明 | + |---|---|---| + | (无后缀) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` | + | `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO 构建,动态链接 glibc,要求 glibc >= 2.31(loong64: >= 2.36) | + | `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO 构建,静态链接 musl | + | (无后缀) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` | + + 对于 Linux,请根据发行版的 libc 类型选择 glibc 或 musl 变体。 + + **运行时要求:** + + - **Linux purego**:`libcronet.so` 必须位于 sing-box 二进制文件相同目录或系统库路径中 + - **Windows**:`libcronet.dll` 必须位于 `sing-box.exe` 相同目录或 `PATH` 中的任意目录 + + 自行构建请参阅 [从源代码构建](/zh/installation/build-from-source/#with_naive_outbound)。 + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### username + +认证用户名。 + +#### password + +认证密码。 + +#### insecure_concurrency + +并发隧道连接数。多连接使隧道更容易被流量分析检测,违背 NaiveProxy 抵抗流量分析的设计目的。 + +#### extra_headers + +HTTP 请求中发送的额外头部。 + +#### udp_over_tcp + +UDP over TCP 配置。 + +参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。 + +#### quic + +使用 QUIC 代替 HTTP/2。 + +#### quic_congestion_control + +QUIC 拥塞控制算法。 + +| 算法 | 描述 | +|------|------| +| `bbr` | BBR | +| `bbr2` | BBRv2 | +| `cubic` | CUBIC | +| `reno` | New Reno | + +默认使用 `bbr`(NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +只有 `server_name`、`certificate`、`certificate_path` 和 `ech` 是被支持的。 + +自签名证书会显著改变流量行为,违背了 NaiveProxy 旨在抵抗流量分析的设计初衷,不应该在生产环境中使用。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/selector.md b/docs/configuration/outbound/selector.md new file mode 100644 index 00000000..ee75358f --- /dev/null +++ b/docs/configuration/outbound/selector.md @@ -0,0 +1,38 @@ +### Structure + +```json +{ + "type": "selector", + "tag": "select", + + "outbounds": [ + "proxy-a", + "proxy-b", + "proxy-c" + ], + "default": "proxy-c", + "interrupt_exist_connections": false +} +``` + +!!! quote "" + + The selector can only be controlled through the [Clash API](/configuration/experimental#clash-api-fields) currently. + +### Fields + +#### outbounds + +==Required== + +List of outbound tags to select. + +#### default + +The default outbound tag. The first outbound will be used if empty. + +#### interrupt_exist_connections + +Interrupt existing connections when the selected outbound has changed. + +Only inbound connections are affected by this setting, internal connections will always be interrupted. diff --git a/docs/configuration/outbound/selector.zh.md b/docs/configuration/outbound/selector.zh.md new file mode 100644 index 00000000..520fb15c --- /dev/null +++ b/docs/configuration/outbound/selector.zh.md @@ -0,0 +1,38 @@ +### 结构 + +```json +{ + "type": "selector", + "tag": "select", + + "outbounds": [ + "proxy-a", + "proxy-b", + "proxy-c" + ], + "default": "proxy-c", + "interrupt_exist_connections": false +} +``` + +!!! quote "" + + 选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。 + +### 字段 + +#### outbounds + +==必填== + +用于选择的出站标签列表。 + +#### default + +默认的出站标签。默认使用第一个出站。 + +#### interrupt_exist_connections + +当选定的出站发生更改时,中断现有连接。 + +仅入站连接受此设置影响,内部连接将始终被中断。 \ No newline at end of file diff --git a/docs/configuration/outbound/shadowsocks.md b/docs/configuration/outbound/shadowsocks.md new file mode 100644 index 00000000..a088d271 --- /dev/null +++ b/docs/configuration/outbound/shadowsocks.md @@ -0,0 +1,102 @@ +### Structure + +```json +{ + "type": "shadowsocks", + "tag": "ss-out", + + "server": "127.0.0.1", + "server_port": 1080, + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "plugin": "", + "plugin_opts": "", + "network": "udp", + "udp_over_tcp": false | {}, + "multiplex": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### method + +==Required== + +Encryption methods: + +* `2022-blake3-aes-128-gcm` +* `2022-blake3-aes-256-gcm` +* `2022-blake3-chacha20-poly1305` +* `none` +* `aes-128-gcm` +* `aes-192-gcm` +* `aes-256-gcm` +* `chacha20-ietf-poly1305` +* `xchacha20-ietf-poly1305` + +Legacy encryption methods: + +* `aes-128-ctr` +* `aes-192-ctr` +* `aes-256-ctr` +* `aes-128-cfb` +* `aes-192-cfb` +* `aes-256-cfb` +* `rc4-md5` +* `chacha20-ietf` +* `xchacha20` + +#### password + +==Required== + +The shadowsocks password. + +#### plugin + +Shadowsocks SIP003 plugin, implemented in internal. + +Only `obfs-local` and `v2ray-plugin` are supported. + +#### plugin_opts + +Shadowsocks SIP003 plugin options. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### udp_over_tcp + +UDP over TCP configuration. + +See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details. + +Conflict with `multiplex`. + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#outbound) for details. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/shadowsocks.zh.md b/docs/configuration/outbound/shadowsocks.zh.md new file mode 100644 index 00000000..7b4ff560 --- /dev/null +++ b/docs/configuration/outbound/shadowsocks.zh.md @@ -0,0 +1,102 @@ +### 结构 + +```json +{ + "type": "shadowsocks", + "tag": "ss-out", + + "server": "127.0.0.1", + "server_port": 1080, + "method": "2022-blake3-aes-128-gcm", + "password": "8JCsPssfgS8tiRwiMlhARg==", + "plugin": "", + "plugin_opts": "", + "network": "udp", + "udp_over_tcp": false | {}, + "multiplex": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### method + +==必填== + +加密方法: + +* `2022-blake3-aes-128-gcm` +* `2022-blake3-aes-256-gcm` +* `2022-blake3-chacha20-poly1305` +* `none` +* `aes-128-gcm` +* `aes-192-gcm` +* `aes-256-gcm` +* `chacha20-ietf-poly1305` +* `xchacha20-ietf-poly1305` + +旧加密方法: + +* `aes-128-ctr` +* `aes-192-ctr` +* `aes-256-ctr` +* `aes-128-cfb` +* `aes-192-cfb` +* `aes-256-cfb` +* `rc4-md5` +* `chacha20-ietf` +* `xchacha20` + +#### password + +==必填== + +Shadowsocks 密码。 + +#### plugin + +Shadowsocks SIP003 插件,由内部实现。 + +仅支持 `obfs-local` 和 `v2ray-plugin`。 + +#### plugin_opts + +Shadowsocks SIP003 插件参数。 + +#### network + +启用的网络协议 + +`tcp` 或 `udp`。 + +默认所有。 + +#### udp_over_tcp + +UDP over TCP 配置。 + +参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。 + +与 `multiplex` 冲突。 + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/shadowtls.md b/docs/configuration/outbound/shadowtls.md new file mode 100644 index 00000000..a54391b5 --- /dev/null +++ b/docs/configuration/outbound/shadowtls.md @@ -0,0 +1,56 @@ +### Structure + +```json +{ + "type": "shadowtls", + "tag": "st-out", + + "server": "127.0.0.1", + "server_port": 1080, + "version": 3, + "password": "fuck me till the daylight", + "tls": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### version + +ShadowTLS protocol version. + +| Value | Protocol Version | +|---------------|-----------------------------------------------------------------------------------------| +| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | +| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | +| `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | + +#### password + +Set password. + +Only available in the ShadowTLS v2/v3 protocol. + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/shadowtls.zh.md b/docs/configuration/outbound/shadowtls.zh.md new file mode 100644 index 00000000..72a73d7d --- /dev/null +++ b/docs/configuration/outbound/shadowtls.zh.md @@ -0,0 +1,56 @@ +### 结构 + +```json +{ + "type": "shadowtls", + "tag": "st-out", + + "server": "127.0.0.1", + "server_port": 1080, + "version": 3, + "password": "fuck me till the daylight", + "tls": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### version + +ShadowTLS 协议版本。 + +| 值 | 协议版本 | +|---------------|-----------------------------------------------------------------------------------------| +| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | +| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | +| `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | + +#### password + +设置密码。 + +仅在 ShadowTLS v2/v3 协议中可用。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/socks.md b/docs/configuration/outbound/socks.md new file mode 100644 index 00000000..b04e67b6 --- /dev/null +++ b/docs/configuration/outbound/socks.md @@ -0,0 +1,66 @@ +`socks` outbound is a socks4/socks4a/socks5 client. + +### Structure + +```json +{ + "type": "socks", + "tag": "socks-out", + + "server": "127.0.0.1", + "server_port": 1080, + "version": "5", + "username": "sekai", + "password": "admin", + "network": "udp", + "udp_over_tcp": false | {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### version + +The SOCKS version, one of `4` `4a` `5`. + +SOCKS5 used by default. + +#### username + +SOCKS username. + +#### password + +SOCKS5 password. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### udp_over_tcp + +UDP over TCP protocol settings. + +See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/socks.zh.md b/docs/configuration/outbound/socks.zh.md new file mode 100644 index 00000000..dd9a1ac9 --- /dev/null +++ b/docs/configuration/outbound/socks.zh.md @@ -0,0 +1,66 @@ +`socks` 出站是 socks4/socks4a/socks5 客户端 + +### 结构 + +```json +{ + "type": "socks", + "tag": "socks-out", + + "server": "127.0.0.1", + "server_port": 1080, + "version": "5", + "username": "sekai", + "password": "admin", + "network": "udp", + "udp_over_tcp": false | {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### version + +SOCKS 版本, 可为 `4` `4a` `5`. + +默认使用 SOCKS5。 + +#### username + +SOCKS 用户名。 + +#### password + +SOCKS5 密码。 + +#### network + +启用的网络协议 + +`tcp` 或 `udp`。 + +默认所有。 + +#### udp_over_tcp + +UDP over TCP 配置。 + +参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/ssh.md b/docs/configuration/outbound/ssh.md new file mode 100644 index 00000000..45ec72b2 --- /dev/null +++ b/docs/configuration/outbound/ssh.md @@ -0,0 +1,71 @@ +### Structure + +```json +{ + "type": "ssh", + "tag": "ssh-out", + + "server": "127.0.0.1", + "server_port": 22, + "user": "root", + "password": "admin", + "private_key": "", + "private_key_path": "$HOME/.ssh/id_rsa", + "private_key_passphrase": "", + "host_key": [ + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdH..." + ], + "host_key_algorithms": [], + "client_version": "SSH-2.0-OpenSSH_7.4p1", + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +Server address. + +#### server_port + +Server port. 22 will be used if empty. + +#### user + +SSH user, root will be used if empty. + +#### password + +Password. + +#### private_key + +Private key. + +#### private_key_path + +Private key path. + +#### private_key_passphrase + +Private key passphrase. + +#### host_key + +Host key. Accept any if empty. + +#### host_key_algorithms + +Host key algorithms. + +#### client_version + +Client version. Random version will be used if empty. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/ssh.zh.md b/docs/configuration/outbound/ssh.zh.md new file mode 100644 index 00000000..e538e64a --- /dev/null +++ b/docs/configuration/outbound/ssh.zh.md @@ -0,0 +1,71 @@ +### 结构 + +```json +{ + "type": "ssh", + "tag": "ssh-out", + + "server": "127.0.0.1", + "server_port": 22, + "user": "root", + "password": "admin", + "private_key": "", + "private_key_path": "$HOME/.ssh/id_rsa", + "private_key_passphrase": "", + "host_key": [ + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdH..." + ], + "host_key_algorithms": [], + "client_version": "SSH-2.0-OpenSSH_7.4p1", + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +服务器端口,默认使用 22。 + +#### user + +SSH 用户, 默认使用 root。 + +#### password + +密码。 + +#### private_key + +密钥。 + +#### private_key_path + +密钥路径。 + +#### private_key_passphrase + +密钥密码。 + +#### host_key + +主机密钥,留空接受所有。 + +#### host_key_algorithms + +主机密钥算法。 + +#### client_version + +客户端版本,默认使用随机值。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md new file mode 100644 index 00000000..ac778335 --- /dev/null +++ b/docs/configuration/outbound/tor.md @@ -0,0 +1,51 @@ +### Structure + +```json +{ + "type": "tor", + "tag": "tor-out", + + "executable_path": "/usr/bin/tor", + "extra_args": [], + "data_directory": "$HOME/.cache/tor", + "torrc": { + "ClientOnly": 1 + }, + + ... // Dial Fields +} +``` + +!!! info "" + + Embedded Tor is not included by default, see [Installation](/installation/build-from-source/#build-tags). + +### Fields + +#### executable_path + +The path to the Tor executable. + +Embedded Tor will be ignored if set. + +#### extra_args + +List of extra arguments passed to the Tor instance when started. + +#### data_directory + +==Recommended== + +The data directory of Tor. + +Each start will be very slow if not specified. + +#### torrc + +Map of torrc options. + +See [tor(1)](https://linux.die.net/man/1/tor) for details. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/tor.zh.md b/docs/configuration/outbound/tor.zh.md new file mode 100644 index 00000000..a49eb323 --- /dev/null +++ b/docs/configuration/outbound/tor.zh.md @@ -0,0 +1,51 @@ +### 结构 + +```json +{ + "type": "tor", + "tag": "tor-out", + + "executable_path": "/usr/bin/tor", + "extra_args": [], + "data_directory": "$HOME/.cache/tor", + "torrc": { + "ClientOnly": 1 + }, + + ... // 拨号字段 +} +``` + +!!! info "" + + 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。 + +### 字段 + +#### executable_path + +Tor 可执行文件路径 + +如果设置,将覆盖嵌入式 Tor。 + +#### extra_args + +启动 Tor 时传递的附加参数列表。 + +#### data_directory + +==推荐== + +Tor 的数据目录。 + +如未设置,每次启动都需要长时间。 + +#### torrc + +torrc 参数表。 + +参阅 [tor(1)](https://linux.die.net/man/1/tor)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/trojan.md b/docs/configuration/outbound/trojan.md new file mode 100644 index 00000000..6a45fd02 --- /dev/null +++ b/docs/configuration/outbound/trojan.md @@ -0,0 +1,62 @@ +### Structure + +```json +{ + "type": "trojan", + "tag": "trojan-out", + + "server": "127.0.0.1", + "server_port": 1080, + "password": "8JCsPssfgS8tiRwiMlhARg==", + "network": "tcp", + "tls": {}, + "multiplex": {}, + "transport": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### password + +==Required== + +The Trojan password. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#outbound) for details. + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/trojan.zh.md b/docs/configuration/outbound/trojan.zh.md new file mode 100644 index 00000000..8a78ca2d --- /dev/null +++ b/docs/configuration/outbound/trojan.zh.md @@ -0,0 +1,62 @@ +### 结构 + +```json +{ + "type": "trojan", + "tag": "trojan-out", + + "server": "127.0.0.1", + "server_port": 1080, + "password": "8JCsPssfgS8tiRwiMlhARg==", + "network": "tcp", + "tls": {}, + "multiplex": {}, + "transport": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### password + +==必填== + +Trojan 密码。 + +#### network + +启用的网络协议。 + +`tcp` 或 `udp`。 + +默认所有。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md new file mode 100644 index 00000000..4f4ef485 --- /dev/null +++ b/docs/configuration/outbound/tuic.md @@ -0,0 +1,96 @@ +### Structure + +```json +{ + "type": "tuic", + "tag": "tuic-out", + + "server": "127.0.0.1", + "server_port": 1080, + "uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365", + "password": "hello", + "congestion_control": "cubic", + "udp_relay_mode": "native", + "udp_over_stream": false, + "zero_rtt_handshake": false, + "heartbeat": "10s", + "network": "tcp", + "tls": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### uuid + +==Required== + +TUIC user uuid + +#### password + +TUIC user password + +#### congestion_control + +QUIC congestion control algorithm + +One of: `cubic`, `new_reno`, `bbr` + +`cubic` is used by default. + +#### udp_relay_mode + +UDP packet relay mode + +| Mode | Description | +|:-------|:-------------------------------------------------------------------------| +| native | native UDP characteristics | +| quic | lossless UDP relay using QUIC streams, additional overhead is introduced | + +`native` is used by default. + +Conflict with `udp_over_stream`. + +#### udp_over_stream + +This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC +stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or +another program compatible with the protocol as a server. + +This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP +traffic (basically QUIC streams). + +Conflict with `udp_relay_mode`. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md new file mode 100644 index 00000000..6d31d7bc --- /dev/null +++ b/docs/configuration/outbound/tuic.zh.md @@ -0,0 +1,104 @@ +### 结构 + +```json +{ + "type": "tuic", + "tag": "tuic-out", + + "server": "127.0.0.1", + "server_port": 1080, + "uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365", + "password": "hello", + "congestion_control": "cubic", + "udp_relay_mode": "native", + "udp_over_stream": false, + "zero_rtt_handshake": false, + "heartbeat": "10s", + "network": "tcp", + "tls": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### uuid + +==必填== + +TUIC 用户 UUID + +#### password + +TUIC 用户密码 + +#### congestion_control + +QUIC 拥塞控制算法 + +可选值: `cubic`, `new_reno`, `bbr` + +默认使用 `cubic`。 + +#### udp_relay_mode + +UDP 包中继模式 + +| 模式 | 描述 | +|--------|------------------------------| +| native | 原生 UDP | +| quic | 使用 QUIC 流的无损 UDP 中继,引入了额外的开销 | + +与 `udp_over_stream` 冲突。 + +#### udp_over_stream + +这是 TUIC 的 [UDP over TCP 协议](/zh/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。 + +此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。 + +与 `udp_relay_mode` 冲突。 + +#### zero_rtt_handshake + +在客户端启用 0-RTT QUIC 连接握手 +这对性能影响不大,因为协议是完全复用的 + +!!! warning "" +强烈建议禁用此功能,因为它容易受到重放攻击。 +请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones) + +#### heartbeat + +发送心跳包以保持连接存活的时间间隔 + +#### network + +启用的网络协议。 + +`tcp` 或 `udp`。 + +默认所有。 + +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/urltest.md b/docs/configuration/outbound/urltest.md new file mode 100644 index 00000000..f4b3b0aa --- /dev/null +++ b/docs/configuration/outbound/urltest.md @@ -0,0 +1,49 @@ +### Structure + +```json +{ + "type": "urltest", + "tag": "auto", + + "outbounds": [ + "proxy-a", + "proxy-b", + "proxy-c" + ], + "url": "", + "interval": "", + "tolerance": 0, + "idle_timeout": "", + "interrupt_exist_connections": false +} +``` + +### Fields + +#### outbounds + +==Required== + +List of outbound tags to test. + +#### url + +The URL to test. `https://www.gstatic.com/generate_204` will be used if empty. + +#### interval + +The test interval. `3m` will be used if empty. + +#### tolerance + +The test tolerance in milliseconds. `50` will be used if empty. + +#### idle_timeout + +The idle timeout. `30m` will be used if empty. + +#### interrupt_exist_connections + +Interrupt existing connections when the selected outbound has changed. + +Only inbound connections are affected by this setting, internal connections will always be interrupted. diff --git a/docs/configuration/outbound/urltest.zh.md b/docs/configuration/outbound/urltest.zh.md new file mode 100644 index 00000000..4372298a --- /dev/null +++ b/docs/configuration/outbound/urltest.zh.md @@ -0,0 +1,49 @@ +### 结构 + +```json +{ + "type": "urltest", + "tag": "auto", + + "outbounds": [ + "proxy-a", + "proxy-b", + "proxy-c" + ], + "url": "", + "interval": "", + "tolerance": 50, + "idle_timeout": "", + "interrupt_exist_connections": false +} +``` + +### 字段 + +#### outbounds + +==必填== + +用于测试的出站标签列表。 + +#### url + +用于测试的链接。默认使用 `https://www.gstatic.com/generate_204`。 + +#### interval + +测试间隔。 默认使用 `3m`。 + +#### tolerance + +以毫秒为单位的测试容差。 默认使用 `50`。 + +#### idle_timeout + +空闲超时。默认使用 `30m`。 + +#### interrupt_exist_connections + +当选定的出站发生更改时,中断现有连接。 + +仅入站连接受此设置影响,内部连接将始终被中断。 \ No newline at end of file diff --git a/docs/configuration/outbound/vless.md b/docs/configuration/outbound/vless.md new file mode 100644 index 00000000..28134e55 --- /dev/null +++ b/docs/configuration/outbound/vless.md @@ -0,0 +1,82 @@ +### Structure + +```json +{ + "type": "vless", + "tag": "vless-out", + + "server": "127.0.0.1", + "server_port": 1080, + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "flow": "xtls-rprx-vision", + "network": "tcp", + "tls": {}, + "packet_encoding": "", + "multiplex": {}, + "transport": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### uuid + +==Required== + +VLESS user id. + +#### flow + +VLESS Sub-protocol. + +Available values: + +* `xtls-rprx-vision` + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +#### packet_encoding + +UDP packet encoding, xudp is used by default. + +| Encoding | Description | +|------------|-----------------------| +| (none) | Disabled | +| packetaddr | Supported by v2ray 5+ | +| xudp | Supported by xray | + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#outbound) for details. + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/vless.zh.md b/docs/configuration/outbound/vless.zh.md new file mode 100644 index 00000000..f3bc9a08 --- /dev/null +++ b/docs/configuration/outbound/vless.zh.md @@ -0,0 +1,82 @@ +### 结构 + +```json +{ + "type": "vless", + "tag": "vless-out", + + "server": "127.0.0.1", + "server_port": 1080, + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "flow": "xtls-rprx-vision", + "network": "tcp", + "tls": {}, + "packet_encoding": "", + "multiplex": {}, + "transport": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### uuid + +==必填== + +VLESS 用户 ID。 + +#### flow + +VLESS 子协议。 + +可用值: + +* `xtls-rprx-vision` + +#### network + +启用的网络协议。 + +`tcp` 或 `udp`。 + +默认所有。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +#### packet_encoding + +UDP 包编码,默认使用 xudp。 + +| 编码 | 描述 | +|------------|---------------| +| (空) | 禁用 | +| packetaddr | 由 v2ray 5+ 支持 | +| xudp | 由 xray 支持 | + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/vmess.md b/docs/configuration/outbound/vmess.md new file mode 100644 index 00000000..536601af --- /dev/null +++ b/docs/configuration/outbound/vmess.md @@ -0,0 +1,107 @@ +### Structure + +```json +{ + "type": "vmess", + "tag": "vmess-out", + + "server": "127.0.0.1", + "server_port": 1080, + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "security": "auto", + "alter_id": 0, + "global_padding": false, + "authenticated_length": true, + "network": "tcp", + "tls": {}, + "packet_encoding": "", + "transport": {}, + "multiplex": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required== + +The server address. + +#### server_port + +==Required== + +The server port. + +#### uuid + +==Required== + +The VMess user id. + +#### security + +Encryption methods: + +* `auto` +* `none` +* `zero` +* `aes-128-gcm` +* `chacha20-poly1305` + +Legacy encryption methods: + +* `aes-128-ctr` + +#### alter_id + +| Alter ID | Description | +|----------|---------------------| +| 0 | Use AEAD protocol | +| 1 | Use legacy protocol | +| > 1 | Unused, same as 1 | + +#### global_padding + +Protocol parameter. Will waste traffic randomly if enabled (enabled by default in v2ray and cannot be disabled). + +#### authenticated_length + +Protocol parameter. Enable length block encryption. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#outbound). + +#### packet_encoding + +UDP packet encoding. + +| Encoding | Description | +|------------|-----------------------| +| (none) | Disabled | +| packetaddr | Supported by v2ray 5+ | +| xudp | Supported by xray | + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#outbound) for details. + +#### transport + +V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/vmess.zh.md b/docs/configuration/outbound/vmess.zh.md new file mode 100644 index 00000000..cbc8bdee --- /dev/null +++ b/docs/configuration/outbound/vmess.zh.md @@ -0,0 +1,107 @@ +### 结构 + +```json +{ + "type": "vmess", + "tag": "vmess-out", + + "server": "127.0.0.1", + "server_port": 1080, + "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", + "security": "auto", + "alter_id": 0, + "global_padding": false, + "authenticated_length": true, + "network": "tcp", + "tls": {}, + "packet_encoding": "", + "multiplex": {}, + "transport": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### uuid + +==必填== + +VMess 用户 ID。 + +#### security + +加密方法: + +* `auto` +* `none` +* `zero` +* `aes-128-gcm` +* `chacha20-poly1305` + +旧加密方法: + +* `aes-128-ctr` + +#### alter_id + +| Alter ID | 描述 | +|----------|------------| +| 0 | 禁用旧协议 | +| 1 | 启用旧协议 | +| > 1 | 未使用, 行为同 1 | + +#### global_padding + +协议参数。 如果启用会随机浪费流量(在 v2ray 中默认启用并且无法禁用)。 + +#### authenticated_length + +协议参数。启用长度块加密。 + +#### network + +启用的网络协议。 + +`tcp` 或 `udp`。 + +默认所有。 + +#### tls + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 + +#### packet_encoding + +UDP 包编码。 + +| 编码 | 描述 | +|------------|---------------| +| (空) | 禁用 | +| packetaddr | 由 v2ray 5+ 支持 | +| xudp | 由 xray 支持 | + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 + +#### transport + +V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md new file mode 100644 index 00000000..648ba607 --- /dev/null +++ b/docs/configuration/outbound/wireguard.md @@ -0,0 +1,168 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.11.0" + + WireGuard outbound is deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-wireguard-outbound-to-endpoint). + +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-alert: [gso](#gso) + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [gso](#gso) + +### Structure + +```json +{ + "type": "wireguard", + "tag": "wireguard-out", + + "server": "127.0.0.1", + "server_port": 1080, + "system_interface": false, + "interface_name": "wg0", + "local_address": [ + "10.0.0.1/32" + ], + "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", + "peers": [ + { + "server": "127.0.0.1", + "server_port": 1080, + "public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", + "pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=", + "allowed_ips": [ + "0.0.0.0/0" + ], + "reserved": [0, 0, 0] + } + ], + "peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", + "pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=", + "reserved": [0, 0, 0], + "workers": 4, + "mtu": 1408, + "network": "tcp", + + // Deprecated + + "gso": false, + + ... // Dial Fields +} +``` + +### Fields + +#### server + +==Required if multi-peer disabled== + +The server address. + +#### server_port + +==Required if multi-peer disabled== + +The server port. + +#### system_interface + +Use system interface. + +Requires privilege and cannot conflict with exists system interfaces. + +Forced if gVisor not included in the build. + +#### interface_name + +Custom interface name for system interface. + +#### gso + +!!! failure "Deprecated in sing-box 1.11.0" + + GSO will be automatically enabled when available since sing-box 1.11.0. + +!!! question "Since sing-box 1.8.0" + +!!! quote "" + + Only supported on Linux. + +Try to enable generic segmentation offload. + +#### local_address + +==Required== + +List of IP (v4 or v6) address prefixes to be assigned to the interface. + +#### private_key + +==Required== + +WireGuard requires base64-encoded public and private keys. These can be generated using the wg(8) utility: + +```shell +wg genkey +echo "private key" || wg pubkey +``` + +#### peers + +Multi-peer support. + +If enabled, `server, server_port, peer_public_key, pre_shared_key` will be ignored. + +#### peers.allowed_ips + +WireGuard allowed IPs. + +#### peers.reserved + +WireGuard reserved field bytes. + +`$outbound.reserved` will be used if empty. + +#### peer_public_key + +==Required if multi-peer disabled== + +WireGuard peer public key. + +#### pre_shared_key + +WireGuard pre-shared key. + +#### reserved + +WireGuard reserved field bytes. + +#### workers + +WireGuard worker count. + +CPU count is used by default. + +#### mtu + +WireGuard MTU. + +1408 will be used if empty. + +#### network + +Enabled network + +One of `tcp` `udp`. + +Both is enabled by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md new file mode 100644 index 00000000..2b6d4a0a --- /dev/null +++ b/docs/configuration/outbound/wireguard.zh.md @@ -0,0 +1,142 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.11.0 废弃" + + WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 + +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-alert: [gso](#gso) + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [gso](#gso) + +### 结构 + +```json +{ + "type": "wireguard", + "tag": "wireguard-out", + + "server": "127.0.0.1", + "server_port": 1080, + "system_interface": false, + "interface_name": "wg0", + "local_address": [ + "10.0.0.1/32" + ], + "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", + "peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", + "pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=", + "reserved": [0, 0, 0], + "workers": 4, + "mtu": 1408, + "network": "tcp", + + // 废弃的 + + "gso": false, + + ... // 拨号字段 +} +``` + +### 字段 + +#### server + +==必填== + +服务器地址。 + +#### server_port + +==必填== + +服务器端口。 + +#### system_interface + +使用系统设备。 + +需要特权且不能与已有系统接口冲突。 + +如果 gVisor 未包含在构建中,则强制执行。 + +#### interface_name + +为系统接口自定义设备名称。 + +#### gso + +!!! failure "已在 sing-box 1.11.0 废弃" + + 自 sing-box 1.11.0 起,GSO 将可用时自动启用。 + +!!! question "自 sing-box 1.8.0 起" + +!!! quote "" + + 仅支持 Linux。 + +尝试启用通用分段卸载。 + +#### local_address + +==必填== + +接口的 IPv4/IPv6 地址或地址段的列表。 + +要分配给接口的 IP(v4 或 v6)地址段列表。 + +#### private_key + +==必填== + +WireGuard 需要 base64 编码的公钥和私钥。 这些可以使用 wg(8) 实用程序生成: + +```shell +wg genkey +echo "private key" || wg pubkey +``` + +#### peer_public_key + +==必填== + +WireGuard 对等公钥。 + +#### pre_shared_key + +WireGuard 预共享密钥。 + +#### reserved + +WireGuard 保留字段字节。 + +#### workers + +WireGuard worker 数量。 + +默认使用 CPU 数量。 + +#### mtu + +WireGuard MTU。 + +默认使用 1408。 + +#### network + +启用的网络协议 + +`tcp` 或 `udp`。 + +默认所有。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md new file mode 100644 index 00000000..62162cdf --- /dev/null +++ b/docs/configuration/route/geoip.md @@ -0,0 +1,41 @@ +--- +icon: material/note-remove +--- + +!!! failure "Removed in sing-box 1.12.0" + + GeoIP is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). + +### Structure + +```json +{ + "route": { + "geoip": { + "path": "", + "download_url": "", + "download_detour": "" + } + } +} +``` + +### Fields + +#### path + +The path to the sing-geoip database. + +`geoip.db` will be used if empty. + +#### download_url + +The download URL of the sing-geoip database. + +Default is `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`. + +#### download_detour + +The tag of the outbound to download the database. + +Default outbound will be used if empty. \ No newline at end of file diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md new file mode 100644 index 00000000..17559a46 --- /dev/null +++ b/docs/configuration/route/geoip.zh.md @@ -0,0 +1,41 @@ +--- +icon: material/note-remove +--- + +!!! failure "已在 sing-box 1.12.0 中被移除" + + GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 + +### 结构 + +```json +{ + "route": { + "geoip": { + "path": "", + "download_url": "", + "download_detour": "" + } + } +} +``` + +### 字段 + +#### path + +指定 GeoIP 资源的路径。 + +默认 `geoip.db`。 + +#### download_url + +指定 GeoIP 资源的下载链接。 + +默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 + +#### download_detour + +用于下载 GeoIP 资源的出站的标签。 + +如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md new file mode 100644 index 00000000..830d1158 --- /dev/null +++ b/docs/configuration/route/geosite.md @@ -0,0 +1,41 @@ +--- +icon: material/note-remove +--- + +!!! failure "Removed in sing-box 1.12.0" + + Geosite is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). + +### Structure + +```json +{ + "route": { + "geosite": { + "path": "", + "download_url": "", + "download_detour": "" + } + } +} +``` + +### Fields + +#### path + +The path to the sing-geosite database. + +`geosite.db` will be used if empty. + +#### download_url + +The download URL of the sing-geoip database. + +Default is `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`. + +#### download_detour + +The tag of the outbound to download the database. + +Default outbound will be used if empty. \ No newline at end of file diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md new file mode 100644 index 00000000..1ea0752a --- /dev/null +++ b/docs/configuration/route/geosite.zh.md @@ -0,0 +1,41 @@ +--- +icon: material/note-remove +--- + +!!! failure "已在 sing-box 1.12.0 中被移除" + + Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 + +### 结构 + +```json +{ + "route": { + "geosite": { + "path": "", + "download_url": "", + "download_detour": "" + } + } +} +``` + +### 字段 + +#### path + +指定 GeoSite 资源的路径。 + +默认 `geosite.db`。 + +#### download_url + +指定 GeoSite 资源的下载链接。 + +默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 + +#### download_detour + +用于下载 GeoSite 资源的出站的标签。 + +如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md new file mode 100644 index 00000000..6c59f850 --- /dev/null +++ b/docs/configuration/route/index.md @@ -0,0 +1,186 @@ +--- +icon: material/alert-decagram +--- + +# Route + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [find_neighbor](#find_neighbor) + :material-plus: [dhcp_lease_files](#dhcp_lease_files) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [default_domain_resolver](#default_domain_resolver) + :material-note-remove: [geoip](#geoip) + :material-note-remove: [geosite](#geosite) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [default_network_strategy](#default_network_strategy) + :material-plus: [default_network_type](#default_network_type) + :material-plus: [default_fallback_network_type](#default_fallback_network_type) + :material-plus: [default_fallback_delay](#default_fallback_delay) + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + +### Structure + +```json +{ + "route": { + "rules": [], + "rule_set": [], + "final": "", + "auto_detect_interface": false, + "override_android_vpn": false, + "default_interface": "", + "default_mark": 0, + "find_process": false, + "find_neighbor": false, + "dhcp_lease_files": [], + "default_domain_resolver": "", // or {} + "default_network_strategy": "", + "default_network_type": [], + "default_fallback_network_type": [], + "default_fallback_delay": "", + + // Removed + + "geoip": {}, + "geosite": {} + } +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### rules + +List of [Route Rule](./rule/) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [rule-set](/configuration/rule-set/) + +#### final + +Default outbound tag. the first outbound will be used if empty. + +#### auto_detect_interface + +!!! quote "" + + Only supported on Linux, Windows and macOS. + +Bind outbound connections to the default NIC by default to prevent routing loops under tun. + +Takes no effect if `outbound.bind_interface` is set. + +#### override_android_vpn + +!!! quote "" + + Only supported on Android. + +Accept Android VPN as upstream NIC when `auto_detect_interface` enabled. + +#### default_interface + +!!! quote "" + + Only supported on Linux, Windows and macOS. + +Bind outbound connections to the specified NIC by default to prevent routing loops under tun. + +Takes no effect if `auto_detect_interface` is set. + +#### default_mark + +!!! quote "" + + Only supported on Linux. + +Set routing mark by default. + +Takes no effect if `outbound.routing_mark` is set. + +#### find_process + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist. + +#### find_neighbor + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux and macOS. + +Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist. + +See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. + +#### dhcp_lease_files + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux and macOS. + +Custom DHCP lease file paths for hostname and MAC address resolution. + +Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty. + +#### default_domain_resolver + +!!! question "Since sing-box 1.12.0" + +See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details. + +Can be overridden by `outbound.domain_resolver`. + +#### default_network_strategy + +!!! question "Since sing-box 1.11.0" + +See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. + +Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set. + +Can be overridden by `outbound.network_strategy`. + +Conflicts with `default_interface`. + +#### default_network_type + +!!! question "Since sing-box 1.11.0" + +See [Dial Fields](/configuration/shared/dial/#network_type) for details. + +#### default_fallback_network_type + +!!! question "Since sing-box 1.11.0" + +See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details. + +#### default_fallback_delay + +!!! question "Since sing-box 1.11.0" + +See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md new file mode 100644 index 00000000..4977b084 --- /dev/null +++ b/docs/configuration/route/index.zh.md @@ -0,0 +1,185 @@ +--- +icon: material/alert-decagram +--- + +# 路由 + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [find_neighbor](#find_neighbor) + :material-plus: [dhcp_lease_files](#dhcp_lease_files) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [default_domain_resolver](#default_domain_resolver) + :material-note-remove: [geoip](#geoip) + :material-note-remove: [geosite](#geosite) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [default_network_strategy](#default_network_strategy) + :material-plus: [default_network_type](#default_network_type) + :material-plus: [default_fallback_network_type](#default_fallback_network_type) + :material-plus: [default_fallback_delay](#default_fallback_delay) + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + +### 结构 + +```json +{ + "route": { + "geoip": {}, + "geosite": {}, + "rules": [], + "rule_set": [], + "final": "", + "auto_detect_interface": false, + "override_android_vpn": false, + "default_interface": "", + "default_mark": 0, + "find_process": false, + "find_neighbor": false, + "dhcp_lease_files": [], + "default_network_strategy": "", + "default_fallback_delay": "" + } +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 字段 + +| 键 | 格式 | +|-----------|-----------------------| +| `geoip` | [GeoIP](./geoip/) | +| `geosite` | [Geosite](./geosite/) | + +#### rule + +一组 [路由规则](./rule/) 。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/zh/configuration/rule-set/)。 + +#### final + +默认出站标签。如果为空,将使用第一个可用于对应协议的出站。 + +#### auto_detect_interface + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +默认将出站连接绑定到默认网卡,以防止在 tun 下出现路由环路。 + +如果设置了 `outbound.bind_interface` 设置,则不生效。 + +#### override_android_vpn + +!!! quote "" + + 仅支持 Android。 + +启用 `auto_detect_interface` 时接受 Android VPN 作为上游网卡。 + +#### default_interface + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +默认将出站连接绑定到指定网卡,以防止在 tun 下出现路由环路。 + +如果设置了 `auto_detect_interface` 设置,则不生效。 + +#### default_mark + +!!! quote "" + + 仅支持 Linux。 + +默认为出站连接设置路由标记。 + +如果设置了 `outbound.routing_mark` 设置,则不生效。 + +#### find_process + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +在没有 `process_name`、`process_path`、`package_name`、`user` 或 `user_id` 规则时启用进程搜索以输出日志。 + +#### find_neighbor + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux 和 macOS。 + +在没有 `source_mac_address` 或 `source_hostname` 规则时启用邻居解析以输出日志。 + +参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 + +#### dhcp_lease_files + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux 和 macOS。 + +用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。 + +为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。 + +#### default_domain_resolver + +!!! question "自 sing-box 1.12.0 起" + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#domain_resolver)。 + +可以被 `outbound.domain_resolver` 覆盖。 + +#### network_strategy + +!!! question "自 sing-box 1.11.0 起" + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。 + +当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。 + +可以被 `outbound.network_strategy` 覆盖。 + +与 `default_interface` 冲突。 + +#### default_network_type + +!!! question "自 sing-box 1.11.0 起" + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_network_type)。 + +#### default_fallback_network_type + +!!! question "自 sing-box 1.11.0 起" + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_fallback_network_type)。 + +#### default_fallback_delay + +!!! question "自 sing-box 1.11.0 起" + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md new file mode 100644 index 00000000..97bbe376 --- /dev/null +++ b/docs/configuration/route/rule.md @@ -0,0 +1,547 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [package_name_regex](#package_name_regex) + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [interface_address](#interface_address) + :material-plus: [network_interface_address](#network_interface_address) + :material-plus: [default_interface_address](#default_interface_address) + :material-plus: [preferred_by](#preferred_by) + :material-alert: [network](#network) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [action](#action) + :material-alert: [outbound](#outbound) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [client](#client) + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [process_path_regex](#process_path_regex) + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + +### Structure + +```json +{ + "route": { + "rules": [ + { + "inbound": [ + "mixed-in" + ], + "ip_version": 6, + "network": [ + "tcp" + ], + "auth_user": [ + "usera", + "userb" + ], + "protocol": [ + "tls", + "http", + "quic" + ], + "client": [ + "chromium", + "safari", + "firefox", + "quic-go" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "geosite": [ + "cn" + ], + "source_geoip": [ + "private" + ], + "geoip": [ + "cn" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_ip_is_private": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "package_name_regex": [ + "^com\\.termux.*" + ], + "user": [ + "sekai" + ], + "user_id": [ + 1000 + ], + "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "interface_address": { + "en0": [ + "2000::/3" + ] + }, + "network_interface_address": { + "wifi": [ + "2000::/3" + ] + }, + "default_interface_address": [ + "2000::/3" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "preferred_by": [ + "tailscale", + "wireguard" + ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], + // deprecated + "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, + "invert": false, + "action": "route", + "outbound": "direct" + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false, + "action": "route", + "outbound": "direct" + } + ] + } +} + +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr` || `ip_is_private`) && + (`port` || `port_range`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && + (`source_port` || `source_port_range`) && + `other fields` + + Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. + +#### inbound + +Tags of [Inbound](/configuration/inbound/). + +#### ip_version + +4 or 6. + +Not limited if empty. + +#### auth_user + +Username, see each inbound for details. + +#### protocol + +Sniffed protocol, see [Protocol Sniff](/configuration/route/sniff/) for details. + +#### client + +!!! question "Since sing-box 1.10.0" + +Sniffed client type, see [Protocol Sniff](/configuration/route/sniff/) for details. + +#### network + +!!! quote "Changes in sing-box 1.13.0" + + Since sing-box 1.13.0, you can match ICMP echo (ping) requests via the new `icmp` network. + + Such traffic originates from `TUN`, `WireGuard`, and `Tailscale` inbounds and can be routed to `Direct`, `WireGuard`, and `Tailscale` outbounds. + +Match network type. + +`tcp`, `udp` or `icmp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### geosite + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). + +Match geosite. + +#### source_geoip + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). + +Match source geoip. + +#### geoip + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). + +Match geoip. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public IP. + +#### ip_cidr + +Match IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + +#### package_name + +Match android package name. + +#### package_name_regex + +!!! question "Since sing-box 1.14.0" + +Match android package name using regular expression. + +#### user + +!!! quote "" + + Only supported on Linux. + +Match user name. + +#### user_id + +!!! quote "" + + Only supported on Linux. + +Match user id. + +#### clash_mode + +Match Clash mode. + +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match network type. + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match if network is considered Metered (on Android) or considered expensive, +such as Cellular or a Personal Hotspot (on Apple platforms). + +#### network_is_constrained + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Match if network is in Low Data Mode. + +#### interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match interface address. + +#### network_interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Matches network interface (same values as `network_type`) address. + +#### default_interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match default interface address. + +#### wifi_ssid + +Match WiFi SSID. + +See [Wi-Fi State](/configuration/shared/wifi-state/) for details. + +#### wifi_bssid + +Match WiFi BSSID. + +See [Wi-Fi State](/configuration/shared/wifi-state/) for details. + +#### preferred_by + +!!! question "Since sing-box 1.13.0" + +Match specified outbounds' preferred routes. + +| Type | Match | +|-------------|-----------------------------------------------| +| `tailscale` | Match MagicDNS domains and peers' allowed IPs | +| `wireguard` | Match peers's allowed IPs | + +#### source_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. + +Match source device MAC address. + +#### source_hostname + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. + +Match source device hostname from DHCP leases. + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [rule-set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +!!! failure "Deprecated in sing-box 1.10.0" + + `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +Make `ip_cidr` in rule-sets match the source IP. + +#### rule_set_ip_cidr_match_source + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` in rule-sets match the source IP. + +#### invert + +Invert match result. + +#### action + +==Required== + +See [Rule Actions](../rule_action/) for details. + +#### outbound + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [Rule Action](../rule_action#route). + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md new file mode 100644 index 00000000..d55b565d --- /dev/null +++ b/docs/configuration/route/rule.zh.md @@ -0,0 +1,545 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [package_name_regex](#package_name_regex) + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [interface_address](#interface_address) + :material-plus: [network_interface_address](#network_interface_address) + :material-plus: [default_interface_address](#default_interface_address) + :material-plus: [preferred_by](#preferred_by) + :material-alert: [network](#network) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [action](#action) + :material-alert: [outbound](#outbound) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [client](#client) + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [process_path_regex](#process_path_regex) + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + +### 结构 + +```json +{ + "route": { + "rules": [ + { + "inbound": [ + "mixed-in" + ], + "ip_version": 6, + "network": [ + "tcp" + ], + "auth_user": [ + "usera", + "userb" + ], + "protocol": [ + "tls", + "http", + "quic" + ], + "client": [ + "chromium", + "safari", + "firefox", + "quic-go" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "geosite": [ + "cn" + ], + "source_geoip": [ + "private" + ], + "geoip": [ + "cn" + ], + "source_ip_cidr": [ + "10.0.0.0/24" + ], + "source_ip_is_private": false, + "ip_cidr": [ + "10.0.0.0/24" + ], + "ip_is_private": false, + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "package_name_regex": [ + "^com\\.termux.*" + ], + "user": [ + "sekai" + ], + "user_id": [ + 1000 + ], + "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "interface_address": { + "en0": [ + "2000::/3" + ] + }, + "network_interface_address": { + "wifi": [ + "2000::/3" + ] + }, + "default_interface_address": [ + "2000::/3" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "preferred_by": [ + "tailscale", + "wireguard" + ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], + // 已弃用 + "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, + "invert": false, + "action": "route", + "outbound": "direct" + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false, + "action": "route", + "outbound": "direct" + } + ] + } +} + +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +### 默认字段 + +!!! note "" + + 默认规则使用以下匹配逻辑: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr` || `ip_is_private`) && + (`port` || `port_range`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && + (`source_port` || `source_port_range`) && + `other fields` + + 另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。 + +#### inbound + +[入站](/zh/configuration/inbound/) 标签。 + +#### ip_version + +4 或 6。 + +默认不限制。 + +#### auth_user + +认证用户名,参阅入站设置。 + +#### protocol + +探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。 + +#### client + +!!! question "自 sing-box 1.10.0 起" + +探测到的客户端类型, 参阅 [协议探测](/zh/configuration/route/sniff/)。 + +#### network + +!!! quote "sing-box 1.13.0 中的更改" + + 自 sing-box 1.13.0 起,您可以通过新的 `icmp` 网络匹配 ICMP 回显(ping)请求。 + + 此类流量源自 `TUN`、`WireGuard` 和 `Tailscale` 入站,并可路由至 `Direct`、`WireGuard` 和 `Tailscale` 出站。 + +匹配网络类型。 + +`tcp`、`udp` 或 `icmp`。 + +#### domain + +匹配完整域名。 + +#### domain_suffix + +匹配域名后缀。 + +#### domain_keyword + +匹配域名关键字。 + +#### domain_regex + +匹配域名正则表达式。 + +#### geosite + +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 + +匹配 Geosite。 + +#### source_geoip + +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 + +匹配源 GeoIP。 + +#### geoip + +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 + +匹配 GeoIP。 + +#### source_ip_cidr + +匹配源 IP CIDR。 + +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + +#### ip_cidr + +匹配 IP CIDR。 + +#### ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开 IP。 + +#### source_port + +匹配源端口。 + +#### source_port_range + +匹配源端口范围。 + +#### port + +匹配端口。 + +#### port_range + +匹配端口范围。 + +#### process_name + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +匹配进程名称。 + +#### process_path + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配进程路径。 + +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + +#### package_name + +匹配 Android 应用包名。 + +#### package_name_regex + +!!! question "自 sing-box 1.14.0 起" + +使用正则表达式匹配 Android 应用包名。 + +#### user + +!!! quote "" + + 仅支持 Linux. + +匹配用户名。 + +#### user_id + +!!! quote "" + + 仅支持 Linux. + +匹配用户 ID。 + +#### clash_mode + +匹配 Clash 模式。 + +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络类型。 + +可用值: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配如果网络被视为计费 (在 Android) 或被视为昂贵, +像蜂窝网络或个人热点 (在 Apple 平台)。 + +#### network_is_constrained + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +匹配如果网络在低数据模式下。 + +#### interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配接口地址。 + +#### network_interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络接口(可用值同 `network_type`)地址。 + +#### default_interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配默认接口地址。 + +#### wifi_ssid + +匹配 WiFi SSID。 + +参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。 + +#### wifi_bssid + +匹配 WiFi BSSID。 + +参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。 + +#### preferred_by + +!!! question "自 sing-box 1.13.0 起" + +匹配制定出站的首选路由。 + +| 类型 | 匹配 | +|-------------|--------------------------------| +| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs | +| `wireguard` | 匹配对端的 allowed IPs | + +#### source_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 + +匹配源设备 MAC 地址。 + +#### source_hostname + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 + +匹配源设备从 DHCP 租约获取的主机名。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +!!! failure "已在 sing-box 1.10.0 废弃" + + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### rule_set_ip_cidr_match_source + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### invert + +反选匹配结果。 + +#### action + +==必填== + +参阅 [规则动作](../rule_action/)。 + +#### outbound + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [规则动作](../rule_action#route). + +### 逻辑字段 + +#### type + +`logical` + +#### mode + +==必填== + +`and` 或 `or` + +#### rules + +==必填== + +包括的规则。 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md new file mode 100644 index 00000000..1ba69039 --- /dev/null +++ b/docs/configuration/route/rule_action.md @@ -0,0 +1,330 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [bypass](#bypass) + :material-alert: [reject](#reject) + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [tls_fragment](#tls_fragment) + :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) + :material-plus: [tls_record_fragment](#tls_record_fragment) + :material-plus: [resolve.disable_cache](#disable_cache) + :material-plus: [resolve.rewrite_ttl](#rewrite_ttl) + :material-plus: [resolve.client_subnet](#client_subnet) + +## Final actions + +### route + +```json +{ + "action": "route", // default + "outbound": "", + + ... // route-options Fields +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +`route` inherits the classic rule behavior of routing connection to the specified outbound. + +#### outbound + +==Required== + +Tag of target outbound. + +#### route-options Fields + +See `route-options` fields below. + +### bypass + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux with `auto_redirect` enabled. + +```json +{ + "action": "bypass", + "outbound": "", + + ... // route-options Fields +} +``` + +`bypass` bypasses sing-box at the kernel level for auto redirect connections in pre-match. + +For non-auto-redirect connections and already established connections, +if `outbound` is specified, the behavior is the same as `route`; +otherwise, the rule will be skipped. + +#### outbound + +Tag of target outbound. + +If not specified, the rule only matches in [pre-match](/configuration/shared/pre-match/) +from auto redirect, and will be skipped in other contexts. + +#### route-options Fields + +See `route-options` fields below. + +### reject + +!!! quote "Changes in sing-box 1.13.0" + + Since sing-box 1.13.0, you can reject (or directly reply to) ICMP echo (ping) requests using `reject` action. + +```json +{ + "action": "reject", + "method": "default", // default + "no_drop": false +} +``` + +`reject` reject connections + +The specified method is used for reject tun connections if `sniff` action has not been performed yet. + +For non-tun connections and already established connections, will just be closed. + +#### method + +For TCP and UDP connections: + +- `default`: Reply with TCP RST for TCP connections, and ICMP port unreachable for UDP packets. +- `drop`: Drop packets. + +For ICMP echo requests: + +- `default`: Reply with ICMP host unreachable. +- `drop`: Drop packets. +- `reply`: Reply with ICMP echo reply. + +#### no_drop + +If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. + +Not available when `method` is set to drop. + +### hijack-dns + +```json +{ + "action": "hijack-dns" +} +``` + +`hijack-dns` hijack DNS requests to the sing-box DNS module. + +## Non-final actions + +### route-options + +```json +{ + "action": "route-options", + "override_address": "", + "override_port": 0, + "network_strategy": "", + "fallback_delay": "", + "udp_disable_domain_unmapping": false, + "udp_connect": false, + "udp_timeout": "", + "tls_fragment": false, + "tls_fragment_fallback_delay": "", + "tls_record_fragment": "" +} +``` + +`route-options` set options for routing. + +#### override_address + +Override the connection destination address. + +#### override_port + +Override the connection destination port. + +#### network_strategy + +See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. + +Only take effect if outbound is direct without `outbound.bind_interface`, +`outbound.inet4_bind_address` and `outbound.inet6_bind_address` set. + +#### network_type + +See [Dial Fields](/configuration/shared/dial/#network_type) for details. + +#### fallback_network_type + +See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details. + +#### fallback_delay + +See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. + +#### udp_disable_domain_unmapping + +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. + +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. + +#### udp_connect + +If enabled, attempts to connect UDP connection to the destination instead of listen. + +#### udp_timeout + +Timeout for UDP connections. + +Setting a larger value than the UDP timeout in inbounds will have no effect. + +Default value for protocol sniffed connections: + +| Timeout | Protocol | +|---------|----------------------| +| `10s` | `dns`, `ntp`, `stun` | +| `30s` | `quic`, `dtls` | + +If no protocol is sniffed, the following ports will be recognized as protocols by default: + +| Port | Protocol | +|------|----------| +| 53 | `dns` | +| 123 | `ntp` | +| 443 | `quic` | +| 3478 | `stun` | + +#### tls_fragment + +!!! question "Since sing-box 1.12.0" + +Fragment TLS handshakes to bypass firewalls. + +This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, +and should not be used to circumvent real censorship. + +Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked. + +On Linux, Apple platforms, (administrator privileges required) Windows, +the wait time can be automatically detected. Otherwise, it will fall back to +waiting for a fixed time specified by `tls_fragment_fallback_delay`. + +In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, +because the target is considered to be local or behind a transparent proxy. + +#### tls_fragment_fallback_delay + +!!! question "Since sing-box 1.12.0" + +The fallback value used when TLS segmentation cannot automatically determine the wait time. + +`500ms` is used by default. + +#### tls_record_fragment + +!!! question "Since sing-box 1.12.0" + +Fragment TLS handshake into multiple TLS records to bypass firewalls. + +### sniff + +```json +{ + "action": "sniff", + "sniffer": [], + "timeout": "" +} +``` + +`sniff` performs protocol sniffing on connections. + +For deprecated `inbound.sniff` options, it is considered to `sniff()` performed before routing. + +#### sniffer + +Enabled sniffers. + +All sniffers enabled by default. + +Available protocol values an be found on in [Protocol Sniff](../sniff/) + +#### timeout + +Timeout for sniffing. + +`300ms` is used by default. + +### resolve + +```json +{ + "action": "resolve", + "server": "", + "strategy": "", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`resolve` resolve request destination from domain to IP addresses. + +#### server + +Specifies DNS server tag to use instead of selecting through DNS routing. + +#### strategy + +DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. + +`dns.strategy` will be used by default. + +#### disable_cache + +!!! question "Since sing-box 1.12.0" + +Disable cache and save cache in this query. + +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + +#### rewrite_ttl + +!!! question "Since sing-box 1.12.0" + +Rewrite TTL in DNS responses. + +#### client_subnet + +!!! question "Since sing-box 1.12.0" + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Will override `dns.client_subnet`. diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md new file mode 100644 index 00000000..5b13219b --- /dev/null +++ b/docs/configuration/route/rule_action.zh.md @@ -0,0 +1,319 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [bypass](#bypass) + :material-alert: [reject](#reject) + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [tls_fragment](#tls_fragment) + :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) + :material-plus: [tls_record_fragment](#tls_record_fragment) + :material-plus: [resolve.disable_cache](#disable_cache) + :material-plus: [resolve.rewrite_ttl](#rewrite_ttl) + :material-plus: [resolve.client_subnet](#client_subnet) + +## 最终动作 + +### route + +```json +{ + "action": "route", // 默认 + "outbound": "", + + ... // route-options 字段 +} +``` + +`route` 继承了将连接路由到指定出站的经典规则动作。 + +#### outbound + +==必填== + +目标出站的标签。 + +#### route-options 字段 + +参阅下方的 `route-options` 字段。 + +### bypass + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux,且需要启用 `auto_redirect`。 + +```json +{ + "action": "bypass", + "outbound": "", + + ... // route-options 字段 +} +``` + +`bypass` 在预匹配中为 auto redirect 连接在内核层面绕过 sing-box。 + +对于非 auto redirect 连接和已建立的连接,如果指定了 `outbound`,行为与 `route` 相同;否则规则将被跳过。 + +#### outbound + +目标出站的标签。 + +如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。 + +#### route-options 字段 + +参阅下方的 `route-options` 字段。 + +### reject + +!!! quote "sing-box 1.13.0 中的更改" + + 自 sing-box 1.13.0 起,您可以通过 `reject` 动作拒绝(或直接回复)ICMP 回显(ping)请求。 + +```json +{ + "action": "reject", + "method": "default", // 默认 + "no_drop": false +} +``` + +`reject` 拒绝连接。 + +如果尚未执行 `sniff` 操作,则将使用指定方法拒绝 tun 连接。 + +对于非 tun 连接和已建立的连接,将直接关闭。 + +#### method + +对于 TCP 和 UDP 连接: + +- `default`: 对于 TCP 连接回复 RST,对于 UDP 包回复 ICMP 端口不可达。 +- `drop`: 丢弃数据包。 + +对于 ICMP 回显请求: + +- `default`: 回复 ICMP 主机不可达。 +- `drop`: 丢弃数据包。 +- `reply`: 回复以 ICMP 回显应答。 + +#### no_drop + +如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 + +当 `method` 设为 `drop` 时不可用。 + +### hijack-dns + +```json +{ + "action": "hijack-dns" +} +``` + +`hijack-dns` 劫持 DNS 请求至 sing-box DNS 模块。 + +## 非最终动作 + +### route-options + +```json +{ + "action": "route-options", + "override_address": "", + "override_port": 0, + "network_strategy": "", + "fallback_delay": "", + "udp_disable_domain_unmapping": false, + "udp_connect": false, + "udp_timeout": "" +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +`route-options` 为路由设置选项。 + +#### override_address + +覆盖目标地址。 + +#### override_port + +覆盖目标端口。 + +#### network_strategy + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。 + +仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address` +且 `outbound.inet6_bind_address` 未设置时生效。 + +#### network_type + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。 + +#### fallback_network_type + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。 + +#### fallback_delay + +详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。 + +#### udp_disable_domain_unmapping + +如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 + +此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 + +#### udp_connect + +如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 + +#### udp_timeout + +UDP 连接超时时间。 + +设置比入站 UDP 超时更大的值将无效。 + +已探测协议连接的默认值: + +| 超时 | 协议 | +|-------|----------------------| +| `10s` | `dns`, `ntp`, `stun` | +| `30s` | `quic`, `dtls` | + +如果没有探测到协议,以下端口将默认识别为协议: + +| 端口 | 协议 | +|------|--------| +| 53 | `dns` | +| 123 | `ntp` | +| 443 | `quic` | +| 3478 | `stun` | + +#### tls_fragment + +!!! question "自 sing-box 1.12.0 起" + +通过分段 TLS 握手数据包来绕过防火墙检测。 + +此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。 + +由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。 + +在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。 +若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。 + +此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。 + +#### tls_fragment_fallback_delay + +!!! question "自 sing-box 1.12.0 起" + +当 TLS 分片功能无法自动判定等待时间时使用的回退值。 + +默认使用 `500ms`。 + +#### tls_record_fragment + +!!! question "自 sing-box 1.12.0 起" + +通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。 + +### sniff + +```json +{ + "action": "sniff", + "sniffer": [], + "timeout": "" +} +``` + +`sniff` 对连接执行协议嗅探。 + +对于已弃用的 `inbound.sniff` 选项,被视为在路由之前执行的 `sniff`。 + +#### sniffer + +启用的探测器。 + +默认启用所有探测器。 + +可用的协议值可以在 [协议嗅探](../sniff/) 中找到。 + +#### timeout + +探测超时时间。 + +默认使用 300ms。 + +### resolve + +```json +{ + "action": "resolve", + "server": "", + "strategy": "", + "disable_cache": false, + "disable_optimistic_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`resolve` 将请求的目标从域名解析为 IP 地址。 + +#### server + +指定要使用的 DNS 服务器的标签,而不是通过 DNS 路由进行选择。 + +#### strategy + +DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、`ipv6_only`。 + +默认使用 `dns.strategy`。 + +#### disable_cache + +!!! question "自 sing-box 1.12.0 起" + +在此查询中禁用缓存。 + +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + +#### rewrite_ttl + +!!! question "自 sing-box 1.12.0 起" + +重写 DNS 回应中的 TTL。 + +#### client_subnet + +!!! question "自 sing-box 1.12.0 起" + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +将覆盖 `dns.client_subnet`. diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md new file mode 100644 index 00000000..0fb386f7 --- /dev/null +++ b/docs/configuration/route/sniff.md @@ -0,0 +1,32 @@ +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: QUIC client type detect support for QUIC + :material-plus: Chromium support for QUIC + :material-plus: BitTorrent support + :material-plus: DTLS support + :material-plus: SSH support + :material-plus: RDP support + +If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. + +#### Supported Protocols + +| Network | Protocol | Domain Name | Client | +|:-------:|:------------:|:-----------:|:----------------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | +| UDP | `quic` | Server Name | QUIC Client Type | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | +| TCP | `ssh` | / | SSH Client Name | +| TCP | `rdp` | / | / | +| UDP | `ntp` | / | / | + +| QUIC Client | Type | +|:------------------------:|:----------:| +| Chromium/Cronet | `chromium` | +| Safari/Apple Network API | `safari` | +| Firefox / uquic firefox | `firefox` | +| quic-go / uquic chrome | `quic-go` | diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md new file mode 100644 index 00000000..8cb9488f --- /dev/null +++ b/docs/configuration/route/sniff.zh.md @@ -0,0 +1,32 @@ +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: QUIC 的 客户端类型探测支持 + :material-plus: QUIC 的 Chromium 支持 + :material-plus: BitTorrent 支持 + :material-plus: DTLS 支持 + :material-plus: SSH 支持 + :material-plus: RDP 支持 + +如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 + +#### 支持的协议 + +| 网络 | 协议 | 域名 | 客户端 | +|:-------:|:------------:|:-----------:|:----------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | +| UDP | `quic` | Server Name | QUIC 客户端类型 | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | +| TCP | `ssh` | / | SSH 客户端名称 | +| TCP | `rdp` | / | / | +| UDP | `ntp` | / | / | + +| QUIC 客户端 | 类型 | +|:------------------------:|:----------:| +| Chromium/Cronet | `chromium` | +| Safari/Apple Network API | `safari` | +| Firefox / uquic firefox | `firefox` | +| quic-go / uquic chrome | `quic-go` | diff --git a/docs/configuration/rule-set/adguard.md b/docs/configuration/rule-set/adguard.md new file mode 100644 index 00000000..c8bd32fa --- /dev/null +++ b/docs/configuration/rule-set/adguard.md @@ -0,0 +1,65 @@ +!!! question "Since sing-box 1.10.0" + +sing-box supports some rule-set formats from other projects which cannot be fully translated to sing-box, +currently only AdGuard DNS Filter. + +These formats are not directly supported as source formats, +instead you need to convert them to binary rule-set. + +## Convert + +Use `sing-box rule-set convert --type adguard [--output .srs] .txt` to convert to binary rule-set. + +## Performance + +AdGuard keeps all rules in memory and matches them sequentially, +while sing-box chooses high performance and smaller memory usage. +As a trade-off, you cannot know which rule item is matched. + +## Compatibility + +Almost all rules in [AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) +and rules in rule-sets listed in [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) +are supported. + +## Supported formats + +### AdGuard Filter + +#### Basic rule syntax + +| Syntax | Supported | +|--------|------------------| +| `@@` | :material-check: | +| `\|\|` | :material-check: | +| `\|` | :material-check: | +| `^` | :material-check: | +| `*` | :material-check: | + +#### Host syntax + +| Syntax | Example | Supported | +|-------------|--------------------------|--------------------------| +| Scheme | `https://` | :material-alert: Ignored | +| Domain Host | `example.org` | :material-check: | +| IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | +| Regexp | `/regexp/` | :material-check: | +| Port | `example.org:80` | :material-close: | +| Path | `example.org/path/ad.js` | :material-close: | + +#### Modifier syntax + +| Modifier | Supported | +|-----------------------|--------------------------| +| `$important` | :material-check: | +| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | +| Any other modifiers | :material-close: | + +### Hosts + +Only items with `0.0.0.0` IP addresses will be accepted. + +### Simple + +When all rule lines are valid domains, they are treated as simple line-by-line domain rules which, +like hosts, only match the exact same domain. \ No newline at end of file diff --git a/docs/configuration/rule-set/adguard.zh.md b/docs/configuration/rule-set/adguard.zh.md new file mode 100644 index 00000000..82773280 --- /dev/null +++ b/docs/configuration/rule-set/adguard.zh.md @@ -0,0 +1,64 @@ +!!! question "自 sing-box 1.10.0 起" + +sing-box 支持其他项目的一些规则集格式,这些格式无法完全转换为 sing-box, +目前只有 AdGuard DNS Filter。 + +这些格式不直接作为源格式支持, +而是需要将它们转换为二进制规则集。 + +## 转换 + +使用 `sing-box rule-set convert --type adguard [--output .srs] .txt` 以转换为二进制规则集。 + +## 性能 + +AdGuard 将所有规则保存在内存中并按顺序匹配, +而 sing-box 选择高性能和较小的内存使用量。 +作为权衡,您无法知道匹配了哪个规则项。 + +## 兼容性 + +[AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) +中的几乎所有规则以及 [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) +中列出的规则集中的规则均受支持。 + +## 支持的格式 + +### AdGuard Filter + +#### 基本规则语法 + +| 语法 | 支持 | +|--------|------------------| +| `@@` | :material-check: | +| `\|\|` | :material-check: | +| `\|` | :material-check: | +| `^` | :material-check: | +| `*` | :material-check: | + +#### 主机语法 + +| 语法 | 示例 | 支持 | +|-------------|--------------------------|--------------------------| +| Scheme | `https://` | :material-alert: Ignored | +| Domain Host | `example.org` | :material-check: | +| IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | +| Regexp | `/regexp/` | :material-check: | +| Port | `example.org:80` | :material-close: | +| Path | `example.org/path/ad.js` | :material-close: | + +#### 描述符语法 + +| 描述符 | 支持 | +|-----------------------|--------------------------| +| `$important` | :material-check: | +| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | +| 任何其他描述符 | :material-close: | + +### Hosts + +只有 IP 地址为 `0.0.0.0` 的条目将被接受。 + +### 简易 + +当所有行都是有效域时,它们被视为简单的逐行域规则, 与 hosts 一样,只匹配完全相同的域。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 00000000..81a5e9a0 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,325 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [query_type](#query_type) + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [network_interface_address](#network_interface_address) + :material-plus: [default_interface_address](#default_interface_address) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "package_name_regex": [ + "^com\\.termux.*" + ], + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "network_interface_address": { + "wifi": [ + "2000::/3" + ] + }, + "default_interface_address": [ + "2000::/3" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +!!! quote "Changes in sing-box 1.14.0" + + When a DNS rule references this rule-set, this field now also applies + when the DNS rule is matched from an internal domain resolution that + does not target a specific DNS server. In earlier versions, only DNS + queries received from a client evaluated this field. See + [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules) + for the full list. + + When a DNS rule references a rule-set containing this field, the DNS + rule is incompatible in the same DNS configuration with Legacy Address + Filter Fields in DNS rules, the Legacy `strategy` DNS rule action + option, and the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item. + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when `rule_set_ipcidr_match_source` enabled in route/DNS rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + +#### package_name + +Match android package name. + +#### package_name_regex + +!!! question "Since sing-box 1.14.0" + +Match android package name using regular expression. + +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match network type. + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match if network is considered Metered (on Android) or considered expensive, +such as Cellular or a Personal Hotspot (on Apple platforms). + +#### network_is_constrained + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Match if network is in Low Data Mode. + +#### network_interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Matches network interface (same values as `network_type`) address. + +#### default_interface_address + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match default interface address. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/headless-rule.zh.md b/docs/configuration/rule-set/headless-rule.zh.md new file mode 100644 index 00000000..ad78ffe4 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.zh.md @@ -0,0 +1,316 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [query_type](#query_type) + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [network_interface_address](#network_interface_address) + :material-plus: [default_interface_address](#default_interface_address) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + +### 结构 + +!!! question "自 sing-box 1.8.0 起" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "package_name_regex": [ + "^com\\.termux.*" + ], + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "network_interface_address": { + "wifi": [ + "2000::/3" + ] + }, + "default_interface_address": [ + "2000::/3" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +### Default Fields + +!!! note "" + + 默认规则使用以下匹配逻辑: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +!!! quote "sing-box 1.14.0 中的更改" + + 当 DNS 规则引用此规则集时,此字段现在也会在 DNS 规则被未指定具体 + DNS 服务器的内部域名解析匹配时生效。此前只有来自客户端的 DNS 查询 + 才会评估此字段。完整列表参阅 + [迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。 + + 当 DNS 规则引用了包含此字段的规则集时,该 DNS 规则在同一 DNS 配置中 + 不能与旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项, + 或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。 + +DNS 查询类型。值可以为整数或者类型名称字符串。 + +#### network + +`tcp` 或 `udp`。 + +#### domain + +匹配完整域名。 + +#### domain_suffix + +匹配域名后缀。 + +#### domain_keyword + +匹配域名关键字。 + +#### domain_regex + +匹配域名正则表达式。 + +#### source_ip_cidr + +匹配源 IP CIDR。 + +#### ip_cidr + +匹配 IP CIDR。 + +#### source_port + +匹配源端口。 + +#### source_port_range + +匹配源端口范围。 + +#### port + +匹配端口。 + +#### port_range + +匹配端口范围。 + +#### process_name + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +匹配进程名称。 + +#### process_path + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配进程路径。 + +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + +#### package_name + +匹配 Android 应用包名。 + +#### package_name_regex + +!!! question "自 sing-box 1.14.0 起" + +使用正则表达式匹配 Android 应用包名。 + +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络类型。 + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配如果网络被视为计费 (在 Android) 或被视为昂贵, +像蜂窝网络或个人热点 (在 Apple 平台)。 + +#### network_is_constrained + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +匹配如果网络在低数据模式下。 + +#### network_interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络接口(可用值同 `network_type`)地址。 + +#### default_interface_address + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配默认接口地址。 + +#### wifi_ssid + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配 WiFi SSID。 + +#### wifi_bssid + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +#### invert + +反选匹配结果。 + +### 逻辑字段 + +#### type + +`logical` + +#### mode + +==必填== + +`and` 或 `or` + +#### rules + +==必填== + +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 00000000..73ec7b85 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,115 @@ +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: `type: inline` + +# rule-set + +!!! question "Since sing-box 1.8.0" + +### Structure + +=== "Inline" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "type": "inline", // optional + "tag": "", + "rules": [] + } + ``` + +=== "Local File" + + ```json + { + "type": "local", + "tag": "", + "format": "source", // or binary + "path": "" + } + ``` + +=== "Remote File" + + !!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + + ```json + { + "type": "remote", + "tag": "", + "format": "source", // or binary + "url": "", + "download_detour": "", // optional + "update_interval": "" // optional + } + ``` + +### Fields + +#### type + +==Required== + +Type of rule-set, `local` or `remote`. + +#### tag + +==Required== + +Tag of rule-set. + +### Inline Fields + +!!! question "Since sing-box 1.10.0" + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule/). + +### Local or Remote Fields + +#### format + +==Required== + +Format of rule-set file, `source` or `binary`. + +Optional when `path` or `url` uses `json` or `srs` as extension. + +### Local Fields + +#### path + +==Required== + +!!! note "" + + Will be automatically reloaded if file modified since sing-box 1.10.0. + +File path of rule-set. + +### Remote Fields + +#### url + +==Required== + +Download URL of rule-set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of rule-set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/index.zh.md b/docs/configuration/rule-set/index.zh.md new file mode 100644 index 00000000..eac51953 --- /dev/null +++ b/docs/configuration/rule-set/index.zh.md @@ -0,0 +1,115 @@ +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: `type: inline` + +# 规则集 + +!!! question "自 sing-box 1.8.0 起" + +### 结构 + +=== "内联" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "type": "inline", // 可选 + "tag": "", + "rules": [] + } + ``` + +=== "本地文件" + + ```json + { + "type": "local", + "tag": "", + "format": "source", // or binary + "path": "" + } + ``` + +=== "远程文件" + + !!! info "" + + 远程规则集将被缓存如果 `experimental.cache_file.enabled` 已启用。 + + ```json + { + "type": "remote", + "tag": "", + "format": "source", // or binary + "url": "", + "download_detour": "", // 可选 + "update_interval": "" // 可选 + } + ``` + +### 字段 + +#### type + +==必填== + +规则集类型, `local` 或 `remote`。 + +#### tag + +==必填== + +规则集的标签。 + +### 内联字段 + +!!! question "自 sing-box 1.10.0 起" + +#### rules + +==必填== + +一组 [无头规则](./headless-rule/). + +### 本地或远程字段 + +#### format + +==必填== + +规则集格式, `source` 或 `binary`。 + +当 `path` 或 `url` 使用 `json` 或 `srs` 作为扩展名时可选。 + +### 本地字段 + +#### path + +==必填== + +!!! note "" + + 自 sing-box 1.10.0 起,文件更改时将自动重新加载。 + +规则集的文件路径。 + +### 远程字段 + +#### url + +==必填== + +规则集的下载 URL。 + +#### download_detour + +用于下载规则集的出站的标签。 + +如果为空,将使用默认出站。 + +#### update_interval + +规则集的更新间隔。 + +默认使用 `1d`。 diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 00000000..47e0e245 --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,54 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: version `5` + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: version `4` + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: version `3` + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: version `2` + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 3, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of rule-set. + +* 1: sing-box 1.8.0: Initial rule-set version. +* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets. +* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items. +* 4: sing-box 1.13.0: Added `network_interface_address` and `default_interface_address` rule items. +* 5: sing-box 1.14.0: Added `package_name_regex` rule item. + +#### rules + +==Required== + +List of [Headless Rule](../headless-rule/). diff --git a/docs/configuration/rule-set/source-format.zh.md b/docs/configuration/rule-set/source-format.zh.md new file mode 100644 index 00000000..3f710864 --- /dev/null +++ b/docs/configuration/rule-set/source-format.zh.md @@ -0,0 +1,54 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: version `5` + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: version `4` + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: version `3` + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: version `2` + +!!! question "自 sing-box 1.8.0 起" + +### 结构 + +```json +{ + "version": 3, + "rules": [] +} +``` + +### 编译 + +使用 `sing-box rule-set compile [--output .srs] .json` 以编译源文件为二进制规则集。 + +### 字段 + +#### version + +==必填== + +规则集版本。 + +* 1: sing-box 1.8.0: 初始规则集版本。 +* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。 +* 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。 +* 4: sing-box 1.13.0: 添加了 `network_interface_address` 和 `default_interface_address` 规则项。 +* 5: sing-box 1.14.0: 添加了 `package_name_regex` 规则项。 + +#### rules + +==必填== + +一组 [无头规则](../headless-rule/). diff --git a/docs/configuration/service/ccm.md b/docs/configuration/service/ccm.md new file mode 100644 index 00000000..337cacb1 --- /dev/null +++ b/docs/configuration/service/ccm.md @@ -0,0 +1,131 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.13.0" + +# CCM + +CCM (Claude Code Multiplexer) service is a multiplexing service that allows you to access your local Claude Code subscription remotely through custom tokens. + +It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable. + +### Structure + +```json +{ + "type": "ccm", + + ... // Listen Fields + + "credential_path": "", + "usages_path": "", + "users": [], + "headers": {}, + "detour": "", + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### credential_path + +Path to the Claude Code OAuth credentials file. + +If not specified, defaults to: +- `$CLAUDE_CONFIG_DIR/.credentials.json` if `CLAUDE_CONFIG_DIR` environment variable is set +- `~/.claude/.credentials.json` otherwise + +On macOS, credentials are read from the system keychain first, then fall back to the file if unavailable. + +Refreshed tokens are automatically written back to the same location. + +#### usages_path + +Path to the file for storing aggregated API usage statistics. + +Usage tracking is disabled if not specified. + +When enabled, the service tracks and saves comprehensive statistics including: +- Request counts +- Token usage (input, output, cache read, cache creation) +- Calculated costs in USD based on Claude API pricing + +Statistics are organized by model, context window (200k standard vs 1M premium), and optionally by user when authentication is enabled. + +The statistics file is automatically saved every minute and upon service shutdown. + +#### users + +List of authorized users for token authentication. + +If empty, no authentication is required. + +Object format: + +```json +{ + "name": "", + "token": "" +} +``` + +Object fields: + +- `name`: Username identifier for tracking purposes. +- `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value. + +#### headers + +Custom HTTP headers to send to the Claude API. + +These headers will override any existing headers with the same name. + +#### detour + +Outbound tag for connecting to the Claude API. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +### Example + +#### Server + +```json +{ + "services": [ + { + "type": "ccm", + "listen": "0.0.0.0", + "listen_port": 8080, + "usages_path": "./claude-usages.json", + "users": [ + { + "name": "alice", + "token": "ak-ccm-hello-world" + }, + { + "name": "bob", + "token": "ak-ccm-hello-bob" + } + ] + } + ] +} +``` + +#### Client + +```bash +export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" +export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world" + +claude +``` diff --git a/docs/configuration/service/ccm.zh.md b/docs/configuration/service/ccm.zh.md new file mode 100644 index 00000000..f6490b5e --- /dev/null +++ b/docs/configuration/service/ccm.zh.md @@ -0,0 +1,131 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.13.0 起" + +# CCM + +CCM(Claude Code 多路复用器)服务是一个多路复用服务,允许您通过自定义令牌远程访问本地的 Claude Code 订阅。 + +它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。 + +### 结构 + +```json +{ + "type": "ccm", + + ... // 监听字段 + + "credential_path": "", + "usages_path": "", + "users": [], + "headers": {}, + "detour": "", + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 + +### 字段 + +#### credential_path + +Claude Code OAuth 凭据文件的路径。 + +如果未指定,默认值为: +- 如果设置了 `CLAUDE_CONFIG_DIR` 环境变量,则使用 `$CLAUDE_CONFIG_DIR/.credentials.json` +- 否则使用 `~/.claude/.credentials.json` + +在 macOS 上,首先从系统钥匙串读取凭据,如果不可用则回退到文件。 + +刷新的令牌会自动写回相同位置。 + +#### usages_path + +用于存储聚合 API 使用统计信息的文件路径。 + +如果未指定,使用跟踪将被禁用。 + +启用后,服务会跟踪并保存全面的统计信息,包括: +- 请求计数 +- 令牌使用量(输入、输出、缓存读取、缓存创建) +- 基于 Claude API 定价计算的美元成本 + +统计信息按模型、上下文窗口(200k 标准版 vs 1M 高级版)以及可选的用户(启用身份验证时)进行组织。 + +统计文件每分钟自动保存一次,并在服务关闭时保存。 + +#### users + +用于令牌身份验证的授权用户列表。 + +如果为空,则不需要身份验证。 + +对象格式: + +```json +{ + "name": "", + "token": "" +} +``` + +对象字段: + +- `name`:用于跟踪的用户名标识符。 +- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。 + +#### headers + +发送到 Claude API 的自定义 HTTP 头。 + +这些头会覆盖同名的现有头。 + +#### detour + +用于连接 Claude API 的出站标签。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +### 示例 + +#### 服务端 + +```json +{ + "services": [ + { + "type": "ccm", + "listen": "0.0.0.0", + "listen_port": 8080, + "usages_path": "./claude-usages.json", + "users": [ + { + "name": "alice", + "token": "ak-ccm-hello-world" + }, + { + "name": "bob", + "token": "ak-ccm-hello-bob" + } + ] + } + ] +} +``` + +#### 客户端 + +```bash +export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" +export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world" + +claude +``` diff --git a/docs/configuration/service/derp.md b/docs/configuration/service/derp.md new file mode 100644 index 00000000..3d7443a3 --- /dev/null +++ b/docs/configuration/service/derp.md @@ -0,0 +1,135 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# DERP + +DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper). + +### Structure + +```json +{ + "type": "derp", + + ... // Listen Fields + + "tls": {}, + "config_path": "", + "verify_client_endpoint": [], + "verify_client_url": [], + "home": "", + "mesh_with": [], + "mesh_psk": "", + "mesh_psk_file": "", + "stun": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +#### config_path + +==Required== + +Derper configuration file path. + +Example: `derper.key` + +#### verify_client_endpoint + +Tailscale endpoints tags to verify clients. + +#### verify_client_url + +URL to verify clients. + +Object format: + +```json +{ + "url": "https://my-headscale.com/verify", + + ... // Dial Fields +} +``` + +Setting Array value to a string `__URL__` is equivalent to configuring: + +```json +{ "url": __URL__ } +``` + +#### home + +What to serve at the root path. It may be left empty (the default, for a default homepage), `blank` for a blank page, or a URL to redirect to + +#### mesh_with + +Mesh with other DERP servers. + +Object format: + +```json +{ + "server": "", + "server_port": "", + "host": "", + "tls": {}, + + ... // Dial Fields +} +``` + +Object fields: + +- `server`: **Required** DERP server address. +- `server_port`: **Required** DERP server port. +- `host`: Custom DERP hostname. +- `tls`: [TLS](/configuration/shared/tls/#outbound) +- `Dial Fields`: [Dial Fields](/configuration/shared/dial/) + +#### mesh_psk + +Pre-shared key for DERP mesh. + +#### mesh_psk_file + +Pre-shared key file for DERP mesh. + +#### stun + +STUN server listen options. + +Object format: + +```json +{ + "enabled": true, + + ... // Listen Fields +} +``` + +Object fields: + +- `enabled`: **Required** Enable STUN server. +- `listen`: **Required** STUN server listen address, default to `::`. +- `listen_port`: **Required** STUN server listen port, default to `3478`. +- `other Listen Fields`: [Listen Fields](/configuration/shared/listen/) + +Setting `stun` value to a number `__PORT__` is equivalent to configuring: + +```json +{ "enabled": true, "listen_port": __PORT__ } +``` diff --git a/docs/configuration/service/derp.zh.md b/docs/configuration/service/derp.zh.md new file mode 100644 index 00000000..b22ff413 --- /dev/null +++ b/docs/configuration/service/derp.zh.md @@ -0,0 +1,135 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# DERP + +DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.go.dev/tailscale.com/cmd/derper)。 + +### 结构 + +```json +{ + "type": "derp", + + ... // 监听字段 + + "tls": {}, + "config_path": "", + "verify_client_endpoint": [], + "verify_client_url": [], + "home": "", + "mesh_with": [], + "mesh_psk": "", + "mesh_psk_file": "", + "stun": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 + +### 字段 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +#### config_path + +==必填== + +Derper 配置文件路径。 + +示例:`derper.key` + +#### verify_client_endpoint + +用于验证客户端的 Tailscale 端点标签。 + +#### verify_client_url + +用于验证客户端的 URL。 + +对象格式: + +```json +{ + "url": "https://my-headscale.com/verify", + + ... // 拨号字段 +} +``` + +将数组值设置为字符串 `__URL__` 等同于配置: + +```json +{ "url": __URL__ } +``` + +#### home + +在根路径提供的内容。可以留空(默认值,显示默认主页)、`blank` 显示空白页面,或一个重定向的 URL。 + +#### mesh_with + +与其他 DERP 服务器组网。 + +对象格式: + +```json +{ + "server": "", + "server_port": "", + "host": "", + "tls": {}, + + ... // 拨号字段 +} +``` + +对象字段: + +- `server`:**必填** DERP 服务器地址。 +- `server_port`:**必填** DERP 服务器端口。 +- `host`:自定义 DERP 主机名。 +- `tls`:[TLS](/zh/configuration/shared/tls/#出站) +- `拨号字段`:[拨号字段](/zh/configuration/shared/dial/) + +#### mesh_psk + +DERP 组网的预共享密钥。 + +#### mesh_psk_file + +DERP 组网的预共享密钥文件。 + +#### stun + +STUN 服务器监听选项。 + +对象格式: + +```json +{ + "enabled": true, + + ... // 监听字段 +} +``` + +对象字段: + +- `enabled`:**必填** 启用 STUN 服务器。 +- `listen`:**必填** STUN 服务器监听地址,默认为 `::`。 +- `listen_port`:**必填** STUN 服务器监听端口,默认为 `3478`。 +- `其他监听字段`:[监听字段](/zh/configuration/shared/listen/) + +将 `stun` 值设置为数字 `__PORT__` 等同于配置: + +```json +{ "enabled": true, "listen_port": __PORT__ } +``` \ No newline at end of file diff --git a/docs/configuration/service/index.md b/docs/configuration/service/index.md new file mode 100644 index 00000000..de3583b2 --- /dev/null +++ b/docs/configuration/service/index.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# Service + +### Structure + +```json +{ + "services": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | +|------------|------------------------| +| `ccm` | [CCM](./ccm) | +| `derp` | [DERP](./derp) | +| `ocm` | [OCM](./ocm) | +| `resolved` | [Resolved](./resolved) | +| `ssm-api` | [SSM API](./ssm-api) | + +#### tag + +The tag of the endpoint. diff --git a/docs/configuration/service/index.zh.md b/docs/configuration/service/index.zh.md new file mode 100644 index 00000000..a0d18cbb --- /dev/null +++ b/docs/configuration/service/index.zh.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# 服务 + +### 结构 + +```json +{ + "services": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | +|-----------|------------------------| +| `ccm` | [CCM](./ccm) | +| `derp` | [DERP](./derp) | +| `ocm` | [OCM](./ocm) | +| `resolved`| [Resolved](./resolved) | +| `ssm-api` | [SSM API](./ssm-api) | + +#### tag + +端点的标签。 \ No newline at end of file diff --git a/docs/configuration/service/ocm.md b/docs/configuration/service/ocm.md new file mode 100644 index 00000000..5fdf2b6b --- /dev/null +++ b/docs/configuration/service/ocm.md @@ -0,0 +1,185 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.13.0" + +# OCM + +OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you to access your local OpenAI Codex subscription remotely through custom tokens. + +It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens. + +### Structure + +```json +{ + "type": "ocm", + + ... // Listen Fields + + "credential_path": "", + "usages_path": "", + "users": [], + "headers": {}, + "detour": "", + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### credential_path + +Path to the OpenAI OAuth credentials file. + +If not specified, defaults to: +- `$CODEX_HOME/auth.json` if `CODEX_HOME` environment variable is set +- `~/.codex/auth.json` otherwise + +Refreshed tokens are automatically written back to the same location. + +#### usages_path + +Path to the file for storing aggregated API usage statistics. + +Usage tracking is disabled if not specified. + +When enabled, the service tracks and saves comprehensive statistics including: +- Request counts +- Token usage (input, output, cached) +- Calculated costs in USD based on OpenAI API pricing + +Statistics are organized by model and optionally by user when authentication is enabled. + +The statistics file is automatically saved every minute and upon service shutdown. + +#### users + +List of authorized users for token authentication. + +If empty, no authentication is required. + +Object format: + +```json +{ + "name": "", + "token": "" +} +``` + +Object fields: + +- `name`: Username identifier for tracking purposes. +- `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer ` header. + +#### headers + +Custom HTTP headers to send to the OpenAI API. + +These headers will override any existing headers with the same name. + +#### detour + +Outbound tag for connecting to the OpenAI API. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +### Example + +#### Server + +```json +{ + "services": [ + { + "type": "ocm", + "listen": "127.0.0.1", + "listen_port": 8080 + } + ] +} +``` + +#### Client + +Add to `~/.codex/config.toml`: + +```toml +# profile = "ocm" # set as default profile + +[model_providers.ocm] +name = "OCM Proxy" +base_url = "http://127.0.0.1:8080/v1" +supports_websockets = true + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # if the latest model is not yet publicly released +# model_reasoning_effort = "xhigh" +``` + +Then run: + +```bash +codex --profile ocm +``` + +### Example with Authentication + +#### Server + +```json +{ + "services": [ + { + "type": "ocm", + "listen": "0.0.0.0", + "listen_port": 8080, + "usages_path": "./codex-usages.json", + "users": [ + { + "name": "alice", + "token": "sk-ocm-hello-world" + }, + { + "name": "bob", + "token": "sk-ocm-hello-bob" + } + ] + } + ] +} +``` + +#### Client + +Add to `~/.codex/config.toml`: + +```toml +# profile = "ocm" # set as default profile + +[model_providers.ocm] +name = "OCM Proxy" +base_url = "http://127.0.0.1:8080/v1" +supports_websockets = true +experimental_bearer_token = "sk-ocm-hello-world" + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # if the latest model is not yet publicly released +# model_reasoning_effort = "xhigh" +``` + +Then run: + +```bash +codex --profile ocm +``` diff --git a/docs/configuration/service/ocm.zh.md b/docs/configuration/service/ocm.zh.md new file mode 100644 index 00000000..90394006 --- /dev/null +++ b/docs/configuration/service/ocm.zh.md @@ -0,0 +1,186 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.13.0 起" + +# OCM + +OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许您通过自定义令牌远程访问本地的 OpenAI Codex 订阅。 + +它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。 + +### 结构 + +```json +{ + "type": "ocm", + + ... // 监听字段 + + "credential_path": "", + "usages_path": "", + "users": [], + "headers": {}, + "detour": "", + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 + +### 字段 + +#### credential_path + +OpenAI OAuth 凭据文件的路径。 + +如果未指定,默认值为: +- 如果设置了 `CODEX_HOME` 环境变量,则使用 `$CODEX_HOME/auth.json` +- 否则使用 `~/.codex/auth.json` + +刷新的令牌会自动写回相同位置。 + +#### usages_path + +用于存储聚合 API 使用统计信息的文件路径。 + +如果未指定,使用跟踪将被禁用。 + +启用后,服务会跟踪并保存全面的统计信息,包括: +- 请求计数 +- 令牌使用量(输入、输出、缓存) +- 基于 OpenAI API 定价计算的美元成本 + +统计信息按模型以及可选的用户(启用身份验证时)进行组织。 + +统计文件每分钟自动保存一次,并在服务关闭时保存。 + +#### users + +用于令牌身份验证的授权用户列表。 + +如果为空,则不需要身份验证。 + +对象格式: + +```json +{ + "name": "", + "token": "" +} +``` + +对象字段: + +- `name`:用于跟踪的用户名标识符。 +- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer ` 头进行身份验证。 + +#### headers + +发送到 OpenAI API 的自定义 HTTP 头。 + +这些头会覆盖同名的现有头。 + +#### detour + +用于连接 OpenAI API 的出站标签。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +### 示例 + +#### 服务端 + +```json +{ + "services": [ + { + "type": "ocm", + "listen": "127.0.0.1", + "listen_port": 8080 + } + ] +} +``` + +#### 客户端 + +在 `~/.codex/config.toml` 中添加: + +```toml +# profile = "ocm" # 设为默认配置 + + +[model_providers.ocm] +name = "OCM Proxy" +base_url = "http://127.0.0.1:8080/v1" +supports_websockets = true + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # 如果最新模型尚未公开发布 +# model_reasoning_effort = "xhigh" +``` + +然后运行: + +```bash +codex --profile ocm +``` + +### 带身份验证的示例 + +#### 服务端 + +```json +{ + "services": [ + { + "type": "ocm", + "listen": "0.0.0.0", + "listen_port": 8080, + "usages_path": "./codex-usages.json", + "users": [ + { + "name": "alice", + "token": "sk-ocm-hello-world" + }, + { + "name": "bob", + "token": "sk-ocm-hello-bob" + } + ] + } + ] +} +``` + +#### 客户端 + +在 `~/.codex/config.toml` 中添加: + +```toml +# profile = "ocm" # 设为默认配置 + +[model_providers.ocm] +name = "OCM Proxy" +base_url = "http://127.0.0.1:8080/v1" +supports_websockets = true +experimental_bearer_token = "sk-ocm-hello-world" + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # 如果最新模型尚未公开发布 +# model_reasoning_effort = "xhigh" +``` + +然后运行: + +```bash +codex --profile ocm +``` diff --git a/docs/configuration/service/resolved.md b/docs/configuration/service/resolved.md new file mode 100644 index 00000000..47b7e255 --- /dev/null +++ b/docs/configuration/service/resolved.md @@ -0,0 +1,44 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# Resolved + +Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs +(e.g. NetworkManager) and provide DNS resolution. + +See also: [Resolved DNS Server](/configuration/dns/server/resolved/) + +### Structure + +```json +{ + "type": "resolved", + + ... // Listen Fields +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### listen + +==Required== + +Listen address. + +`127.0.0.53` will be used by default. + +#### listen_port + +==Required== + +Listen port. + +`53` will be used by default. diff --git a/docs/configuration/service/resolved.zh.md b/docs/configuration/service/resolved.zh.md new file mode 100644 index 00000000..b8af4e95 --- /dev/null +++ b/docs/configuration/service/resolved.zh.md @@ -0,0 +1,44 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# Resolved + +Resolved 服务是一个伪造的 systemd-resolved DBUS 服务,用于从其他程序 +(如 NetworkManager)接收 DNS 设置并提供 DNS 解析。 + +另请参阅:[Resolved DNS 服务器](/zh/configuration/dns/server/resolved/) + +### 结构 + +```json +{ + "type": "resolved", + + ... // 监听字段 +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 + +### 字段 + +#### listen + +==必填== + +监听地址。 + +默认使用 `127.0.0.53`。 + +#### listen_port + +==必填== + +监听端口。 + +默认使用 `53`。 \ No newline at end of file diff --git a/docs/configuration/service/ssm-api.md b/docs/configuration/service/ssm-api.md new file mode 100644 index 00000000..1ef9f373 --- /dev/null +++ b/docs/configuration/service/ssm-api.md @@ -0,0 +1,58 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.12.0" + +# SSM API + +SSM API service is a RESTful API server for managing Shadowsocks servers. + +See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadowsocks-server-management-api-v1.md + +### Structure + +```json +{ + "type": "ssm-api", + + ... // Listen Fields + + "servers": {}, + "cache_path": "", + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### servers + +==Required== + +A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inbound/shadowsocks) tags. + +Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled. + +Example: + +```json +{ + "servers": { + "/": "ss-in" + } +} +``` + +#### cache_path + +If set, when the server is about to stop, traffic and user state will be saved to the specified JSON file +to be restored on the next startup. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). diff --git a/docs/configuration/service/ssm-api.zh.md b/docs/configuration/service/ssm-api.zh.md new file mode 100644 index 00000000..fbe45ebb --- /dev/null +++ b/docs/configuration/service/ssm-api.zh.md @@ -0,0 +1,58 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.12.0 起" + +# SSM API + +SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务器。 + +参阅 https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadowsocks-server-management-api-v1.md + +### 结构 + +```json +{ + "type": "ssm-api", + + ... // 监听字段 + + "servers": {}, + "cache_path": "", + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 + +### 字段 + +#### servers + +==必填== + +从 HTTP 端点到 [Shadowsocks 入站](/zh/configuration/inbound/shadowsocks) 标签的映射对象。 + +选定的 Shadowsocks 入站必须配置启用 [managed](/zh/configuration/inbound/shadowsocks#managed)。 + +示例: + +```json +{ + "servers": { + "/": "ss-in" + } +} +``` + +#### cache_path + +如果设置,当服务器即将停止时,流量和用户状态将保存到指定的 JSON 文件中, +以便在下次启动时恢复。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/shared/certificate-provider/acme.md b/docs/configuration/shared/certificate-provider/acme.md new file mode 100644 index 00000000..440ed156 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/acme.md @@ -0,0 +1,150 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [account_key](#account_key) + :material-plus: [key_type](#key_type) + :material-plus: [detour](#detour) + +# ACME + +!!! quote "" + + `with_acme` build tag required. + +### Structure + +```json +{ + "type": "acme", + "tag": "", + + "domain": [], + "data_directory": "", + "default_server_name": "", + "email": "", + "provider": "", + "account_key": "", + "disable_http_challenge": false, + "disable_tls_alpn_challenge": false, + "alternative_http_port": 0, + "alternative_tls_port": 0, + "external_account": { + "key_id": "", + "mac_key": "" + }, + "dns01_challenge": {}, + "key_type": "", + "detour": "" +} +``` + +### Fields + +#### domain + +==Required== + +List of domains. + +#### data_directory + +The directory to store ACME data. + +`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty. + +#### default_server_name + +Server name to use when choosing a certificate if the ClientHello's ServerName field is empty. + +#### email + +The email address to use when creating or selecting an existing ACME server account. + +#### provider + +The ACME CA provider to use. + +| Value | Provider | +|-------------------------|---------------| +| `letsencrypt (default)` | Let's Encrypt | +| `zerossl` | ZeroSSL | +| `https://...` | Custom | + +When `provider` is `zerossl`, sing-box will automatically request ZeroSSL EAB credentials if `email` is set and +`external_account` is empty. + +When `provider` is `zerossl`, at least one of `external_account`, `email`, or `account_key` is required. + +#### account_key + +!!! question "Since sing-box 1.14.0" + +The PEM-encoded private key of an existing ACME account. + +#### disable_http_challenge + +Disable all HTTP challenges. + +#### disable_tls_alpn_challenge + +Disable all TLS-ALPN challenges + +#### alternative_http_port + +The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a +listener for the HTTP challenge. + +#### alternative_tls_port + +The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to +succeed. + +#### external_account + +EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known +by the CA. + +External account bindings are used to associate an ACME account with an existing account in a non-ACME system, such as +a CA customer database. + +To enable ACME account binding, the CA operating the ACME server needs to provide the ACME client with a MAC key and a +key identifier, using some mechanism outside of ACME. §7.3.4 + +#### external_account.key_id + +The key identifier. + +#### external_account.mac_key + +The MAC key. + +#### dns01_challenge + +ACME DNS01 challenge field. If configured, other challenge methods will be disabled. + +See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details. + +#### key_type + +!!! question "Since sing-box 1.14.0" + +The private key type to generate for new certificates. + +| Value | Type | +|------------|---------| +| `ed25519` | Ed25519 | +| `p256` | P-256 | +| `p384` | P-384 | +| `rsa2048` | RSA | +| `rsa4096` | RSA | + +#### detour + +!!! question "Since sing-box 1.14.0" + +The tag of the upstream outbound. + +All provider HTTP requests will use this outbound. diff --git a/docs/configuration/shared/certificate-provider/acme.zh.md b/docs/configuration/shared/certificate-provider/acme.zh.md new file mode 100644 index 00000000..d95930a5 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/acme.zh.md @@ -0,0 +1,145 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [account_key](#account_key) + :material-plus: [key_type](#key_type) + :material-plus: [detour](#detour) + +# ACME + +!!! quote "" + + 需要 `with_acme` 构建标签。 + +### 结构 + +```json +{ + "type": "acme", + "tag": "", + + "domain": [], + "data_directory": "", + "default_server_name": "", + "email": "", + "provider": "", + "account_key": "", + "disable_http_challenge": false, + "disable_tls_alpn_challenge": false, + "alternative_http_port": 0, + "alternative_tls_port": 0, + "external_account": { + "key_id": "", + "mac_key": "" + }, + "dns01_challenge": {}, + "key_type": "", + "detour": "" +} +``` + +### 字段 + +#### domain + +==必填== + +域名列表。 + +#### data_directory + +ACME 数据存储目录。 + +如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。 + +#### default_server_name + +如果 ClientHello 的 ServerName 字段为空,则选择证书时要使用的服务器名称。 + +#### email + +创建或选择现有 ACME 服务器帐户时使用的电子邮件地址。 + +#### provider + +要使用的 ACME CA 提供商。 + +| 值 | 提供商 | +|--------------------|---------------| +| `letsencrypt (默认)` | Let's Encrypt | +| `zerossl` | ZeroSSL | +| `https://...` | 自定义 | + +当 `provider` 为 `zerossl` 时,如果设置了 `email` 且未设置 `external_account`, +sing-box 会自动向 ZeroSSL 请求 EAB 凭据。 + +当 `provider` 为 `zerossl` 时,必须至少设置 `external_account`、`email` 或 `account_key` 之一。 + +#### account_key + +!!! question "自 sing-box 1.14.0 起" + +现有 ACME 帐户的 PEM 编码私钥。 + +#### disable_http_challenge + +禁用所有 HTTP 质询。 + +#### disable_tls_alpn_challenge + +禁用所有 TLS-ALPN 质询。 + +#### alternative_http_port + +用于 ACME HTTP 质询的备用端口;如果非空,将使用此端口而不是 80 来启动 HTTP 质询的侦听器。 + +#### alternative_tls_port + +用于 ACME TLS-ALPN 质询的备用端口; 系统必须将 443 转发到此端口以使质询成功。 + +#### external_account + +EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。 + +外部帐户绑定用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。 + +为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4 + +#### external_account.key_id + +密钥标识符。 + +#### external_account.mac_key + +MAC 密钥。 + +#### dns01_challenge + +ACME DNS01 质询字段。如果配置,将禁用其他质询方法。 + +参阅 [DNS01 质询字段](/zh/configuration/shared/dns01_challenge/)。 + +#### key_type + +!!! question "自 sing-box 1.14.0 起" + +为新证书生成的私钥类型。 + +| 值 | 类型 | +|-----------|----------| +| `ed25519` | Ed25519 | +| `p256` | P-256 | +| `p384` | P-384 | +| `rsa2048` | RSA | +| `rsa4096` | RSA | + +#### detour + +!!! question "自 sing-box 1.14.0 起" + +上游出站的标签。 + +所有提供者 HTTP 请求将使用此出站。 diff --git a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md new file mode 100644 index 00000000..cfd2da4f --- /dev/null +++ b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md @@ -0,0 +1,82 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Cloudflare Origin CA + +### Structure + +```json +{ + "type": "cloudflare-origin-ca", + "tag": "", + + "domain": [], + "data_directory": "", + "api_token": "", + "origin_ca_key": "", + "request_type": "", + "requested_validity": 0, + "detour": "" +} +``` + +### Fields + +#### domain + +==Required== + +List of domain names or wildcard domain names to include in the certificate. + +#### data_directory + +Root directory used to store the issued certificate, private key, and metadata. + +If empty, sing-box uses the same default data directory as the ACME certificate provider: +`$XDG_DATA_HOME/certmagic` or `$HOME/.local/share/certmagic`. + +#### api_token + +Cloudflare API token used to create the certificate. + +Get or create one in [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens). + +Requires the `Zone / SSL and Certificates / Edit` permission. + +Conflict with `origin_ca_key`. + +#### origin_ca_key + +Cloudflare Origin CA Key. + +Get it in [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens). + +Conflict with `api_token`. + +#### request_type + +The signature type to request from Cloudflare. + +| Value | Type | +|----------------------|-------------| +| `origin-rsa` | RSA | +| `origin-ecc` | ECDSA P-256 | + +`origin-rsa` is used if empty. + +#### requested_validity + +The requested certificate validity in days. + +Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`. + +`5475` days (15 years) is used if empty. + +#### detour + +The tag of the upstream outbound. + +All provider HTTP requests will use this outbound. diff --git a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md new file mode 100644 index 00000000..85036268 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md @@ -0,0 +1,82 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# Cloudflare Origin CA + +### 结构 + +```json +{ + "type": "cloudflare-origin-ca", + "tag": "", + + "domain": [], + "data_directory": "", + "api_token": "", + "origin_ca_key": "", + "request_type": "", + "requested_validity": 0, + "detour": "" +} +``` + +### 字段 + +#### domain + +==必填== + +要写入证书的域名或通配符域名列表。 + +#### data_directory + +保存签发证书、私钥和元数据的根目录。 + +如果为空,sing-box 会使用与 ACME 证书提供者相同的默认数据目录: +`$XDG_DATA_HOME/certmagic` 或 `$HOME/.local/share/certmagic`。 + +#### api_token + +用于创建证书的 Cloudflare API Token。 + +可在 [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens) 获取或创建。 + +需要 `Zone / SSL and Certificates / Edit` 权限。 + +与 `origin_ca_key` 冲突。 + +#### origin_ca_key + +Cloudflare Origin CA Key。 + +可在 [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens) 获取。 + +与 `api_token` 冲突。 + +#### request_type + +向 Cloudflare 请求的签名类型。 + +| 值 | 类型 | +|----------------------|-------------| +| `origin-rsa` | RSA | +| `origin-ecc` | ECDSA P-256 | + +如果为空,使用 `origin-rsa`。 + +#### requested_validity + +请求的证书有效期,单位为天。 + +可用值:`7`、`30`、`90`、`365`、`730`、`1095`、`5475`。 + +如果为空,使用 `5475` 天(15 年)。 + +#### detour + +上游出站的标签。 + +所有提供者 HTTP 请求将使用此出站。 diff --git a/docs/configuration/shared/certificate-provider/index.md b/docs/configuration/shared/certificate-provider/index.md new file mode 100644 index 00000000..c493550a --- /dev/null +++ b/docs/configuration/shared/certificate-provider/index.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Certificate Provider + +### Structure + +```json +{ + "certificate_providers": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | +|--------|------------------| +| `acme` | [ACME](/configuration/shared/certificate-provider/acme) | +| `tailscale` | [Tailscale](/configuration/shared/certificate-provider/tailscale) | +| `cloudflare-origin-ca` | [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca) | + +#### tag + +The tag of the certificate provider. diff --git a/docs/configuration/shared/certificate-provider/index.zh.md b/docs/configuration/shared/certificate-provider/index.zh.md new file mode 100644 index 00000000..2df4b363 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/index.zh.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# 证书提供者 + +### 结构 + +```json +{ + "certificate_providers": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | +|--------|------------------| +| `acme` | [ACME](/zh/configuration/shared/certificate-provider/acme) | +| `tailscale` | [Tailscale](/zh/configuration/shared/certificate-provider/tailscale) | +| `cloudflare-origin-ca` | [Cloudflare Origin CA](/zh/configuration/shared/certificate-provider/cloudflare-origin-ca) | + +#### tag + +证书提供者的标签。 diff --git a/docs/configuration/shared/certificate-provider/tailscale.md b/docs/configuration/shared/certificate-provider/tailscale.md new file mode 100644 index 00000000..045f2c5e --- /dev/null +++ b/docs/configuration/shared/certificate-provider/tailscale.md @@ -0,0 +1,27 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Tailscale + +### Structure + +```json +{ + "type": "tailscale", + "tag": "ts-cert", + "endpoint": "ts-ep" +} +``` + +### Fields + +#### endpoint + +==Required== + +The tag of the [Tailscale endpoint](/configuration/endpoint/tailscale/) to reuse. + +[MagicDNS and HTTPS](https://tailscale.com/kb/1153/enabling-https) must be enabled in the Tailscale admin console. diff --git a/docs/configuration/shared/certificate-provider/tailscale.zh.md b/docs/configuration/shared/certificate-provider/tailscale.zh.md new file mode 100644 index 00000000..1987da50 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/tailscale.zh.md @@ -0,0 +1,27 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# Tailscale + +### 结构 + +```json +{ + "type": "tailscale", + "tag": "ts-cert", + "endpoint": "ts-ep" +} +``` + +### 字段 + +#### endpoint + +==必填== + +要复用的 [Tailscale 端点](/zh/configuration/endpoint/tailscale/) 的标签。 + +必须在 Tailscale 管理控制台中启用 [MagicDNS 和 HTTPS](https://tailscale.com/kb/1153/enabling-https)。 diff --git a/docs/configuration/shared/dial.md b/docs/configuration/shared/dial.md new file mode 100644 index 00000000..306952fc --- /dev/null +++ b/docs/configuration/shared/dial.md @@ -0,0 +1,270 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) + :material-plus: [tcp_keep_alive](#tcp_keep_alive) + :material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval) + :material-plus: [bind_address_no_port](#bind_address_no_port) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [domain_resolver](#domain_resolver) + :material-delete-clock: [domain_strategy](#domain_strategy) + :material-plus: [netns](#netns) + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [network_strategy](#network_strategy) + :material-alert: [fallback_delay](#fallback_delay) + :material-alert: [network_type](#network_type) + :material-alert: [fallback_network_type](#fallback_network_type) + +### Structure + +```json +{ + "detour": "", + "bind_interface": "", + "inet4_bind_address": "", + "inet6_bind_address": "", + "bind_address_no_port": false, + "routing_mark": 0, + "reuse_addr": false, + "netns": "", + "connect_timeout": "", + "tcp_fast_open": false, + "tcp_multi_path": false, + "disable_tcp_keep_alive": false, + "tcp_keep_alive": "", + "tcp_keep_alive_interval": "", + "udp_fragment": false, + + "domain_resolver": "", // or {} + "network_strategy": "", + "network_type": [], + "fallback_network_type": [], + "fallback_delay": "", + + // Deprecated + + "domain_strategy": "" +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### detour + +The tag of the upstream outbound. + +If enabled, all other fields will be ignored. + +#### bind_interface + +The network interface to bind to. + +#### inet4_bind_address + +The IPv4 address to bind to. + +#### inet6_bind_address + +The IPv6 address to bind to. + +#### bind_address_no_port + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux. + +Do not reserve a port when binding to a source address. + +This allows reusing the same source port for multiple connections if the full 4-tuple (source IP, source port, destination IP, destination port) remains unique. + +#### routing_mark + +!!! quote "" + + Only supported on Linux. + +Set netfilter routing mark. + +Integers (e.g. `1234`) and string hexadecimals (e.g. `"0x1234"`) are supported. + +#### reuse_addr + +Reuse listener address. + +#### netns + +!!! question "Since sing-box 1.12.0" + +!!! quote "" + + Only supported on Linux. + +Set network namespace, name or path. + +#### connect_timeout + +Connect timeout, in golang's Duration format. + +A duration string is a possibly signed sequence of +decimal numbers, each with optional fraction and a unit suffix, +such as "300ms", "-1.5h" or "2h45m". +Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + +#### tcp_fast_open + +Enable TCP Fast Open. + +#### tcp_multi_path + +!!! warning "" + + Go 1.21 required. + +Enable TCP Multi Path. + +#### disable_tcp_keep_alive + +!!! question "Since sing-box 1.13.0" + +Disable TCP keep alive. + +#### tcp_keep_alive + +!!! question "Since sing-box 1.13.0" + + Default value changed from `10m` to `5m`. + +TCP keep alive initial period. + +`5m` will be used by default. + +#### tcp_keep_alive_interval + +!!! question "Since sing-box 1.13.0" + +TCP keep alive interval. + +`75s` will be used by default. + +#### udp_fragment + +Enable UDP fragmentation. + +#### domain_resolver + +!!! warning "" + + `outbound` DNS rule items are deprecated and will be removed in sing-box 1.14.0, so this item will be required for outbound/endpoints using domain name in server address since sing-box 1.14.0. + +!!! info "" + + `domain_resolver` or `route.default_domain_resolver` is optional when only one DNS server is configured. + +Set domain resolver to use for resolving domain names. + +This option uses the same format as the [route DNS rule action](/configuration/dns/rule_action/#route) without the `action` field. + +Setting this option directly to a string is equivalent to setting `server` of this options. + +| Outbound/Endpoints | Effected domains | +|--------------------|--------------------------| +| `direct` | Domain in request | +| others | Domain in server address | + +#### network_strategy + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + +Strategy for selecting network interfaces. + +Available values: + +- `default` (default): Connect to default network or networks specified in `network_type` sequentially. +- `hybrid`: Connect to all networks or networks specified in `network_type` concurrently. +- `fallback`: Connect to default network or preferred networks specified in `network_type` concurrently, and try fallback networks when unavailable or timeout. + +For fallback, when preferred interfaces fails or times out, +it will enter a 15s fast fallback state (Connect to all preferred and fallback networks concurrently), +and exit immediately if preferred networks recover. + +Conflicts with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`. + +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + +Network types to use when using `default` or `hybrid` network strategy or +preferred network types to use when using `fallback` network strategy. + +Available values: `wifi`, `cellular`, `ethernet`, `other`. + +Device's default network is used by default. + +#### fallback_network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + +Fallback network types when preferred networks are unavailable or timeout when using `fallback` network strategy. + +All other networks expect preferred are used by default. + +#### fallback_delay + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + +The length of time to wait before spawning a RFC 6555 Fast Fallback connection. + +For `domain_strategy`, is the amount of time to wait for connection to succeed before assuming +that IPv4/IPv6 is misconfigured and falling back to other type of addresses. + +For `network_strategy`, is the amount of time to wait for connection to succeed before falling +back to other interfaces. + +Only take effect when `domain_strategy` or `network_strategy` is set. + +`300ms` is used by default. + +#### domain_strategy + +!!! failure "Deprecated in sing-box 1.12.0" + + `domain_strategy` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver). + +Available values: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. + +If set, the requested domain name will be resolved to IP before connect. + +| Outbound | Effected domains | Fallback Value | +|----------|--------------------------|-------------------------------------------| +| `direct` | Domain in request | Take `inbound.domain_strategy` if not set | +| others | Domain in server address | / | + diff --git a/docs/configuration/shared/dial.zh.md b/docs/configuration/shared/dial.zh.md new file mode 100644 index 00000000..daf7f8e0 --- /dev/null +++ b/docs/configuration/shared/dial.zh.md @@ -0,0 +1,258 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) + :material-plus: [tcp_keep_alive](#tcp_keep_alive) + :material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval) + :material-plus: [bind_address_no_port](#bind_address_no_port) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [domain_resolver](#domain_resolver) + :material-delete-clock: [domain_strategy](#domain_strategy) + :material-plus: [netns](#netns) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [network_strategy](#network_strategy) + :material-alert: [fallback_delay](#fallback_delay) + :material-alert: [network_type](#network_type) + :material-alert: [fallback_network_type](#fallback_network_type) + +### 结构 + +```json +{ + "detour": "", + "bind_interface": "", + "inet4_bind_address": "", + "inet6_bind_address": "", + "bind_address_no_port": false, + "routing_mark": 0, + "reuse_addr": false, + "netns": "", + "connect_timeout": "", + "tcp_fast_open": false, + "tcp_multi_path": false, + "disable_tcp_keep_alive": false, + "tcp_keep_alive": "", + "tcp_keep_alive_interval": "", + "udp_fragment": false, + + "domain_resolver": "", // 或 {} + "network_strategy": "", + "network_type": [], + "fallback_network_type": [], + "fallback_delay": "", + + // 废弃的 + + "domain_strategy": "" +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 字段 + +#### detour + +上游出站的标签。 + +启用时,其他拨号字段将被忽略。 + +#### bind_interface + +要绑定到的网络接口。 + +#### inet4_bind_address + +要绑定的 IPv4 地址。 + +#### inet6_bind_address + +要绑定的 IPv6 地址。 + +#### bind_address_no_port + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux。 + +绑定到源地址时不保留端口。 + +这允许在完整的四元组(源 IP、源端口、目标 IP、目标端口)保持唯一的情况下,为多个连接复用同一源端口。 + +#### routing_mark + +!!! quote "" + + 仅支持 Linux。 + +设置 netfilter 路由标记。 + +支持数字 (如 `1234`) 和十六进制字符串 (如 `"0x1234"`)。 + +#### reuse_addr + +重用监听地址。 + +#### netns + +!!! question "自 sing-box 1.12.0 起" + +!!! quote "" + + 仅支持 Linux。 + +设置网络命名空间,名称或路径。 + +#### connect_timeout + +连接超时,采用 golang 的 Duration 格式。 + +持续时间字符串是一个可能有符号的序列十进制数,每个都有可选的分数和单位后缀, 例如 "300ms"、"-1.5h" 或 "2h45m"。 +有效时间单位为 "ns"、"us"(或 "µs")、"ms"、"s"、"m"、"h"。 + +#### tcp_fast_open + +启用 TCP Fast Open。 + +#### tcp_multi_path + +!!! warning "" + + 需要 Go 1.21。 + +启用 TCP Multi Path。 + +#### disable_tcp_keep_alive + +!!! question "自 sing-box 1.13.0 起" + +禁用 TCP keep alive。 + +#### tcp_keep_alive + +!!! question "自 sing-box 1.13.0 起" + + 默认值从 `10m` 更改为 `5m`。 + +TCP keep alive 初始周期。 + +默认使用 `5m`。 + +#### tcp_keep_alive_interval + +!!! question "自 sing-box 1.13.0 起" + +TCP keep alive 间隔。 + +默认使用 `75s`。 + +#### udp_fragment + +启用 UDP 分段。 + +#### domain_resolver + +!!! warning "" + + `outbound` DNS 规则项已弃用,且将在 sing-box 1.14.0 中被移除。因此,从 sing-box 1.14.0 版本开始,所有在服务器地址中使用域名的出站/端点均需配置此项。 + +!!! info "" + + 当只有一个 DNS 服务器已配置时,`domain_resolver` 或 `route.default_domain_resolver` 是可选的。 + +用于设置解析域名的域名解析器。 + +此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 + +若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。 + +| 出站/端点 | 受影响的域名 | +|----------------|---------------------------| +| `direct` | 请求中的域名 | +| 其他类型 | 服务器地址中的域名 | + +#### network_strategy + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 + +用于选择网络接口的策略。 + +可用值: + +- `default`(默认值):按顺序连接默认网络或 `network_type` 中指定的网络。 +- `hybrid`:同时连接所有网络或 `network_type` 中指定的网络。 +- `fallback`:同时连接默认网络或 `network_type` 中指定的首选网络,当不可用或超时时尝试回退网络。 + +对于回退模式,当首选接口失败或超时时, +将进入15秒的快速回退状态(同时连接所有首选和回退网络), +如果首选网络恢复,则立即退出。 + +与 `bind_interface`, `bind_inet4_address` 和 `bind_inet6_address` 冲突。 + +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 + +当使用 `default` 或 `hybrid` 网络策略时要使用的网络类型,或当使用 `fallback` 网络策略时要使用的首选网络类型。 + +可用值:`wifi`, `cellular`, `ethernet`, `other`。 + +默认使用设备默认网络。 + +#### fallback_network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 + +当使用 `fallback` 网络策略时,在首选网络不可用或超时的情况下要使用的回退网络类型。 + +默认使用除首选网络外的所有其他网络。 + +#### fallback_delay + +在生成 RFC 6555 快速回退连接之前等待的时间长度。 + +对于 `domain_strategy`,是在假设之前等待 IPv6 成功的时间量如果设置了 "prefer_ipv4",则 IPv6 配置错误并回退到 IPv4。 + +对于 `network_strategy`,对于 `network_strategy`,是在回退到其他接口之前等待连接成功的时间。 + +仅当 `domain_strategy` 或 `network_strategy` 已设置时生效。 + +默认使用 `300ms`。 + +#### domain_strategy + +!!! failure "已在 sing-box 1.12.0 废弃" + + `domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移出站域名策略选项到域名解析器)。 + +可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 + +如果设置,域名将在请求发出之前解析为 IP。 + +| 出站 | 受影响的域名 | 默认回退值 | +|----------|-----------|---------------------------| +| `direct` | 请求中的域名 | `inbound.domain_strategy` | +| others | 服务器地址中的域名 | / | \ No newline at end of file diff --git a/docs/configuration/shared/dns01_challenge.md b/docs/configuration/shared/dns01_challenge.md new file mode 100644 index 00000000..0157cb45 --- /dev/null +++ b/docs/configuration/shared/dns01_challenge.md @@ -0,0 +1,126 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [ttl](#ttl) + :material-plus: [propagation_delay](#propagation_delay) + :material-plus: [propagation_timeout](#propagation_timeout) + :material-plus: [resolvers](#resolvers) + :material-plus: [override_domain](#override_domain) + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [alidns.security_token](#security_token) + :material-plus: [cloudflare.zone_token](#zone_token) + :material-plus: [acmedns](#acmedns) + +### Structure + +```json +{ + "ttl": "", + "propagation_delay": "", + "propagation_timeout": "", + "resolvers": [], + "override_domain": "", + "provider": "", + + ... // Provider Fields +} +``` + +### Fields + +#### ttl + +!!! question "Since sing-box 1.14.0" + +The TTL of the temporary TXT record used for the DNS challenge. + +#### propagation_delay + +!!! question "Since sing-box 1.14.0" + +How long to wait after creating the challenge record before starting propagation checks. + +#### propagation_timeout + +!!! question "Since sing-box 1.14.0" + +The maximum time to wait for the challenge record to propagate. + +Set to `-1` to disable propagation checks. + +#### resolvers + +!!! question "Since sing-box 1.14.0" + +Preferred DNS resolvers to use for DNS propagation checks. + +#### override_domain + +!!! question "Since sing-box 1.14.0" + +Override the domain name used for the DNS challenge record. + +Useful when `_acme-challenge` is delegated to a different zone. + +#### provider + +The DNS provider. See below for provider-specific fields. + +### Provider Fields + +#### Alibaba Cloud DNS + +```json +{ + "provider": "alidns", + "access_key_id": "", + "access_key_secret": "", + "region_id": "", + "security_token": "" +} +``` + +##### security_token + +!!! question "Since sing-box 1.13.0" + +The Security Token for STS temporary credentials. + +#### Cloudflare + +```json +{ + "provider": "cloudflare", + "api_token": "", + "zone_token": "" +} +``` + +##### zone_token + +!!! question "Since sing-box 1.13.0" + +Optional API token with `Zone:Read` permission. + +When provided, allows `api_token` to be scoped to a single zone. + +#### ACME-DNS + +!!! question "Since sing-box 1.13.0" + +```json +{ + "provider": "acmedns", + "username": "", + "password": "", + "subdomain": "", + "server_url": "" +} +``` + +See [ACME-DNS](https://github.com/joohoi/acme-dns) for details. diff --git a/docs/configuration/shared/dns01_challenge.zh.md b/docs/configuration/shared/dns01_challenge.zh.md new file mode 100644 index 00000000..8c582bb5 --- /dev/null +++ b/docs/configuration/shared/dns01_challenge.zh.md @@ -0,0 +1,126 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [ttl](#ttl) + :material-plus: [propagation_delay](#propagation_delay) + :material-plus: [propagation_timeout](#propagation_timeout) + :material-plus: [resolvers](#resolvers) + :material-plus: [override_domain](#override_domain) + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [alidns.security_token](#security_token) + :material-plus: [cloudflare.zone_token](#zone_token) + :material-plus: [acmedns](#acmedns) + +### 结构 + +```json +{ + "ttl": "", + "propagation_delay": "", + "propagation_timeout": "", + "resolvers": [], + "override_domain": "", + "provider": "", + + ... // 提供商字段 +} +``` + +### 字段 + +#### ttl + +!!! question "自 sing-box 1.14.0 起" + +DNS 质询临时 TXT 记录的 TTL。 + +#### propagation_delay + +!!! question "自 sing-box 1.14.0 起" + +创建质询记录后,在开始传播检查前要等待的时间。 + +#### propagation_timeout + +!!! question "自 sing-box 1.14.0 起" + +等待质询记录传播完成的最长时间。 + +设为 `-1` 可禁用传播检查。 + +#### resolvers + +!!! question "自 sing-box 1.14.0 起" + +进行 DNS 传播检查时优先使用的 DNS 解析器。 + +#### override_domain + +!!! question "自 sing-box 1.14.0 起" + +覆盖 DNS 质询记录使用的域名。 + +适用于将 `_acme-challenge` 委托到其他 zone 的场景。 + +#### provider + +DNS 提供商。提供商专有字段见下文。 + +### 提供商字段 + +#### Alibaba Cloud DNS + +```json +{ + "provider": "alidns", + "access_key_id": "", + "access_key_secret": "", + "region_id": "", + "security_token": "" +} +``` + +##### security_token + +!!! question "自 sing-box 1.13.0 起" + +用于 STS 临时凭证的安全令牌。 + +#### Cloudflare + +```json +{ + "provider": "cloudflare", + "api_token": "", + "zone_token": "" +} +``` + +##### zone_token + +!!! question "自 sing-box 1.13.0 起" + +具有 `Zone:Read` 权限的可选 API 令牌。 + +提供后可将 `api_token` 限定到单个区域。 + +#### ACME-DNS + +!!! question "自 sing-box 1.13.0 起" + +```json +{ + "provider": "acmedns", + "username": "", + "password": "", + "subdomain": "", + "server_url": "" +} +``` + +参阅 [ACME-DNS](https://github.com/joohoi/acme-dns)。 diff --git a/docs/configuration/shared/listen.md b/docs/configuration/shared/listen.md new file mode 100644 index 00000000..55325564 --- /dev/null +++ b/docs/configuration/shared/listen.md @@ -0,0 +1,202 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) + :material-alert: [tcp_keep_alive](#tcp_keep_alive) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [netns](#netns) + :material-plus: [bind_interface](#bind_interface) + :material-plus: [routing_mark](#routing_mark) + :material-plus: [reuse_addr](#reuse_addr) + +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-clock: [sniff](#sniff) + :material-delete-clock: [sniff_override_destination](#sniff_override_destination) + :material-delete-clock: [sniff_timeout](#sniff_timeout) + :material-delete-clock: [domain_strategy](#domain_strategy) + :material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping) + +### Structure + +```json +{ + "listen": "", + "listen_port": 0, + "bind_interface": "", + "routing_mark": 0, + "reuse_addr": false, + "netns": "", + "tcp_fast_open": false, + "tcp_multi_path": false, + "disable_tcp_keep_alive": false, + "tcp_keep_alive": "", + "tcp_keep_alive_interval": "", + "udp_fragment": false, + "udp_timeout": "", + "detour": "", + + // Deprecated + + "sniff": false, + "sniff_override_destination": false, + "sniff_timeout": "", + "domain_strategy": "", + "udp_disable_domain_unmapping": false +} +``` + +### Fields + +#### listen + +==Required== + +Listen address. + +#### listen_port + +Listen port. + +#### bind_interface + +!!! question "Since sing-box 1.12.0" + +The network interface to bind to. + +#### routing_mark + +!!! question "Since sing-box 1.12.0" + +!!! quote "" + + Only supported on Linux. + +Set netfilter routing mark. + +Integers (e.g. `1234`) and string hexadecimals (e.g. `"0x1234"`) are supported. + +#### reuse_addr + +!!! question "Since sing-box 1.12.0" + +Reuse listener address. + +#### netns + +!!! question "Since sing-box 1.12.0" + +!!! quote "" + + Only supported on Linux. + +Set network namespace, name or path. + +#### tcp_fast_open + +Enable TCP Fast Open. + +#### tcp_multi_path + +!!! warning "" + + Go 1.21 required. + +Enable TCP Multi Path. + +#### disable_tcp_keep_alive + +!!! question "Since sing-box 1.13.0" + +Disable TCP keep alive. + +#### tcp_keep_alive + +!!! question "Since sing-box 1.13.0" + + Default value changed from `10m` to `5m`. + +TCP keep alive initial period. + +`5m` will be used by default. + +#### tcp_keep_alive_interval + +TCP keep alive interval. + +`75s` will be used by default. + +#### udp_fragment + +Enable UDP fragmentation. + +#### udp_timeout + +UDP NAT expiration time. + +`5m` will be used by default. + +#### detour + +If set, connections will be forwarded to the specified inbound. + +Requires target inbound support, see [Injectable](/configuration/inbound/#fields). + +#### sniff + +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + +Enable sniffing. + +See [Protocol Sniff](/configuration/route/sniff/) for details. + +#### sniff_override_destination + +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0. + +Override the connection destination address with the sniffed domain. + +If the domain name is invalid (like tor), this will not work. + +#### sniff_timeout + +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + +Timeout for sniffing. + +`300ms` is used by default. + +#### domain_strategy + +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + +One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. + +If set, the requested domain name will be resolved to IP before routing. + +If `sniff_override_destination` is in effect, its value will be taken as a fallback. + +#### udp_disable_domain_unmapping + +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. + +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md new file mode 100644 index 00000000..0afcbc46 --- /dev/null +++ b/docs/configuration/shared/listen.zh.md @@ -0,0 +1,200 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) + :material-alert: [tcp_keep_alive](#tcp_keep_alive) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [netns](#netns) + :material-plus: [bind_interface](#bind_interface) + :material-plus: [routing_mark](#routing_mark) + :material-plus: [reuse_addr](#reuse_addr) + +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-clock: [sniff](#sniff) + :material-delete-clock: [sniff_override_destination](#sniff_override_destination) + :material-delete-clock: [sniff_timeout](#sniff_timeout) + :material-delete-clock: [domain_strategy](#domain_strategy) + :material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping) + +### 结构 + +```json +{ + "listen": "", + "listen_port": 0, + "bind_interface": "", + "routing_mark": 0, + "reuse_addr": false, + "netns": "", + "tcp_fast_open": false, + "tcp_multi_path": false, + "disable_tcp_keep_alive": false, + "tcp_keep_alive": "", + "tcp_keep_alive_interval": "", + "udp_fragment": false, + "udp_timeout": "", + "detour": "", + + // 废弃的 + + "sniff": false, + "sniff_override_destination": false, + "sniff_timeout": "", + "domain_strategy": "", + "udp_disable_domain_unmapping": false +} +``` + +### 字段 + +#### listen + +==必填== + +监听地址。 + +#### listen_port + +监听端口。 + +#### bind_interface + +!!! question "自 sing-box 1.12.0 起" + +要绑定到的网络接口。 + +#### routing_mark + +!!! question "自 sing-box 1.12.0 起" + +!!! quote "" + + 仅支持 Linux。 + +设置 netfilter 路由标记。 + +支持数字 (如 `1234`) 和十六进制字符串 (如 `"0x1234"`)。 + +#### reuse_addr + +!!! question "自 sing-box 1.12.0 起" + +重用监听地址。 + +#### netns + +!!! question "自 sing-box 1.12.0 起" + +!!! quote "" + + 仅支持 Linux。 + +设置网络命名空间,名称或路径。 + +#### tcp_fast_open + +启用 TCP Fast Open。 + +#### tcp_multi_path + +!!! warning "" + + 需要 Go 1.21。 + +启用 TCP Multi Path。 + +#### disable_tcp_keep_alive + +!!! question "自 sing-box 1.13.0 起" + +禁用 TCP keep alive。 + +#### tcp_keep_alive + +!!! question "自 sing-box 1.13.0 起" + + 默认值从 `10m` 更改为 `5m`。 + +TCP keep alive 初始周期。 + +默认使用 `5m`。 + +#### tcp_keep_alive_interval + +TCP keep alive 间隔。 + +默认使用 `75s`。 + +#### udp_fragment + +启用 UDP 分段。 + +#### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +#### detour + +如果设置,连接将被转发到指定的入站。 + +需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#字段)。 + +#### sniff + +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). + +启用协议探测。 + +参阅 [协议探测](/zh/configuration/route/sniff/) + +#### sniff_override_destination + +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除。 + +用探测出的域名覆盖连接目标地址。 + +如果域名无效(如 Tor),将不生效。 + +#### sniff_timeout + +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). + +探测超时时间。 + +默认使用 300ms。 + +#### domain_strategy + +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). + +可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 + +如果设置,请求的域名将在路由之前解析为 IP。 + +如果 `sniff_override_destination` 生效,它的值将作为后备。 + +#### udp_disable_domain_unmapping + +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). + +如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 + +此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 diff --git a/docs/configuration/shared/multiplex.md b/docs/configuration/shared/multiplex.md new file mode 100644 index 00000000..bf722127 --- /dev/null +++ b/docs/configuration/shared/multiplex.md @@ -0,0 +1,86 @@ +### Inbound + +```json +{ + "enabled": true, + "padding": false, + "brutal": {} +} +``` + +### Outbound + +```json +{ + "enabled": true, + "protocol": "smux", + "max_connections": 4, + "min_streams": 4, + "max_streams": 0, + "padding": false, + "brutal": {} +} +``` + + +### Inbound Fields + +#### enabled + +Enable multiplex support. + +#### padding + +If enabled, non-padded connections will be rejected. + +#### brutal + +See [TCP Brutal](/configuration/shared/tcp-brutal/) for details. + +### Outbound Fields + +#### enabled + +Enable multiplex. + +#### protocol + +Multiplex protocol. + +| Protocol | Description | +|----------|------------------------------------| +| smux | https://github.com/xtaci/smux | +| yamux | https://github.com/hashicorp/yamux | +| h2mux | https://golang.org/x/net/http2 | + +h2mux is used by default. + +#### max_connections + +Maximum connections. + +Conflict with `max_streams`. + +#### min_streams + +Minimum multiplexed streams in a connection before opening a new connection. + +Conflict with `max_streams`. + +#### max_streams + +Maximum multiplexed streams in a connection before opening a new connection. + +Conflict with `max_connections` and `min_streams`. + +#### padding + +!!! info + + Requires sing-box server version 1.3-beta9 or later. + +Enable padding. + +#### brutal + +See [TCP Brutal](/configuration/shared/tcp-brutal/) for details. diff --git a/docs/configuration/shared/multiplex.zh.md b/docs/configuration/shared/multiplex.zh.md new file mode 100644 index 00000000..124fe49b --- /dev/null +++ b/docs/configuration/shared/multiplex.zh.md @@ -0,0 +1,85 @@ +### 入站 + +```json +{ + "enabled": true, + "padding": false, + "brutal": {} +} +``` + +### 出站 + +```json +{ + "enabled": true, + "protocol": "smux", + "max_connections": 4, + "min_streams": 4, + "max_streams": 0, + "padding": false, + "brutal": {} +} +``` + +### 入站字段 + +#### enabled + +启用多路复用支持。 + +#### padding + +如果启用,将拒绝非填充连接。 + +#### brutal + +参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal/)。 + +### 出站字段 + +#### enabled + +启用多路复用。 + +#### protocol + +多路复用协议 + +| 协议 | 描述 | +|-------|------------------------------------| +| smux | https://github.com/xtaci/smux | +| yamux | https://github.com/hashicorp/yamux | +| h2mux | https://golang.org/x/net/http2 | + +默认使用 h2mux。 + +#### max_connections + +最大连接数量。 + +与 `max_streams` 冲突。 + +#### min_streams + +在打开新连接之前,连接中的最小多路复用流数量。 + +与 `max_streams` 冲突。 + +#### max_streams + +在打开新连接之前,连接中的最大多路复用流数量。 + +与 `max_connections` 和 `min_streams` 冲突。 + +#### padding + +!!! info + + 需要 sing-box 服务器版本 1.3-beta9 或更高。 + +启用填充。 + +#### brutal + +参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal/)。 \ No newline at end of file diff --git a/docs/configuration/shared/neighbor.md b/docs/configuration/shared/neighbor.md new file mode 100644 index 00000000..c67d995e --- /dev/null +++ b/docs/configuration/shared/neighbor.md @@ -0,0 +1,49 @@ +--- +icon: material/lan +--- + +# Neighbor Resolution + +Match LAN devices by MAC address and hostname using +[`source_mac_address`](/configuration/route/rule/#source_mac_address) and +[`source_hostname`](/configuration/route/rule/#source_hostname) rule items. + +Neighbor resolution is automatically enabled when these rule items exist. +Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules. + +## Linux + +Works natively. No special setup required. + +Hostname resolution requires DHCP lease files, +automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea). +Custom paths can be set via [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files). + +## Android + +!!! quote "" + + Only supported in graphical clients. + +Requires Android 11 or above and ROOT. + +Must use [VPNHotspot](https://github.com/Mygod/VPNHotspot) to share the VPN connection. +ROM built-in features like "Use VPN for connected devices" can share VPN +but cannot provide MAC address or hostname information. + +Set **IP Masquerade Mode** to **None** in VPNHotspot settings. + +Only route/DNS rules are supported. TUN include/exclude routes are not supported. + +### Hostname Visibility + +Hostname is only visible in sing-box if it is visible in VPNHotspot. +For Apple devices, change **Private Wi-Fi Address** from **Rotating** to **Fixed** in the Wi-Fi settings +of the connected network. Non-Apple devices are always visible. + +## macOS + +Requires the standalone version (macOS system extension). +The App Store version can share the VPN as a hotspot but does not support MAC address or hostname reading. + +See [VPN Hotspot](/manual/misc/vpn-hotspot/#macos) for Internet Sharing setup. diff --git a/docs/configuration/shared/neighbor.zh.md b/docs/configuration/shared/neighbor.zh.md new file mode 100644 index 00000000..96297fcb --- /dev/null +++ b/docs/configuration/shared/neighbor.zh.md @@ -0,0 +1,49 @@ +--- +icon: material/lan +--- + +# 邻居解析 + +通过 +[`source_mac_address`](/configuration/route/rule/#source_mac_address) 和 +[`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。 + +当这些规则项存在时,邻居解析自动启用。 +使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。 + +## Linux + +原生支持,无需特殊设置。 + +主机名解析需要 DHCP 租约文件, +自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。 +可通过 [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files) 设置自定义路径。 + +## Android + +!!! quote "" + + 仅在图形客户端中支持。 + +需要 Android 11 或以上版本和 ROOT。 + +必须使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot) 共享 VPN 连接。 +ROM 自带的「通过 VPN 共享连接」等功能可以共享 VPN, +但无法提供 MAC 地址或主机名信息。 + +在 VPNHotspot 设置中将 **IP 遮掩模式** 设为 **无**。 + +仅支持路由/DNS 规则。不支持 TUN 的 include/exclude 路由。 + +### 设备可见性 + +MAC 地址和主机名仅在 VPNHotspot 中可见时 sing-box 才能读取。 +对于 Apple 设备,需要在所连接网络的 Wi-Fi 设置中将**私有无线局域网地址**从**轮替**改为**固定**。 +非 Apple 设备始终可见。 + +## macOS + +需要独立版本(macOS 系统扩展)。 +App Store 版本可以共享 VPN 热点但不支持 MAC 地址或主机名读取。 + +参阅 [VPN 热点](/manual/misc/vpn-hotspot/#macos) 了解互联网共享设置。 diff --git a/docs/configuration/shared/pre-match.md b/docs/configuration/shared/pre-match.md new file mode 100644 index 00000000..a0faf577 --- /dev/null +++ b/docs/configuration/shared/pre-match.md @@ -0,0 +1,50 @@ +--- +icon: material/new-box +--- + +# Pre-match + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [bypass](#bypass) + +Pre-match is rule matching that runs before the connection is established. + +### How it works + +When TUN receives a connection request, the connection has not yet been established, +so no connection data can be read. In this phase, sing-box runs the routing rules in pre-match mode. + +Since connection data is unavailable, only actions that do not require connection data can be executed. +When a rule matches an action that requires an established connection, pre-match stops at that rule. + +### Supported actions + +#### reject + +Reject with TCP RST / ICMP unreachable. + +See [reject](/configuration/route/rule_action/#reject) for details. + +#### route + +Route ICMP connections to the specified outbound for direct reply. + +See [route](/configuration/route/rule_action/#route) for details. + +#### bypass + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux with `auto_redirect` enabled. + +Bypass sing-box and connect directly at kernel level. + +If `outbound` is not specified, the rule only matches in pre-match from auto redirect, +and will be skipped in other contexts. + +For all other contexts, bypass with `outbound` behaves like `route` action. + +See [bypass](/configuration/route/rule_action/#bypass) for details. diff --git a/docs/configuration/shared/pre-match.zh.md b/docs/configuration/shared/pre-match.zh.md new file mode 100644 index 00000000..06d78f10 --- /dev/null +++ b/docs/configuration/shared/pre-match.zh.md @@ -0,0 +1,47 @@ +--- +icon: material/new-box +--- + +# 预匹配 + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [bypass](#bypass) + +预匹配是在连接建立之前运行的规则匹配。 + +### 工作原理 + +当 TUN 收到连接请求时,连接尚未建立,因此无法读取连接数据。在此阶段,sing-box 在预匹配模式下运行路由规则。 + +由于连接数据不可用,只有不需要连接数据的动作才能执行。当规则匹配到需要已建立连接的动作时,预匹配将在该规则处停止。 + +### 支持的动作 + +#### reject + +以 TCP RST / ICMP 不可达拒绝。 + +详情参阅 [reject](/zh/configuration/route/rule_action/#reject)。 + +#### route + +将 ICMP 连接路由到指定出站以直接回复。 + +详情参阅 [route](/zh/configuration/route/rule_action/#route)。 + +#### bypass + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux,且需要启用 `auto_redirect`。 + +在内核层面绕过 sing-box 直接连接。 + +如果未指定 `outbound`,规则仅在来自 auto redirect 的预匹配中匹配,在其他场景中将被跳过。 + +对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。 + +详情参阅 [bypass](/zh/configuration/route/rule_action/#bypass)。 diff --git a/docs/configuration/shared/tcp-brutal.md b/docs/configuration/shared/tcp-brutal.md new file mode 100644 index 00000000..d248a463 --- /dev/null +++ b/docs/configuration/shared/tcp-brutal.md @@ -0,0 +1,28 @@ +### Server Requirements + +* Linux +* `brutal` congestion control algorithm kernel module installed + +See [tcp-brutal](https://github.com/apernet/tcp-brutal) for details. + +### Structure + +```json +{ + "enabled": true, + "up_mbps": 100, + "down_mbps": 100 +} +``` + +### Fields + +#### enabled + +Enable TCP Brutal congestion control algorithm。 + +#### up_mbps, down_mbps + +==Required== + +Upload and download bandwidth, in Mbps. \ No newline at end of file diff --git a/docs/configuration/shared/tcp-brutal.zh.md b/docs/configuration/shared/tcp-brutal.zh.md new file mode 100644 index 00000000..efb8c51f --- /dev/null +++ b/docs/configuration/shared/tcp-brutal.zh.md @@ -0,0 +1,28 @@ +### 服务器要求 + +* Linux +* `brutal` 拥塞控制算法内核模块已安装 + +参阅 [tcp-brutal](https://github.com/apernet/tcp-brutal)。 + +### 结构 + +```json +{ + "enabled": true, + "up_mbps": 100, + "down_mbps": 100 +} +``` + +### 字段 + +#### enabled + +启用 TCP Brutal 拥塞控制算法。 + +#### up_mbps, down_mbps + +==必填== + +上传和下载带宽,以 Mbps 为单位。 diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md new file mode 100644 index 00000000..518b2f91 --- /dev/null +++ b/docs/configuration/shared/tls.md @@ -0,0 +1,705 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [certificate_provider](#certificate_provider) + :material-delete-clock: [acme](#acme-fields) + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: [kernel_tx](#kernel_tx) + :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [curve_preferences](#curve_preferences) + :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) + :material-plus: [client_certificate](#client_certificate) + :material-plus: [client_certificate_path](#client_certificate_path) + :material-plus: [client_key](#client_key) + :material-plus: [client_key_path](#client_key_path) + :material-plus: [client_authentication](#client_authentication) + :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) + :material-plus: [ech.query_server_name](#query_server_name) + +!!! quote "Changes in sing-box 1.12.0" + + :material-plus: [fragment](#fragment) + :material-plus: [fragment_fallback_delay](#fragment_fallback_delay) + :material-plus: [record_fragment](#record_fragment) + :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) + :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) + +!!! quote "Changes in sing-box 1.10.0" + + :material-alert-decagram: [utls](#utls) + +### Inbound + +```json +{ + "enabled": true, + "server_name": "", + "alpn": [], + "min_version": "", + "max_version": "", + "cipher_suites": [], + "curve_preferences": [], + "certificate": [], + "certificate_path": "", + "client_authentication": "", + "client_certificate": [], + "client_certificate_path": [], + "client_certificate_public_key_sha256": [], + "key": [], + "key_path": "", + "kernel_tx": false, + "kernel_rx": false, + "certificate_provider": "", + + // Deprecated + + "acme": { + "domain": [], + "data_directory": "", + "default_server_name": "", + "email": "", + "provider": "", + "disable_http_challenge": false, + "disable_tls_alpn_challenge": false, + "alternative_http_port": 0, + "alternative_tls_port": 0, + "external_account": { + "key_id": "", + "mac_key": "" + }, + "dns01_challenge": {} + }, + "ech": { + "enabled": false, + "key": [], + "key_path": "", + + // Deprecated + + "pq_signature_schemes_enabled": false, + "dynamic_record_sizing_disabled": false + }, + "reality": { + "enabled": false, + "handshake": { + "server": "google.com", + "server_port": 443, + + ... // Dial Fields + }, + "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + "short_id": [ + "0123456789abcdef" + ], + "max_time_difference": "1m" + } +} +``` + +### Outbound + +```json +{ + "enabled": true, + "disable_sni": false, + "server_name": "", + "insecure": false, + "alpn": [], + "min_version": "", + "max_version": "", + "cipher_suites": [], + "curve_preferences": [], + "certificate": "", + "certificate_path": "", + "certificate_public_key_sha256": [], + "client_certificate": [], + "client_certificate_path": "", + "client_key": [], + "client_key_path": "", + "fragment": false, + "fragment_fallback_delay": "", + "record_fragment": false, + "ech": { + "enabled": false, + "config": [], + "config_path": "", + "query_server_name": "", + + // Deprecated + "pq_signature_schemes_enabled": false, + "dynamic_record_sizing_disabled": false + }, + "utls": { + "enabled": false, + "fingerprint": "" + }, + "reality": { + "enabled": false, + "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + "short_id": "0123456789abcdef" + } +} +``` + +TLS version values: + +* `1.0` +* `1.1` +* `1.2` +* `1.3` + +Cipher suite values: + +* `TLS_RSA_WITH_AES_128_CBC_SHA` +* `TLS_RSA_WITH_AES_256_CBC_SHA` +* `TLS_RSA_WITH_AES_128_GCM_SHA256` +* `TLS_RSA_WITH_AES_256_GCM_SHA384` +* `TLS_AES_128_GCM_SHA256` +* `TLS_AES_256_GCM_SHA384` +* `TLS_CHACHA20_POLY1305_SHA256` +* `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA` +* `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` +* `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` +* `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` +* `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` +* `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` +* `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` +* `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` +* `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` +* `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Fields + +#### enabled + +Enable TLS. + +#### disable_sni + +==Client only== + +Do not send server name in ClientHello. + +#### server_name + +Used to verify the hostname on the returned certificates unless insecure is given. + +It is also included in the client's handshake to support virtual hosting unless it is an IP address. + +#### insecure + +==Client only== + +Accepts any server certificate. + +#### alpn + +List of supported application level protocols, in order of preference. + +If both peers support ALPN, the selected protocol will be one from this list, and the connection will fail if there is +no mutually supported protocol. + +See [Application-Layer Protocol Negotiation](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation). + +#### min_version + +The minimum TLS version that is acceptable. + +By default, TLS 1.2 is currently used as the minimum when acting as a +client, and TLS 1.0 when acting as a server. + +#### max_version + +The maximum TLS version that is acceptable. + +By default, the maximum version is currently TLS 1.3. + +#### cipher_suites + +List of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored. +Note that TLS 1.3 cipher suites are not configurable. + +If empty, a safe default list is used. The default cipher suites might change over time. + +#### curve_preferences + +!!! question "Since sing-box 1.13.0" + +Set of supported key exchange mechanisms. The order of the list is ignored, and key exchange mechanisms are chosen +from this list using an internal preference order by Golang. + +Available values, also the default list: + +* `P256` +* `P384` +* `P521` +* `X25519` +* `X25519MLKEM768` + +#### certificate + +Server certificates chain line array, in PEM format. + +#### certificate_path + +!!! note "" + + Will be automatically reloaded if file modified. + +The path to server certificate chain, in PEM format. + + +#### certificate_public_key_sha256 + +!!! question "Since sing-box 1.13.0" + +==Client only== + +List of SHA-256 hashes of server certificate public keys, in base64 format. + +To generate the SHA-256 hash for a certificate's public key, use the following commands: + +```bash +# For a certificate file +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# For a certificate from a remote server +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` + +#### client_certificate + +!!! question "Since sing-box 1.13.0" + +==Client only== + +Client certificate chain line array, in PEM format. + +#### client_certificate_path + +!!! question "Since sing-box 1.13.0" + +==Client only== + +The path to client certificate chain, in PEM format. + +#### client_key + +!!! question "Since sing-box 1.13.0" + +==Client only== + +Client private key line array, in PEM format. + +#### client_key_path + +!!! question "Since sing-box 1.13.0" + +==Client only== + +The path to client private key, in PEM format. + +#### key + +==Server only== + +The server private key line array, in PEM format. + +#### key_path + +==Server only== + +!!! note "" + + Will be automatically reloaded if file modified. + +The path to the server private key, in PEM format. + +#### client_authentication + +!!! question "Since sing-box 1.13.0" + +==Server only== + +The type of client authentication to use. + +Available values: + +* `no` (default) +* `request` +* `require-any` +* `verify-if-given` +* `require-and-verify` + +One of `client_certificate`, `client_certificate_path`, or `client_certificate_public_key_sha256` is required +if this option is set to `verify-if-given`, or `require-and-verify`. + +#### client_certificate + +!!! question "Since sing-box 1.13.0" + +==Server only== + +Client certificate chain line array, in PEM format. + +#### client_certificate_path + +!!! question "Since sing-box 1.13.0" + +==Server only== + +!!! note "" + + Will be automatically reloaded if file modified. + +List of path to client certificate chain, in PEM format. + +#### client_certificate_public_key_sha256 + +!!! question "Since sing-box 1.13.0" + +==Server only== + +List of SHA-256 hashes of client certificate public keys, in base64 format. + +To generate the SHA-256 hash for a certificate's public key, use the following commands: + +```bash +# For a certificate file +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# For a certificate from a remote server +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` + +#### kernel_tx + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux 5.1+, use a newer kernel if possible. + +!!! quote "" + + Only TLS 1.3 is supported. + +!!! warning "" + + kTLS TX may only improve performance when `splice(2)` is available (both ends must be TCP or TLS without additional protocols after handshake); otherwise, it will definitely degrade performance. + +Enable kernel TLS transmit support. + +#### kernel_rx + +!!! question "Since sing-box 1.13.0" + +!!! quote "" + + Only supported on Linux 5.1+, use a newer kernel if possible. + +!!! quote "" + + Only TLS 1.3 is supported. + +!!! failure "" + + kTLS RX will definitely degrade performance even if `splice(2)` is in use, so enabling it is not recommended. + +Enable kernel TLS receive support. + +#### certificate_provider + +!!! question "Since sing-box 1.14.0" + +==Server only== + +A string or an object. + +When string, the tag of a shared [Certificate Provider](/configuration/shared/certificate-provider/). + +When object, an inline certificate provider. See [Certificate Provider](/configuration/shared/certificate-provider/) for available types and fields. + +## Custom TLS support + +!!! info "QUIC support" + + Only ECH is supported in QUIC. + +#### utls + +==Client only== + +!!! failure "Not Recommended" + + uTLS has had repeated fingerprinting vulnerabilities discovered by researchers. + + uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying + ClientHello structure. However, browsers use completely different TLS stacks + (Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors + that cannot be replicated by simply copying the handshake format, making detection possible. + Additionally, the library lacks active maintenance and has poor code quality, + making it unsuitable for censorship circumvention. + + For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead. + +uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance. + +Available fingerprint values: + +!!! warning "Removed since sing-box 1.10.0" + + Some legacy chrome fingerprints have been removed and will fallback to chrome: + + :material-close: chrome_psk + :material-close: chrome_psk_shuffle + :material-close: chrome_padding_psk_shuffle + :material-close: chrome_pq + :material-close: chrome_pq_psk + +* chrome +* firefox +* edge +* safari +* 360 +* qq +* ios +* android +* random +* randomized + +Chrome fingerprint will be used if empty. + +### ECH Fields + +ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello +message. + +The ECH key and configuration can be generated by `sing-box generate ech-keypair`. + +#### pq_signature_schemes_enabled + +!!! failure "Deprecated in sing-box 1.12.0" + + `pq_signature_schemes_enabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0. + +Enable support for post-quantum peer certificate signature schemes. + +#### dynamic_record_sizing_disabled + +!!! failure "Deprecated in sing-box 1.12.0" + + `dynamic_record_sizing_disabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0. + +Disables adaptive sizing of TLS records. + +When true, the largest possible TLS record size is always used. +When false, the size of TLS records may be adjusted in an attempt to improve latency. + +#### key + +==Server only== + +ECH key line array, in PEM format. + +#### key_path + +==Server only== + +!!! note "" + + Will be automatically reloaded if file modified. + +The path to ECH key, in PEM format. + +#### config + +==Client only== + +ECH configuration line array, in PEM format. + +If empty, load from DNS will be attempted. + +#### config_path + +==Client only== + +The path to ECH configuration, in PEM format. + +If empty, load from DNS will be attempted. + +#### query_server_name + +!!! question "Since sing-box 1.13.0" + +==Client only== + +Overrides the domain name used for ECH HTTPS record queries. + +If empty, `server_name` is used for queries. + +#### fragment + +!!! question "Since sing-box 1.12.0" + +==Client only== + +Fragment TLS handshakes to bypass firewalls. + +This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, +and should not be used to circumvent real censorship. + +Due to poor performance, try `record_fragment` first, and only apply to server names known to be blocked. + +On Linux, Apple platforms, (administrator privileges required) Windows, +the wait time can be automatically detected. Otherwise, it will fall back to +waiting for a fixed time specified by `fragment_fallback_delay`. + +In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, +because the target is considered to be local or behind a transparent proxy. + +#### fragment_fallback_delay + +!!! question "Since sing-box 1.12.0" + +==Client only== + +The fallback value used when TLS segmentation cannot automatically determine the wait time. + +`500ms` is used by default. + +#### record_fragment + +!!! question "Since sing-box 1.12.0" + +==Client only== + +Fragment TLS handshake into multiple TLS records to bypass firewalls. + +### ACME Fields + +!!! failure "Deprecated in sing-box 1.14.0" + + Inline ACME options are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-inline-acme-to-certificate-provider). + +#### domain + +List of domain. + +ACME will be disabled if empty. + +#### data_directory + +The directory to store ACME data. + +`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty. + +#### default_server_name + +Server name to use when choosing a certificate if the ClientHello's ServerName field is empty. + +#### email + +The email address to use when creating or selecting an existing ACME server account + +#### provider + +The ACME CA provider to use. + +| Value | Provider | +|-------------------------|---------------| +| `letsencrypt (default)` | Let's Encrypt | +| `zerossl` | ZeroSSL | +| `https://...` | Custom | + +#### disable_http_challenge + +Disable all HTTP challenges. + +#### disable_tls_alpn_challenge + +Disable all TLS-ALPN challenges + +#### alternative_http_port + +The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a +listener for the HTTP challenge. + +#### alternative_tls_port + +The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to +succeed. + +#### external_account + +EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known +by the CA. + +External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as +a CA customer database. + +To enable ACME account binding, the CA operating the ACME server needs to provide the ACME client with a MAC key and a +key identifier, using some mechanism outside of ACME. §7.3.4 + +#### external_account.key_id + +The key identifier. + +#### external_account.mac_key + +The MAC key. + +#### dns01_challenge + +ACME DNS01 challenge field. If configured, other challenge methods will be disabled. + +See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details. + +### Reality Fields + +#### handshake + +==Server only== + +==Required== + +Handshake server address and [Dial Fields](/configuration/shared/dial/). + +#### private_key + +==Server only== + +==Required== + +Private key, generated by `sing-box generate reality-keypair`. + +#### public_key + +==Client only== + +==Required== + +Public key, generated by `sing-box generate reality-keypair`. + +#### short_id + +==Required== + +A hexadecimal string with zero to eight digits. + +#### max_time_difference + +==Server only== + +The maximum time difference between the server and the client. + +Check disabled if empty. diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md new file mode 100644 index 00000000..56b90d33 --- /dev/null +++ b/docs/configuration/shared/tls.zh.md @@ -0,0 +1,695 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [certificate_provider](#certificate_provider) + :material-delete-clock: [acme](#acme-字段) + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: [kernel_tx](#kernel_tx) + :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [curve_preferences](#curve_preferences) + :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) + :material-plus: [client_certificate](#client_certificate) + :material-plus: [client_certificate_path](#client_certificate_path) + :material-plus: [client_key](#client_key) + :material-plus: [client_key_path](#client_key_path) + :material-plus: [client_authentication](#client_authentication) + :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) + :material-plus: [ech.query_server_name](#query_server_name) + +!!! quote "sing-box 1.12.0 中的更改" + + :material-plus: [fragment](#fragment) + :material-plus: [fragment_fallback_delay](#fragment_fallback_delay) + :material-plus: [record_fragment](#record_fragment) + :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) + :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) + +!!! quote "sing-box 1.10.0 中的更改" + + :material-alert-decagram: [utls](#utls) + +### 入站 + +```json +{ + "enabled": true, + "server_name": "", + "alpn": [], + "min_version": "", + "max_version": "", + "cipher_suites": [], + "curve_preferences": [], + "certificate": [], + "certificate_path": "", + "client_authentication": "", + "client_certificate": [], + "client_certificate_path": [], + "client_certificate_public_key_sha256": [], + "key": [], + "key_path": "", + "kernel_tx": false, + "kernel_rx": false, + "certificate_provider": "", + + // 废弃的 + + "acme": { + "domain": [], + "data_directory": "", + "default_server_name": "", + "email": "", + "provider": "", + "disable_http_challenge": false, + "disable_tls_alpn_challenge": false, + "alternative_http_port": 0, + "alternative_tls_port": 0, + "external_account": { + "key_id": "", + "mac_key": "" + }, + "dns01_challenge": {} + }, + "ech": { + "enabled": false, + "key": [], + "key_path": "", + + // 废弃的 + + "pq_signature_schemes_enabled": false, + "dynamic_record_sizing_disabled": false + }, + "reality": { + "enabled": false, + "handshake": { + "server": "google.com", + "server_port": 443, + + ... // 拨号字段 + }, + "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + "short_id": [ + "0123456789abcdef" + ], + "max_time_difference": "1m" + } +} +``` + +### 出站 + +```json +{ + "enabled": true, + "disable_sni": false, + "server_name": "", + "insecure": false, + "alpn": [], + "min_version": "", + "max_version": "", + "cipher_suites": [], + "curve_preferences": [], + "certificate": "", + "certificate_path": "", + "certificate_public_key_sha256": [], + "client_certificate": [], + "client_certificate_path": "", + "client_key": [], + "client_key_path": "", + "fragment": false, + "fragment_fallback_delay": "", + "record_fragment": false, + "ech": { + "enabled": false, + "config": [], + "config_path": "", + "query_server_name": "", + + // 废弃的 + "pq_signature_schemes_enabled": false, + "dynamic_record_sizing_disabled": false + }, + "utls": { + "enabled": false, + "fingerprint": "" + }, + "reality": { + "enabled": false, + "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + "short_id": "0123456789abcdef" + } +} +``` + +TLS 版本值: + +* `1.0` +* `1.1` +* `1.2` +* `1.3` + +密码套件值: + +* `TLS_RSA_WITH_AES_128_CBC_SHA` +* `TLS_RSA_WITH_AES_256_CBC_SHA` +* `TLS_RSA_WITH_AES_128_GCM_SHA256` +* `TLS_RSA_WITH_AES_256_GCM_SHA384` +* `TLS_AES_128_GCM_SHA256` +* `TLS_AES_256_GCM_SHA384` +* `TLS_CHACHA20_POLY1305_SHA256` +* `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA` +* `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` +* `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` +* `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` +* `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` +* `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` +* `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` +* `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` +* `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` +* `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +### 字段 + +#### enabled + +启用 TLS + +#### disable_sni + +==仅客户端== + +不要在 ClientHello 中发送服务器名称. + +#### server_name + +用于验证返回证书上的主机名,除非设置不安全。 + +它还包含在 ClientHello 中以支持虚拟主机,除非它是 IP 地址。 + +#### insecure + +==仅客户端== + +接受任何服务器证书。 + +#### alpn + +支持的应用层协议协商列表,按优先顺序排列。 + +如果两个对等点都支持 ALPN,则选择的协议将是此列表中的一个,如果没有相互支持的协议则连接将失败。 + +参阅 [Application-Layer Protocol Negotiation](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation)。 + +#### min_version + +可接受的最低 TLS 版本。 + +默认情况下,当前使用 TLS 1.2 作为客户端的最低要求。作为服务器时使用 TLS 1.0。 + +#### max_version + +可接受的最大 TLS 版本。 + +默认情况下,当前最高版本为 TLS 1.3。 + +#### cipher_suites + +启用的 TLS 1.0–1.2 密码套件列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。 + +如果为空,则使用安全的默认列表。默认密码套件可能会随着时间的推移而改变。 + +#### curve_preferences + +!!! question "自 sing-box 1.13.0 起" + +支持的密钥交换机制集合。列表的顺序被忽略,密钥交换机制通过 Golang 的内部偏好顺序从此列表中选择。 + +可用值,同时也是默认列表: + +* `P256` +* `P384` +* `P521` +* `X25519` +* `X25519MLKEM768` + +#### certificate + +服务器证书链行数组,PEM 格式。 + +#### certificate_path + +!!! note "" + + 文件更改时将自动重新加载。 + +服务器证书链路径,PEM 格式。 + +#### certificate_public_key_sha256 + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +服务器证书公钥的 SHA-256 哈希列表,base64 格式。 + +要生成证书公钥的 SHA-256 哈希,请使用以下命令: + +```bash +# 对于证书文件 +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# 对于远程服务器的证书 +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` + +#### client_certificate + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +客户端证书链行数组,PEM 格式。 + +#### client_certificate_path + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +客户端证书链路径,PEM 格式。 + +#### client_key + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +客户端私钥行数组,PEM 格式。 + +#### client_key_path + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +客户端私钥路径,PEM 格式。 + +#### key + +==仅服务器== + +!!! note "" + + 文件更改时将自动重新加载。 + +服务器 PEM 私钥行数组。 + +#### key_path + +==仅服务器== + +!!! note "" + + 文件更改时将自动重新加载。 + +服务器私钥路径,PEM 格式。 + +#### client_authentication + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +要使用的客户端身份验证类型。 + +可用值: + +* `no`(默认) +* `request` +* `require-any` +* `verify-if-given` +* `require-and-verify` + +如果此选项设置为 `verify-if-given` 或 `require-and-verify`, +则需要 `client_certificate`、`client_certificate_path` 或 `client_certificate_public_key_sha256` 中的一个。 + +#### client_certificate + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +客户端证书链行数组,PEM 格式。 + +#### client_certificate_path + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +!!! note "" + + 文件更改时将自动重新加载。 + +客户端证书链路径列表,PEM 格式。 + +#### client_certificate_public_key_sha256 + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +客户端证书公钥的 SHA-256 哈希列表,base64 格式。 + +要生成证书公钥的 SHA-256 哈希,请使用以下命令: + +```bash +# 对于证书文件 +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# 对于远程服务器的证书 +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` + +#### kernel_tx + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux 5.1+,如果可能,使用较新的内核。 + +!!! quote "" + + 仅支持 TLS 1.3。 + +!!! warning "" + + kTLS TX 仅当 `splice(2)` 可用时(两端经过握手后必须为没有附加协议的 TCP 或 TLS)才能提高性能;否则肯定会降低性能。 + +启用内核 TLS 发送支持。 + +#### kernel_rx + +!!! question "自 sing-box 1.13.0 起" + +!!! quote "" + + 仅支持 Linux 5.1+,如果可能,使用较新的内核。 + +!!! quote "" + + 仅支持 TLS 1.3。 + +!!! failure "" + + 即使使用 `splice(2)`,kTLS RX 也肯定会降低性能,因此不建议启用。 + +启用内核 TLS 接收支持。 + +#### certificate_provider + +!!! question "自 sing-box 1.14.0 起" + +==仅服务器== + +字符串或对象。 + +为字符串时,共享[证书提供者](/zh/configuration/shared/certificate-provider/)的标签。 + +为对象时,内联的证书提供者。可用类型和字段参阅[证书提供者](/zh/configuration/shared/certificate-provider/)。 + +## 自定义 TLS 支持 + +!!! info "QUIC 支持" + + 只有 ECH 在 QUIC 中被支持. + +#### utls + +==仅客户端== + +!!! failure "不推荐" + + uTLS 已被研究人员多次发现其指纹可被识别的漏洞。 + + uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。 + 然而,浏览器使用完全不同的 TLS 实现(Chrome 使用 BoringSSL,Firefox 使用 NSS), + 其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。 + 此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。 + + 如需 TLS 指纹抵抗,请改用 [NaiveProxy](/zh/configuration/inbound/naive/)。 + +uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。 + +可用的指纹值: + +!!! warning "已在 sing-box 1.10.0 移除" + + 一些旧 chrome 指纹已被删除,并将会退到 chrome: + + :material-close: chrome_psk + :material-close: chrome_psk_shuffle + :material-close: chrome_padding_psk_shuffle + :material-close: chrome_pq + :material-close: chrome_pq_psk + +* chrome +* firefox +* edge +* safari +* 360 +* qq +* ios +* android +* random +* randomized + +默认使用 chrome 指纹。 + +### ECH 字段 + +ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分信息。 + +ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。 + +#### pq_signature_schemes_enabled + +!!! failure "已在 sing-box 1.12.0 废弃" + + `pq_signature_schemes_enabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除。 + +启用对后量子对等证书签名方案的支持。 + +#### dynamic_record_sizing_disabled + +!!! failure "已在 sing-box 1.12.0 废弃" + + `dynamic_record_sizing_disabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除。 + +禁用 TLS 记录的自适应大小调整。 + +当为 true 时,总是使用最大可能的 TLS 记录大小。 +当为 false 时,可能会调整 TLS 记录的大小以尝试改善延迟。 + +#### key + +==仅服务器== + +ECH 密钥行数组,PEM 格式。 + +#### key_path + +==仅服务器== + +!!! note "" + + 文件更改时将自动重新加载。 + +ECH 密钥路径,PEM 格式。 + +#### config + +==仅客户端== + +ECH 配置行数组,PEM 格式。 + +如果为空,将尝试从 DNS 加载。 + +#### config_path + +==仅客户端== + +ECH 配置路径,PEM 格式。 + +如果为空,将尝试从 DNS 加载。 + +#### query_server_name + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +覆盖用于 ECH HTTPS 记录查询的域名。 + +如果为空,使用 `server_name` 查询。 + +#### fragment + +!!! question "自 sing-box 1.12.0 起" + +==仅客户端== + +通过分段 TLS 握手数据包来绕过防火墙。 + +此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真正的审查。 + +由于性能不佳,请首先尝试 `record_fragment`,且仅应用于已知被阻止的服务器名称。 + +在 Linux、Apple 平台和(需要管理员权限的)Windows 系统上, +可以自动检测等待时间。否则,将回退到 +等待 `fragment_fallback_delay` 指定的固定时间。 + +此外,如果实际等待时间少于 20ms,也会回退到等待固定时间, +因为目标被认为是本地的或在透明代理后面。 + +#### fragment_fallback_delay + +!!! question "自 sing-box 1.12.0 起" + +==仅客户端== + +当 TLS 分段无法自动确定等待时间时使用的回退值。 + +默认使用 `500ms`。 + +#### record_fragment + +!!! question "自 sing-box 1.12.0 起" + +==仅客户端== + +将 TLS 握手分段为多个 TLS 记录以绕过防火墙。 + +### ACME 字段 + +!!! failure "已在 sing-box 1.14.0 废弃" + + 内联 ACME 选项已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。 + +#### domain + +域名列表。 + +如果为空则禁用 ACME。 + +#### data_directory + +ACME 数据存储目录。 + +如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。 + +#### default_server_name + +如果 ClientHello 的 ServerName 字段为空,则选择证书时要使用的服务器名称。 + +#### email + +创建或选择现有 ACME 服务器帐户时使用的电子邮件地址。 + +#### provider + +要使用的 ACME CA 供应商。 + +| 值 | 供应商 | +|--------------------|---------------| +| `letsencrypt (默认)` | Let's Encrypt | +| `zerossl` | ZeroSSL | +| `https://...` | 自定义 | + +#### disable_http_challenge + +禁用所有 HTTP 质询。 + +#### disable_tls_alpn_challenge + +禁用所有 TLS-ALPN 质询。 + +#### alternative_http_port + +用于 ACME HTTP 质询的备用端口;如果非空,将使用此端口而不是 80 来启动 HTTP 质询的侦听器。 + +#### alternative_tls_port + +用于 ACME TLS-ALPN 质询的备用端口; 系统必须将 443 转发到此端口以使质询成功。 + +#### external_account + +EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。 + +外部帐户绑定"用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。 + +为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4 + +#### external_account.key_id + +密钥标识符。 + +#### external_account.mac_key + +MAC 密钥。 + +#### dns01_challenge + +ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 + +参阅 [DNS01 验证字段](/zh/configuration/shared/dns01_challenge/)。 + +### Reality 字段 + +#### handshake + +==仅服务器== + +==必填== + +握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 + +#### private_key + +==仅服务器== + +==必填== + +私钥,由 `sing-box generate reality-keypair` 生成。 + +#### public_key + +==仅客户端== + +==必填== + +公钥,由 `sing-box generate reality-keypair` 生成。 + +#### short_id + +==必填== + +一个零到八位的十六进制字符串。 + +#### max_time_difference + +==仅服务器== + +服务器和客户端之间的最大时间差。 + +如果为空则禁用检查。 diff --git a/docs/configuration/shared/udp-over-tcp.md b/docs/configuration/shared/udp-over-tcp.md new file mode 100644 index 00000000..71a934db --- /dev/null +++ b/docs/configuration/shared/udp-over-tcp.md @@ -0,0 +1,82 @@ +!!! warning "" + + It's a proprietary protocol created by SagerNet, not part of shadowsocks. + +The UDP over TCP protocol is used to transmit UDP packets in TCP. + +### Structure + +```json +{ + "enabled": true, + "version": 2 +} +``` + +!!! info "" + + The structure can be replaced with a boolean value when the version is not specified. + +### Fields + +#### enabled + +Enable the UDP over TCP protocol. + +#### version + +The protocol version, `1` or `2`. + +2 is used by default. + +### Application support + +| Project | UoT v1 | UoT v2 | +|--------------|----------------------|----------------------| +| sing-box | v0 (2022/08/11) | v1.2-beta9 | +| Clash.Meta | v1.12.0 (2022/07/02) | v1.14.3 (2023/03/31) | +| Shadowrocket | v2.2.12 (2022/08/13) | / | + +### Protocol details + +#### Protocol version 1 + +The client requests the magic address to the upper layer proxy protocol to indicate the request: `sp.udp-over-tcp.arpa` + +#### Stream format + +| ATYP | address | port | length | data | +|------|----------|-------|--------|----------| +| u8 | variable | u16be | u16be | variable | + +**ATYP / address / port**: Uses the SOCKS address format, but with different address types: + +| ATYP | Address type | +|--------|--------------| +| `0x00` | IPv4 Address | +| `0x01` | IPv6 Address | +| `0x02` | Domain Name | + +#### Protocol version 2 + +Protocol version 2 uses a new magic address: `sp.v2.udp-over-tcp.arpa` + +##### Request format + +| isConnect | ATYP | address | port | +|-----------|------|----------|-------| +| u8 | u8 | variable | u16be | + +**isConnect**: Set to 1 to indicates that the stream uses the connect format, 0 to disable. + +**ATYP / address / port**: Request destination, uses the SOCKS address format. + +##### Connect stream format + +| length | data | +|--------|----------| +| u16be | variable | + +##### Non-connect stream format + +As the same as the stream format in protocol version 1. \ No newline at end of file diff --git a/docs/configuration/shared/udp-over-tcp.zh.md b/docs/configuration/shared/udp-over-tcp.zh.md new file mode 100644 index 00000000..fec9645e --- /dev/null +++ b/docs/configuration/shared/udp-over-tcp.zh.md @@ -0,0 +1,82 @@ +!!! warning "" + + 这是 SagerNet 创建的专有协议,不是 shadowsocks 的一部分。 + +UDP over TCP 协议用于在 TCP 中传输 UDP 数据包。 + +### 结构 + +```json +{ + "enabled": true, + "version": 2 +} +``` + +!!! info "" + + 当不指定版本时,结构可以用布尔值替换。 + +### 字段 + +#### enabled + +启用 UDP over TCP 协议。 + +#### version + +协议版本,`1` 或 `2`。 + +默认使用 2。 + +### 应用程序支持 + +| 项目 | UoT v1 | UoT v2 | +|--------------|----------------------|----------------------| +| sing-box | v0 (2022/08/11) | v1.2-beta9 | +| Clash.Meta | v1.12.0 (2022/07/02) | v1.14.3 (2023/03/31) | +| Shadowrocket | v2.2.12 (2022/08/13) | / | + +### 协议详情 + +#### 协议版本 1 + +客户端向上层代理协议请求魔法地址以表示请求:`sp.udp-over-tcp.arpa` + +#### 流格式 + +| ATYP | 地址 | 端口 | 长度 | 数据 | +|------|----------|-------|--------|----------| +| u8 | 可变长 | u16be | u16be | 可变长 | + +**ATYP / 地址 / 端口**:使用 SOCKS 地址格式,但使用不同的地址类型: + +| ATYP | 地址类型 | +|--------|-----------| +| `0x00` | IPv4 地址 | +| `0x01` | IPv6 地址 | +| `0x02` | 域名 | + +#### 协议版本 2 + +协议版本 2 使用新的魔法地址:`sp.v2.udp-over-tcp.arpa` + +##### 请求格式 + +| isConnect | ATYP | 地址 | 端口 | +|-----------|------|----------|-------| +| u8 | u8 | 可变长 | u16be | + +**isConnect**:设置为 1 表示流使用连接格式,0 表示禁用。 + +**ATYP / 地址 / 端口**:请求目标,使用 SOCKS 地址格式。 + +##### 连接流格式 + +| 长度 | 数据 | +|--------|----------| +| u16be | 可变长 | + +##### 非连接流格式 + +与协议版本 1 中的流格式相同。 \ No newline at end of file diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md new file mode 100644 index 00000000..afc33241 --- /dev/null +++ b/docs/configuration/shared/v2ray-transport.md @@ -0,0 +1,229 @@ +V2Ray Transport is a set of private protocols invented by v2ray, and has contaminated the names of other protocols, such +as `trojan-grpc` in clash. + +### Structure + +```json +{ + "type": "" +} +``` + +Available transports: + +* HTTP +* WebSocket +* QUIC +* gRPC +* HTTPUpgrade + +!!! warning "Difference from v2ray-core" + + * No TCP transport, plain HTTP is merged into the HTTP transport. + * No mKCP transport. + * No DomainSocket transport. + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### HTTP + +```json +{ + "type": "http", + "host": [], + "path": "", + "method": "", + "headers": {}, + "idle_timeout": "15s", + "ping_timeout": "15s" +} +``` + +!!! warning "Difference from v2ray-core" + + TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used. + +#### host + +List of host domain. + +The client will choose randomly and the server will verify if not empty. + +#### path + +!!! warning + + V2Ray's documentation says that the path between the server and the client must be consistent, + but the actual code allows the client to add any suffix to the path. + sing-box uses the same behavior as V2Ray, but note that the behavior does not exist in `WebSocket` and `HTTPUpgrade` transport. + +Path of HTTP request. + +The server will verify. + +#### method + +Method of HTTP request. + +The server will verify if not empty. + +#### headers + +Extra headers of HTTP request. + +The server will write in response if not empty. + +#### idle_timeout + +In HTTP2 server: + +Specifies the time until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity. + +In HTTP2 client: + +Specifies the period of time after which a health check will be performed using a ping frame if no frames have been +received on the connection.Please note that a ping response is considered a received frame, so if there is no other +traffic on the connection, the health check will be executed every interval. If the value is zero, no health check will +be performed. + +Zero is used by default. + +#### ping_timeout + +In HTTP2 client: + +Specifies the timeout duration after sending a PING frame, within which a response must be received. +If a response to the PING frame is not received within the specified timeout duration, the connection will be closed. +The default timeout duration is 15 seconds. + +### WebSocket + +```json +{ + "type": "ws", + "path": "", + "headers": {}, + "max_early_data": 0, + "early_data_header_name": "" +} +``` + +#### path + +Path of HTTP request. + +The server will verify. + +#### headers + +Extra headers of HTTP request. + +The server will write in response if not empty. + +#### max_early_data + +Allowed payload size is in the request. Enabled if not zero. + +#### early_data_header_name + +Early data is sent in path instead of header by default. + +To be compatible with Xray-core, set this to `Sec-WebSocket-Protocol`. + +It needs to be consistent with the server. + +### QUIC + +```json +{ + "type": "quic" +} +``` + +!!! warning "Difference from v2ray-core" + + No additional encryption support: + It's basically duplicate encryption. And Xray-core is not compatible with v2ray-core in here. + +### gRPC + +!!! note "" + + standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](/installation/build-from-source/#build-tags). + +```json +{ + "type": "grpc", + "service_name": "TunService", + "idle_timeout": "15s", + "ping_timeout": "15s", + "permit_without_stream": false +} +``` + +#### service_name + +Service name of gRPC. + +#### idle_timeout + +In standard gRPC server/client: + +If the transport doesn't see any activity after a duration of this time, +it pings the client to check if the connection is still active. + +In default gRPC server/client: + +It has the same behavior as the corresponding setting in HTTP transport. + +#### ping_timeout + +In standard gRPC server/client: + +The timeout that after performing a keepalive check, the client will wait for activity. +If no activity is detected, the connection will be closed. + +In default gRPC server/client: + +It has the same behavior as the corresponding setting in HTTP transport. + +#### permit_without_stream + +In standard gRPC client: + +If enabled, the client transport sends keepalive pings even with no active connections. +If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive +pings will be sent. + +Disabled by default. + +### HTTPUpgrade + +```json +{ + "type": "httpupgrade", + "host": "", + "path": "", + "headers": {} +} +``` + +#### host + +Host domain. + +The server will verify if not empty. + +#### path + +Path of HTTP request. + +The server will verify. + +#### headers + +Extra headers of HTTP request. + +The server will write in response if not empty. diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md new file mode 100644 index 00000000..e5ea3ed6 --- /dev/null +++ b/docs/configuration/shared/v2ray-transport.zh.md @@ -0,0 +1,218 @@ +V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议的名称,如 clash 中的 `trojan-grpc`。 + +### 结构 + +```json +{ + "type": "" +} +``` + +可用的传输协议: + +* HTTP +* WebSocket +* QUIC +* gRPC +* HTTPUpgrade + +!!! warning "与 v2ray-core 的区别" + + * 没有 TCP 传输层, 纯 HTTP 已合并到 HTTP 传输层。 + * 没有 mKCP 传输层。 + * 没有 DomainSocket 传输层。 + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +### HTTP + +```json +{ + "type": "http", + "host": [], + "path": "", + "method": "", + "headers": {}, + "idle_timeout": "15s", + "ping_timeout": "15s" +} +``` + +!!! warning "与 v2ray-core 的区别" + + 不强制执行 TLS。如果未配置 TLS,将使用纯 HTTP 1.1。 + +#### host + +主机域名列表。 + +如果设置,客户端将随机选择,服务器将验证。 + +#### path + +!!! warning + + V2Ray 文档称服务端和客户端的路径必须一致,但实际代码允许客户端向路径添加任何后缀。 + sing-box 使用与 V2Ray 相同的行为,但请注意,该行为在 `WebSocket` 和 `HTTPUpgrade` 传输层中不存在。 + +HTTP 请求路径 + +服务器将验证。 + +#### method + +HTTP 请求方法 + +如果设置,服务器将验证。 + +#### headers + +HTTP 请求的额外标头 + +如果设置,服务器将写入响应。 + +#### idle_timeout + +在 HTTP2 服务器中: + +指定闲置客户端应在多长时间内使用 GOAWAY 帧关闭。PING 帧不被视为活动。 + +在 HTTP2 客户端中: + +如果连接上没有收到任何帧,指定一段时间后将使用 PING 帧执行健康检查。需要注意的是,PING 响应被视为已接收的帧,因此如果连接上没有其他流量,则健康检查将在每个间隔执行一次。如果值为零,则不会执行健康检查。 + +默认使用零。 + +#### ping_timeout + +在 HTTP2 客户端中: + +指定发送 PING 帧后,在指定的超时时间内必须接收到响应。如果在指定的超时时间内没有收到 PING 帧的响应,则连接将关闭。默认超时持续时间为 15 秒。 + +### WebSocket + +```json +{ + "type": "ws", + "path": "", + "headers": {}, + "max_early_data": 0, + "early_data_header_name": "" +} +``` + +#### path + +HTTP 请求路径 + +服务器将验证。 + +#### headers + +HTTP 请求的额外标头 + +如果设置,服务器将写入响应。 + +#### max_early_data + +请求中允许的最大有效负载大小。默认启用。 + +#### early_data_header_name + +默认情况下,早期数据在路径而不是标头中发送。 + +要与 Xray-core 兼容,请将其设置为 `Sec-WebSocket-Protocol`。 + +它需要与服务器保持一致。 + +### QUIC + +```json +{ + "type": "quic" +} +``` + +!!! warning "与 v2ray-core 的区别" + + 没有额外的加密支持: + 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 + +### gRPC + +!!! note "" + + 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#构建标记)。 + +```json +{ + "type": "grpc", + "service_name": "TunService", + "idle_timeout": "15s", + "ping_timeout": "15s", + "permit_without_stream": false +} +``` + +#### service_name + +gRPC 服务名称。 + +#### idle_timeout + +在标准 gRPC 服务器/客户端: + +如果传输在此时间段后没有看到任何活动,它会向客户端发送 ping 请求以检查连接是否仍然活动。 + +在默认 gRPC 服务器/客户端: + +它的行为与 HTTP 传输层中的相应设置相同。 + +#### ping_timeout + +在标准 gRPC 服务器/客户端: + +经过一段时间之后,客户端将执行 keepalive 检查并等待活动。如果没有检测到任何活动,则会关闭连接。 + +在默认 gRPC 服务器/客户端: + +它的行为与 HTTP 传输层中的相应设置相同。 + +#### permit_without_stream + +在标准 gRPC 客户端: + +如果启用,客户端传输即使没有活动连接也会发送 keepalive ping。如果禁用,则在没有活动连接时,将忽略 `idle_timeout` 和 `ping_timeout`,并且不会发送 keepalive ping。 + +默认禁用。 + +### HTTPUpgrade + +```json +{ + "type": "httpupgrade", + "host": "", + "path": "", + "headers": {} +} +``` + +#### host + +主机域名。 + +服务器将验证。 + +#### path + +HTTP 请求路径 + +服务器将验证。 + +#### headers + +HTTP 请求的额外标头。 + +如果设置,服务器将写入响应。 diff --git a/docs/configuration/shared/wifi-state.md b/docs/configuration/shared/wifi-state.md new file mode 100644 index 00000000..a32675b3 --- /dev/null +++ b/docs/configuration/shared/wifi-state.md @@ -0,0 +1,41 @@ +--- +icon: material/new-box +--- + +# Wi-Fi State + +!!! quote "Changes in sing-box 1.13.0" + + :material-plus: Linux support + :material-plus: Windows support + +sing-box can monitor Wi-Fi state to enable routing rules based on `wifi_ssid` and `wifi_bssid`. + +### Platform Support + +| Platform | Support | Notes | +|-----------------|------------------|--------------------------| +| Android | :material-check: | In graphical client | +| Apple platforms | :material-check: | In graphical clients | +| Linux | :material-check: | Requires supported daemon | +| Windows | :material-check: | WLAN API | +| Others | :material-close: | | + +### Linux + +!!! question "Since sing-box 1.13.0" + +The following backends are supported and will be auto-detected in order of priority: + +| Backend | Interface | +|------------------|-------------| +| NetworkManager | D-Bus | +| IWD | D-Bus | +| wpa_supplicant | Unix socket | +| ConnMan | D-Bus | + +### Windows + +!!! question "Since sing-box 1.13.0" + +Uses Windows WLAN API. diff --git a/docs/configuration/shared/wifi-state.zh.md b/docs/configuration/shared/wifi-state.zh.md new file mode 100644 index 00000000..c7d4db5f --- /dev/null +++ b/docs/configuration/shared/wifi-state.zh.md @@ -0,0 +1,41 @@ +--- +icon: material/new-box +--- + +# Wi-Fi 状态 + +!!! quote "sing-box 1.13.0 中的更改" + + :material-plus: Linux 支持 + :material-plus: Windows 支持 + +sing-box 可以监控 Wi-Fi 状态,以启用基于 `wifi_ssid` 和 `wifi_bssid` 的路由规则。 + +### 平台支持 + +| 平台 | 支持 | 备注 | +|-----------------|------------------|----------------| +| Android | :material-check: | 仅图形客户端 | +| Apple 平台 | :material-check: | 仅图形客户端 | +| Linux | :material-check: | 需要支持的守护进程 | +| Windows | :material-check: | WLAN API | +| 其他 | :material-close: | | + +### Linux + +!!! question "自 sing-box 1.13.0 起" + +支持以下后端,将按优先级顺序自动探测: + +| 后端 | 接口 | +|------------------|-------------| +| NetworkManager | D-Bus | +| IWD | D-Bus | +| wpa_supplicant | Unix socket | +| ConnMan | D-Bus | + +### Windows + +!!! question "自 sing-box 1.13.0 起" + +使用 Windows WLAN API。 diff --git a/docs/deprecated.md b/docs/deprecated.md new file mode 100644 index 00000000..1eeab10d --- /dev/null +++ b/docs/deprecated.md @@ -0,0 +1,177 @@ +--- +icon: material/delete-alert +--- + +# Deprecated Feature List + +## 1.14.0 + +#### Inline ACME options in TLS + +Inline ACME options (`tls.acme`) are deprecated +and can be replaced by the ACME certificate provider, +check [Migration](../migration/#migrate-inline-acme-to-certificate-provider). + +Old fields will be removed in sing-box 1.16.0. + +#### Legacy `strategy` DNS rule action option + +Legacy `strategy` DNS rule action option is deprecated. + +Old fields will be removed in sing-box 1.16.0. + +#### Legacy `rule_set_ip_cidr_accept_empty` DNS rule item + +Legacy `rule_set_ip_cidr_accept_empty` DNS rule item is deprecated, +check [Migration](../migration/#migrate-address-filter-fields-to-response-matching). + +Old fields will be removed in sing-box 1.16.0. + +#### `independent_cache` DNS option + +`independent_cache` DNS option is deprecated. +The DNS cache now always keys by transport, making this option unnecessary, +check [Migration](../migration/#migrate-independent-dns-cache). + +Old fields will be removed in sing-box 1.16.0. + +#### `store_rdrc` cache file option + +`store_rdrc` cache file option is deprecated, +check [Migration](../migration/#migrate-store-rdrc). + +Old fields will be removed in sing-box 1.16.0. + +#### Legacy Address Filter Fields in DNS rules + +Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) +in DNS rules are deprecated, +check [Migration](../migration/#migrate-address-filter-fields-to-response-matching). + +Old behavior will be removed in sing-box 1.16.0. + +## 1.12.0 + +#### Legacy DNS server formats + +DNS servers are refactored, +check [Migration](../migration/#migrate-to-new-dns-server-formats). + +Old formats were removed in sing-box 1.14.0. + +#### `outbound` DNS rule item + +Legacy `outbound` DNS rules are deprecated +and can be replaced by dial fields, +check [Migration](../migration/#migrate-outbound-dns-rule-items-to-domain-resolver). + +#### Legacy ECH fields + +ECH support has been migrated to use stdlib in sing-box 1.12.0, +which does not come with support for PQ signature schemes, +so `pq_signature_schemes_enabled` has been deprecated and no longer works. + +Also, `dynamic_record_sizing_disabled` has nothing to do with ECH, +was added by mistake, has been deprecated and no longer works. + +These fields were removed in sing-box 1.13.0. + +## 1.11.0 + +#### Legacy special outbounds + +Legacy special outbounds (`block` / `dns`) are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions). + +Old fields were removed in sing-box 1.13.0. + +#### Legacy inbound fields + +Legacy inbound fields (`inbound.` are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions). + +Old fields were removed in sing-box 1.13.0. + +#### Destination override fields in direct outbound + +Destination override fields (`override_address` / `override_port`) in direct outbound are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-destination-override-fields-to-route-options). + +Old fields were removed in sing-box 1.13.0. + +#### WireGuard outbound + +WireGuard outbound is deprecated and can be replaced by endpoint, +check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint). + +Old outbound was removed in sing-box 1.13.0. + +#### GSO option in TUN + +GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN. + +Old fields were removed in sing-box 1.13.0. + +## 1.10.0 + +#### TUN address fields are merged + +`inet4_address` and `inet6_address` are merged into `address`, +`inet4_route_address` and `inet6_route_address` are merged into `route_address`, +`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. + +Old fields were removed in sing-box 1.12.0. + +#### Match source rule items are renamed + +`rule_set_ipcidr_match_source` route and DNS rule items are renamed to +`rule_set_ip_cidr_match_source` and were removed in sing-box 1.11.0. + +#### Drop support for go1.18 and go1.19 + +Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. + +## 1.8.0 + +#### Cache file and related features in Clash API + +`cache_file` and related features in Clash API is migrated to independent `cache_file` options, +check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +#### GeoIP + +GeoIP is deprecated and was removed in sing-box 1.12.0. + +The maxmind GeoIP National Database, as an IP classification database, +is not entirely suitable for traffic bypassing, +and all existing implementations suffer from high memory usage and difficult management. + +sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace GeoIP, +check [Migration](/migration/#migrate-geoip-to-rule-sets). + +#### Geosite + +Geosite is deprecated and was removed in sing-box 1.12.0. + +Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, +suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. + +sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace Geosite, +check [Migration](/migration/#migrate-geosite-to-rule-sets). + +## 1.6.0 + +The following features will be marked deprecated in 1.5.0 and removed entirely in 1.6.0. + +#### ShadowsocksR + +ShadowsocksR support has never been enabled by default, since the most commonly used proxy sales panel in the +illegal industry stopped using this protocol, it does not make sense to continue to maintain it. + +#### Proxy Protocol + +Proxy Protocol is added by Pull Request, has problems, is only used by the backend of HTTP multiplexers such as nginx, +is intrusive, and is meaningless for proxy purposes. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md new file mode 100644 index 00000000..5dabd69f --- /dev/null +++ b/docs/deprecated.zh.md @@ -0,0 +1,168 @@ +--- +icon: material/delete-alert +--- + +# 废弃功能列表 + +## 1.14.0 + +#### TLS 中的内联 ACME 选项 + +TLS 中的内联 ACME 选项(`tls.acme`)已废弃, +且可以通过 ACME 证书提供者替代, +参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 旧版 DNS 规则动作 `strategy` 选项 + +旧版 DNS 规则动作 `strategy` 选项已废弃。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项 + +旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项已废弃, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### `independent_cache` DNS 选项 + +`independent_cache` DNS 选项已废弃。 +DNS 缓存现在始终按传输分离,使此选项不再需要, +参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### `store_rdrc` 缓存文件选项 + +`store_rdrc` 缓存文件选项已废弃, +参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 旧版地址筛选字段 (DNS 规则) + +旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +旧行为将在 sing-box 1.16.0 中被移除。 + +## 1.12.0 + +#### 旧的 DNS 服务器格式 + +DNS 服务器已重构, +参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式). + +旧格式已在 sing-box 1.14.0 中被移除。 + +#### `outbound` DNS 规则项 + +旧的 `outbound` DNS 规则已废弃, +且可被拨号字段代替, +参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项). + +#### 旧的 ECH 字段 + +ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案, +因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。 + +另外,`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。 + +相关字段已在 sing-box 1.13.0 中被移除。 + +## 1.11.0 + +#### 旧的特殊出站 + +旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。 + +旧字段已在 sing-box 1.13.0 中被移除。 + +#### 旧的入站字段 + +旧的入站字段(`inbound.`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。 + +旧字段已在 sing-box 1.13.0 中被移除。 + +#### direct 出站中的目标地址覆盖字段 + +direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 + +旧字段已在 sing-box 1.13.0 中被移除。 + +#### WireGuard 出站 + +WireGuard 出站已废弃且可以通过端点替代, +参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 + +旧出站已在 sing-box 1.13.0 中被移除。 + +#### TUN 的 GSO 字段 + +GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。 + +旧字段已在 sing-box 1.13.0 中被移除。 + +## 1.10.0 + +#### Match source 规则项已重命名 + +`rule_set_ipcidr_match_source` 路由和 DNS 规则项已被重命名为 +`rule_set_ip_cidr_match_source` 且已在 sing-box 1.11.0 中被移除。 + +#### TUN 地址字段已合并 + +`inet4_address` 和 `inet6_address` 已合并为 `address`, +`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, +`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 + +旧字段已在 sing-box 1.12.0 中被移除。 + +#### 移除对 go1.18 和 go1.19 的支持 + +由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。 + +## 1.8.0 + +#### Clash API 中的 Cache file 及相关功能 + +Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置, +参阅 [迁移指南](/zh/migration/#将缓存文件从-clash-api-迁移到独立选项)。 + +#### GeoIP + +GeoIP 已废弃且已在 sing-box 1.12.0 中被移除。 + +maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, +且现有的实现均存在内存使用大与管理困难的问题。 + +sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), +可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 + +#### Geosite + +Geosite 已废弃且已在 sing-box 1.12.0 中被移除。 + +Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, +存在着包括缺少维护、规则不准确和管理困难内的大量问题。 + +sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), +可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 + +## 1.6.0 + +下列功能已在 1.5.0 中标记为已弃用,并在 1.6.0 中完全删除。 + +#### ShadowsocksR + +ShadowsocksR 支持从未默认启用,自从常用的黑产代理销售面板停止使用该协议,继续维护它是没有意义的。 + +#### Proxy Protocol + +Proxy Protocol 支持由 Pull Request 添加,存在问题且仅由 HTTP 多路复用器(如 nginx)的后端使用,具有侵入性,对于代理目的毫无意义。 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..2af7222e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,31 @@ +--- +description: Welcome to the wiki page for the sing-box project. +--- + +# :material-home: Home + +Welcome to the wiki page for the sing-box project. + +The universal proxy platform. + +## License + +``` +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +In addition, no derivative work may use the name or imply association +with this application without prior consent. +``` diff --git a/docs/index.zh.md b/docs/index.zh.md new file mode 100644 index 00000000..72877118 --- /dev/null +++ b/docs/index.zh.md @@ -0,0 +1,31 @@ +--- +description: 欢迎来到该 sing-box 项目的文档页。 +--- + +# :material-home: 开始 + +欢迎来到该 sing-box 项目的文档页。 + +通用代理平台。 + +## 授权 + +``` +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +In addition, no derivative work may use the name or imply association +with this application without prior consent. +``` diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md new file mode 100644 index 00000000..48b53b17 --- /dev/null +++ b/docs/installation/build-from-source.md @@ -0,0 +1,126 @@ +--- +icon: material/file-code +--- + +# Build from source + +## :material-graph: Requirements + +### sing-box 1.11 + +* Go 1.23.1 - ~ + +### sing-box 1.10 + +* Go 1.20.0 - ~ + +### sing-box 1.9 + +* Go 1.18.5 - 1.22.x +* Go 1.20.0 - 1.22.x with tag `with_quic`, or `with_utls` enabled + +## :material-fast-forward: Simple Build + +```bash +make +``` + +Or build and install binary to `$GOBIN`: + +```bash +make install +``` + +## :material-cog: Custom Build + +```bash +TAGS="tag_a tag_b" make +``` + +or + +```bash +go build -tags "tag_a tag_b" ./cmd/sing-box +``` + +## :material-folder-settings: Build Tags + +| Build Tag | Enabled by default | Description | +|------------------------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | +| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | +| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | +| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | +| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | +| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale). | +| `with_ccm` | :material-check: | Build with Claude Code Multiplexer service support. | +| `with_ocm` | :material-check: | Build with OpenAI Codex Multiplexer service support. | +| `with_naive_outbound` | :material-check: | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). | +| `with_cloudflared` | :material-check: | Build with Cloudflare Tunnel inbound support, see [Cloudflared inbound](/configuration/inbound/cloudflared/). | +| `badlinkname` | :material-check: | Enable `go:linkname` access to internal standard library functions. Required because the Go standard library does not expose many low-level APIs needed by this project, and reimplementing them externally is impractical. Used for kTLS (kernel TLS offload) and raw TLS record manipulation. | +| `tfogo_checklinkname0` | :material-check: | Companion to `badlinkname`. Go 1.23+ enforces `go:linkname` restrictions via the linker; this tag signals the build uses `-checklinkname=0` to bypass that enforcement. | + +It is not recommended to change the default build tag list unless you really know what you are adding. + +## :material-wrench: Linker Flags + +The following `-ldflags` are used in official builds: + +| Flag | Description | +|-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 enabled Multipath TCP for listeners by default (`multipathtcp=2`). This may cause errors on low-level sockets, and sing-box has its own MPTCP control (`tcp_multi_path` option). This flag disables the Go default. | +| `-checklinkname=0` | Go 1.23+ linker rejects unauthorized `go:linkname` usage. This flag disables the check, required together with the `badlinkname` build tag. | + +## :material-package-variant: For Downstream Packagers + +The default build tag lists and linker flags are available as files in the repository for downstream packagers to reference directly: + +| File | Description | +|------|-------------| +| `release/DEFAULT_BUILD_TAGS` | Default for Linux (common architectures), Darwin, and Android. | +| `release/DEFAULT_BUILD_TAGS_WINDOWS` | Default for Windows (includes `with_purego`). | +| `release/DEFAULT_BUILD_TAGS_OTHERS` | Default for other platforms (no `with_naive_outbound`). | +| `release/LDFLAGS` | Required linker flags (see above). | + +## :material-layers: with_naive_outbound + +NaiveProxy outbound requires special build configurations depending on your target platform. + +### Supported Platforms + +| Platform | Architectures | Mode | Requirements | +|-----------------|--------------------------------------------------------|--------|-----------------------------------------------------------------| +| Linux | amd64, arm64 | purego | None (library included in official releases) | +| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium toolchain, glibc >= 2.31 (loong64: >= 2.36) at runtime | +| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium toolchain | +| Windows | amd64, arm64 | purego | None (library included in official releases) | +| Apple platforms | * | CGO | Xcode | +| Android | * | CGO | Android NDK | + +### Windows + +Use `with_purego` tag. + +For official releases, `libcronet.dll` is included in the archive. For self-built binaries, download from [cronet-go releases](https://github.com/sagernet/cronet-go/releases) and place in the same directory as `sing-box.exe` or in a directory listed in `PATH`. + +### Linux (purego, amd64/arm64 only) + +Use `with_purego` tag. + +For official releases, `libcronet.so` is included in the archive. For self-built binaries, download from [cronet-go releases](https://github.com/sagernet/cronet-go/releases) and place in the same directory as sing-box binary or in system library path. + +### Linux (CGO) + +See [cronet-go](https://github.com/sagernet/cronet-go#linux-build-instructions). + +- **glibc build**: Requires glibc >= 2.31 at runtime +- **musl build**: Use `with_musl` tag, statically linked, no runtime requirements + +### Apple platforms / Android + +See [cronet-go](https://github.com/sagernet/cronet-go). diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md new file mode 100644 index 00000000..4ffebdc6 --- /dev/null +++ b/docs/installation/build-from-source.zh.md @@ -0,0 +1,130 @@ +--- +icon: material/file-code +--- + +# 从源代码构建 + +## :material-graph: 要求 + +### sing-box 1.11 + +* Go 1.23.1 - ~ + +### sing-box 1.10 + +* Go 1.20.0 - ~ +* Go 1.21.0 - ~ with tag `with_ech` enabled + +### sing-box 1.9 + +* Go 1.18.5 - 1.22.x +* Go 1.20.0 - 1.22.x with tag `with_quic`, or `with_utls` enabled +* Go 1.21.0 - 1.22.x with tag `with_ech` enabled + +您可以从 https://go.dev/doc/install 下载并安装 Go,推荐使用最新版本。 + +## :material-fast-forward: 快速开始 + +```bash +make +``` + +或者构建二进制文件并将其安装到 `$GOBIN`: + +```bash +make install +``` + +## :material-cog: 自定义构建 + +```bash +TAGS="tag_a tag_b" make +``` + +or + +```bash +go build -tags "tag_a tag_b" ./cmd/sing-box +``` + +## :material-folder-settings: 构建标记 + +| 构建标记 | 默认启动 | 说明 | +|------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/zh/configuration/dns/server/), [Naive inbound](/zh/configuration/inbound/naive/), [Hysteria Inbound](/zh/configuration/inbound/hysteria/), [Hysteria Outbound](/zh/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/zh/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/zh/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/zh/configuration/dns/server/). | +| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/zh/configuration/outbound/wireguard/). | +| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/zh/configuration/shared/tls#utls). | +| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/zh/configuration/shared/tls/). | +| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/zh/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/zh/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/zh/configuration/inbound/tun#stack) and [WireGuard outbound](/zh/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/zh/configuration/outbound/tor/). | +| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/zh/configuration/endpoint/tailscale)。 | +| `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 | +| `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 | +| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 | +| `with_cloudflared` | :material-check: | 构建 Cloudflare Tunnel 入站支持,参阅 [Cloudflared 入站](/zh/configuration/inbound/cloudflared/)。 | +| `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API,且在外部重新实现不切实际。用于 kTLS(内核 TLS 卸载)和原始 TLS 记录操作。 | +| `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 | + +除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。 + +## :material-wrench: 链接器标志 + +以下 `-ldflags` 在官方构建中使用: + +| 标志 | 说明 | +|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 默认为监听器启用 Multipath TCP(`multipathtcp=2`)。这可能在底层 socket 上导致错误,且 sing-box 有自己的 MPTCP 控制(`tcp_multi_path` 选项)。此标志禁用 Go 的默认行为。 | +| `-checklinkname=0` | Go 1.23+ 链接器拒绝未授权的 `go:linkname` 使用。此标志禁用该检查,需要与 `badlinkname` 构建标记一起使用。 | + +## :material-package-variant: 下游打包者 + +默认构建标签列表和链接器标志以文件形式存放在仓库中,供下游打包者直接引用: + +| 文件 | 说明 | +|------|------| +| `release/DEFAULT_BUILD_TAGS` | Linux(常见架构)、Darwin 和 Android 的默认标签。 | +| `release/DEFAULT_BUILD_TAGS_WINDOWS` | Windows 的默认标签(包含 `with_purego`)。 | +| `release/DEFAULT_BUILD_TAGS_OTHERS` | 其他平台的默认标签(不含 `with_naive_outbound`)。 | +| `release/LDFLAGS` | 必需的链接器标志(参见上文)。 | + +## :material-layers: with_naive_outbound + +NaiveProxy 出站需要根据目标平台进行特殊的构建配置。 + +### 支持的平台 + +| 平台 | 架构 | 模式 | 要求 | +|--------------|----------------------------------------------------------|--------|-----------------------------------------------------| +| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | +| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31(loong64: >= 2.36) | +| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium 工具链 | +| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | +| Apple 平台 | * | CGO | Xcode | +| Android | * | CGO | Android NDK | + +### Windows + +使用 `with_purego` 标记。 + +官方发布版本已包含 `libcronet.dll`。自行构建时,从 [cronet-go releases](https://github.com/sagernet/cronet-go/releases) 下载并放置在 `sing-box.exe` 相同目录或 `PATH` 中的任意目录。 + +### Linux (purego, 仅 amd64/arm64) + +使用 `with_purego` 标记。 + +官方发布版本已包含 `libcronet.so`。自行构建时,从 [cronet-go releases](https://github.com/sagernet/cronet-go/releases) 下载并放置在 sing-box 二进制文件相同目录或系统库路径中。 + +### Linux (CGO) + +参阅 [cronet-go](https://github.com/sagernet/cronet-go#linux-build-instructions)。 + +- **glibc 构建**:运行时需要 glibc >= 2.31 +- **musl 构建**:使用 `with_musl` 标记,静态链接,无运行时要求 + +### Apple 平台 / Android + +参阅 [cronet-go](https://github.com/sagernet/cronet-go)。 diff --git a/docs/installation/docker.md b/docs/installation/docker.md new file mode 100644 index 00000000..ae1682dd --- /dev/null +++ b/docs/installation/docker.md @@ -0,0 +1,31 @@ +--- +icon: material/docker +--- + +# Docker + +## :material-console: Command + +```bash +docker run -d \ + -v /etc/sing-box:/etc/sing-box/ \ + --name=sing-box \ + --restart=always \ + ghcr.io/sagernet/sing-box \ + -D /var/lib/sing-box \ + -C /etc/sing-box/ run +``` + +## :material-box-shadow: Compose + +```yaml +version: "3.8" +services: + sing-box: + image: ghcr.io/sagernet/sing-box + container_name: sing-box + restart: always + volumes: + - /etc/sing-box:/etc/sing-box/ + command: -D /var/lib/sing-box -C /etc/sing-box/ run +``` diff --git a/docs/installation/docker.zh.md b/docs/installation/docker.zh.md new file mode 100644 index 00000000..ecc66e3a --- /dev/null +++ b/docs/installation/docker.zh.md @@ -0,0 +1,31 @@ +--- +icon: material/docker +--- + +# Docker + +## :material-console: 命令 + +```bash +docker run -d \ + -v /etc/sing-box:/etc/sing-box/ \ + --name=sing-box \ + --restart=always \ + ghcr.io/sagernet/sing-box \ + -D /var/lib/sing-box \ + -C /etc/sing-box/ run +``` + +## :material-box-shadow: Compose + +```yaml +version: "3.8" +services: + sing-box: + image: ghcr.io/sagernet/sing-box + container_name: sing-box + restart: always + volumes: + - /etc/sing-box:/etc/sing-box/ + command: -D /var/lib/sing-box -C /etc/sing-box/ run +``` diff --git a/docs/installation/package-manager.md b/docs/installation/package-manager.md new file mode 100644 index 00000000..fe59c73a --- /dev/null +++ b/docs/installation/package-manager.md @@ -0,0 +1,157 @@ +--- +icon: material/package +--- + +# Package Manager + +## :material-tram: Repository Installation + +=== ":material-debian: Debian / APT" + + ```bash + sudo mkdir -p /etc/apt/keyrings && + sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc && + sudo chmod a+r /etc/apt/keyrings/sagernet.asc && + echo ' + Types: deb + URIs: https://deb.sagernet.org/ + Suites: * + Components: * + Enabled: yes + Signed-By: /etc/apt/keyrings/sagernet.asc + ' | sudo tee /etc/apt/sources.list.d/sagernet.sources && + sudo apt-get update && + sudo apt-get install sing-box # or sing-box-beta + ``` + +=== ":material-redhat: Redhat / DNF 5" + + ```bash + sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo && + sudo dnf install sing-box # or sing-box-beta + ``` + +=== ":material-redhat: Redhat / DNF 4" + + ```bash + sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo && + sudo dnf -y install dnf-plugins-core && + sudo dnf install sing-box # or sing-box-beta + ``` + +## :material-download-box: Manual Installation + +The script download and install the latest package from GitHub releases +for deb or rpm based Linux distributions, ArchLinux and OpenWrt. + +```shell +curl -fsSL https://sing-box.app/install.sh | sh +``` + +or latest beta: + +```shell +curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta +``` + +or specific version: + +```shell +curl -fsSL https://sing-box.app/install.sh | sh -s -- --version +``` + +## :material-book-lock-open: Managed Installation + +=== ":material-linux: Linux" + + | Type | Platform | Command | Link | + |----------|---------------|------------------------------|---------------------------------------------------------------------------------------------------------------| + | AUR | Arch Linux | `? -S sing-box` | [![AUR package](https://repology.org/badge/version-for-repo/aur/sing-box.svg)][aur] | + | nixpkgs | NixOS | `nix-env -iA nixos.sing-box` | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/sing-box.svg)][nixpkgs] | + | Homebrew | macOS / Linux | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | + | APK | Alpine | `apk add sing-box` | [![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/sing-box.svg)][alpine] | + | DEB | AOSC | `apt install sing-box` | [![AOSC package](https://repology.org/badge/version-for-repo/aosc/sing-box.svg)][aosc] | + +=== ":material-apple: macOS" + + | Type | Platform | Command | Link | + |----------|----------|-------------------------|------------------------------------------------------------------------------------------------| + | Homebrew | macOS | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | + +=== ":material-microsoft-windows: Windows" + + | Type | Platform | Command | Link | + |------------|----------|---------------------------|-----------------------------------------------------------------------------------------------------| + | Scoop | Windows | `scoop install sing-box` | [![Scoop package](https://repology.org/badge/version-for-repo/scoop/sing-box.svg)][scoop] | + | Chocolatey | Windows | `choco install sing-box` | [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/sing-box.svg)][choco] | + | winget | Windows | `winget install sing-box` | [![winget package](https://repology.org/badge/version-for-repo/winget/sing-box.svg)][winget] | + +=== ":material-android: Android" + + | Type | Platform | Command | Link | + |--------|----------|--------------------|----------------------------------------------------------------------------------------------| + | Termux | Android | `pkg add sing-box` | [![Termux package](https://repology.org/badge/version-for-repo/termux/sing-box.svg)][termux] | + +=== ":material-freebsd: FreeBSD" + + | Type | Platform | Command | Link | + |------------|----------|------------------------|--------------------------------------------------------------------------------------------| + | FreshPorts | FreeBSD | `pkg install sing-box` | [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/sing-box.svg)][ports] | + +## :material-alert: Problematic Sources + +| Type | Platform | Link | Promblem(s) | +|------------|----------|-------------------------------------------------------------------------------------------|-----------------------------------------| +| DEB | AOSC | [aosc-os-abbs](https://github.com/AOSC-Dev/aosc-os-abbs/tree/stable/app-network/sing-box) | Problematic build tag list modification | +| Homebrew | / | [homebrew-core][brew] | Problematic build tag list modification | +| Termux | Android | [termux-packages][termux] | Problematic build tag list modification | +| FreshPorts | FreeBSD | [FreeBSD ports][ports] | Old Go (go1.20) | + +If you are a user of them, please report issues to them: + +1. Please do not modify release build tags without full understanding of the related functionality: enabling non-default + labels may result in decreased performance; the lack of default labels may cause user confusion. +2. sing-box supports compiling with some older Go versions, but it is not recommended (especially versions that are no + longer supported by Go). + +## :material-book-multiple: Service Management + +For Linux systems with [systemd][systemd], usually the installation already includes a sing-box service, +you can manage the service using the following command: + +| Operation | Command | +|-----------|-----------------------------------------------| +| Enable | `sudo systemctl enable sing-box` | +| Disable | `sudo systemctl disable sing-box` | +| Start | `sudo systemctl start sing-box` | +| Stop | `sudo systemctl stop sing-box` | +| Kill | `sudo systemctl kill sing-box` | +| Restart | `sudo systemctl restart sing-box` | +| Logs | `sudo journalctl -u sing-box --output cat -e` | +| New Logs | `sudo journalctl -u sing-box --output cat -f` | + +[alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box + +[aur]: https://aur.archlinux.org/packages/sing-box + +[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix + +[brew]: https://formulae.brew.sh/formula/sing-box + +[openwrt]: https://github.com/openwrt/packages/tree/master/net/sing-box + +[immortalwrt]: https://github.com/immortalwrt/packages/tree/master/net/sing-box + +[choco]: https://chocolatey.org/packages/sing-box + +[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json + +[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box + +[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box + +[ports]: https://www.freshports.org/net/sing-box + +[aosc]: https://packages.aosc.io/packages/sing-box + +[systemd]: https://systemd.io/ diff --git a/docs/installation/package-manager.zh.md b/docs/installation/package-manager.zh.md new file mode 100644 index 00000000..f942e924 --- /dev/null +++ b/docs/installation/package-manager.zh.md @@ -0,0 +1,150 @@ +--- +icon: material/package +--- + +# 包管理器 + +## :material-tram: 仓库安装 + +=== ":material-debian: Debian / APT" + + ```bash + sudo mkdir -p /etc/apt/keyrings && + sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc && + sudo chmod a+r /etc/apt/keyrings/sagernet.asc && + echo ' + Types: deb + URIs: https://deb.sagernet.org/ + Suites: * + Components: * + Enabled: yes + Signed-By: /etc/apt/keyrings/sagernet.asc + ' | sudo tee /etc/apt/sources.list.d/sagernet.sources && + sudo apt-get update && + sudo apt-get install sing-box # or sing-box-beta + ``` + +=== ":material-redhat: Redhat / DNF 5" + + ```bash + sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo && + sudo dnf install sing-box # or sing-box-beta + ``` + +=== ":material-redhat: Redhat / DNF 4" + + ```bash + sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo && + sudo dnf -y install dnf-plugins-core && + sudo dnf install sing-box # or sing-box-beta + ``` + +## :material-download-box: 手动安装 + +该脚本从 GitHub 发布中下载并安装最新的软件包,适用于基于 deb 或 rpm 的 Linux 发行版、ArchLinux 和 OpenWrt。 + +```shell +curl -fsSL https://sing-box.app/install.sh | sh +``` + +或最新测试版: + +```shell +curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta +``` + +或指定版本: + +```shell +curl -fsSL https://sing-box.app/install.sh | sh -s -- --version +``` + +## :material-book-lock-open: 托管安装 + +=== ":material-linux: Linux" + + | 类型 | 平台 | 命令 | 链接 | + |----------|---------------|------------------------------|---------------------------------------------------------------------------------------------------------------| + | AUR | Arch Linux | `? -S sing-box` | [![AUR package](https://repology.org/badge/version-for-repo/aur/sing-box.svg)][aur] | + | nixpkgs | NixOS | `nix-env -iA nixos.sing-box` | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/sing-box.svg)][nixpkgs] | + | Homebrew | macOS / Linux | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | + | APK | Alpine | `apk add sing-box` | [![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/sing-box.svg)][alpine] | + | DEB | AOSC | `apt install sing-box` | [![AOSC package](https://repology.org/badge/version-for-repo/aosc/sing-box.svg)][aosc] | + +=== ":material-apple: macOS" + + | 类型 | 平台 | 命令 | 链接 | + |----------|-------|-------------------------|------------------------------------------------------------------------------------------------| + | Homebrew | macOS | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | + +=== ":material-microsoft-windows: Windows" + + | 类型 | 平台 | 命令 | 链接 | + |------------|---------|---------------------------|-----------------------------------------------------------------------------------------------------| + | Scoop | Windows | `scoop install sing-box` | [![Scoop package](https://repology.org/badge/version-for-repo/scoop/sing-box.svg)][scoop] | + | Chocolatey | Windows | `choco install sing-box` | [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/sing-box.svg)][choco] | + | winget | Windows | `winget install sing-box` | [![winget package](https://repology.org/badge/version-for-repo/winget/sing-box.svg)][winget] | + +=== ":material-android: Android" + + | 类型 | 平台 | 命令 | 链接 | + |--------|---------|--------------------|----------------------------------------------------------------------------------------------| + | Termux | Android | `pkg add sing-box` | [![Termux package](https://repology.org/badge/version-for-repo/termux/sing-box.svg)][termux] | + +=== ":material-freebsd: FreeBSD" + + | 类型 | 平台 | 命令 | 链接 | + |------------|---------|------------------------|--------------------------------------------------------------------------------------------| + | FreshPorts | FreeBSD | `pkg install sing-box` | [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/sing-box.svg)][ports] | + +## :material-alert: 存在问题的源 + +| 类型 | 平台 | 链接 | 原因 | +|------------|---------|-------------------------------------------------------------------------------------------|-----------------| +| DEB | AOSC | [aosc-os-abbs](https://github.com/AOSC-Dev/aosc-os-abbs/tree/stable/app-network/sing-box) | 存在问题的构建标志列表修改 | +| Homebrew | / | [homebrew-core][brew] | 存在问题的构建标志列表修改 | +| Termux | Android | [termux-packages][termux] | 存在问题的构建标志列表修改 | +| FreshPorts | FreeBSD | [FreeBSD ports][ports] | 太旧的 Go (go1.20) | + +如果您是其用户,请向他们报告问题: + +1. 在未完全了解相关功能的情况下,请勿修改发布版本标签:启用非默认标签可能会导致性能下降;缺少默认标签可能会引起用户混淆。 +2. sing-box 支持使用一些较旧的 Go 版本进行编译,但不推荐使用(特别是已不再受 Go 支持的版本)。 + +## :material-book-multiple: 服务管理 + +对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 sing-box 服务, +您可以使用以下命令管理服务: + +| 行动 | 命令 | +|------|-----------------------------------------------| +| 启用 | `sudo systemctl enable sing-box` | +| 禁用 | `sudo systemctl disable sing-box` | +| 启动 | `sudo systemctl start sing-box` | +| 停止 | `sudo systemctl stop sing-box` | +| 强行停止 | `sudo systemctl kill sing-box` | +| 重新启动 | `sudo systemctl restart sing-box` | +| 查看日志 | `sudo journalctl -u sing-box --output cat -e` | +| 实时日志 | `sudo journalctl -u sing-box --output cat -f` | + +[alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box + +[aur]: https://aur.archlinux.org/packages/sing-box + +[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix + +[brew]: https://formulae.brew.sh/formula/sing-box + +[choco]: https://chocolatey.org/packages/sing-box + +[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json + +[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box + +[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box + +[ports]: https://www.freshports.org/net/sing-box + +[aosc]: https://packages.aosc.io/packages/sing-box + +[systemd]: https://systemd.io/ diff --git a/docs/installation/tools/arch-install.sh b/docs/installation/tools/arch-install.sh new file mode 100644 index 00000000..50ce5767 --- /dev/null +++ b/docs/installation/tools/arch-install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e -o pipefail + +ARCH_RAW=$(uname -m) +case "${ARCH_RAW}" in + 'x86_64') ARCH='amd64';; + 'x86' | 'i686' | 'i386') ARCH='386';; + 'aarch64' | 'arm64') ARCH='arm64';; + 'armv7l') ARCH='armv7';; + 's390x') ARCH='s390x';; + *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; +esac + +VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep tag_name \ + | cut -d ":" -f2 \ + | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') + +curl -Lo sing-box.pkg.tar.zst "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.pkg.tar.zst" +sudo pacman -U sing-box.pkg.tar.zst +rm sing-box.pkg.tar.zst diff --git a/docs/installation/tools/deb-install.sh b/docs/installation/tools/deb-install.sh new file mode 100644 index 00000000..587b6f61 --- /dev/null +++ b/docs/installation/tools/deb-install.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e -o pipefail + +ARCH_RAW=$(uname -m) +case "${ARCH_RAW}" in + 'x86_64') ARCH='amd64';; + 'x86' | 'i686' | 'i386') ARCH='386';; + 'aarch64' | 'arm64') ARCH='arm64';; + 'armv7l') ARCH='armv7';; + 's390x') ARCH='s390x';; + *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; +esac + +VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep tag_name \ + | cut -d ":" -f2 \ + | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') + +curl -Lo sing-box.deb "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.deb" +sudo dpkg -i sing-box.deb +rm sing-box.deb + diff --git a/docs/installation/tools/install.sh b/docs/installation/tools/install.sh new file mode 100644 index 00000000..7bfbc365 --- /dev/null +++ b/docs/installation/tools/install.sh @@ -0,0 +1,127 @@ +#!/bin/sh + +download_beta=false +download_version="" + +while [ $# -gt 0 ]; do + case "$1" in + --beta) + download_beta=true + shift + ;; + --version) + shift + if [ $# -eq 0 ]; then + echo "Missing argument for --version" + echo "Usage: $0 [--beta] [--version ]" + exit 1 + fi + download_version="$1" + shift + ;; + *) + echo "Unknown argument: $1" + echo "Usage: $0 [--beta] [--version ]" + exit 1 + ;; + esac +done + +if command -v pacman >/dev/null 2>&1; then + os="linux" + arch=$(uname -m) + package_suffix=".pkg.tar.zst" + package_install="pacman -U --noconfirm" +elif command -v dpkg >/dev/null 2>&1; then + os="linux" + arch=$(dpkg --print-architecture) + package_suffix=".deb" + package_install="dpkg -i" +elif command -v dnf >/dev/null 2>&1; then + os="linux" + arch=$(uname -m) + package_suffix=".rpm" + package_install="dnf install -y" +elif command -v rpm >/dev/null 2>&1; then + os="linux" + arch=$(uname -m) + package_suffix=".rpm" + package_install="rpm -i" +elif command -v apk >/dev/null 2>&1 && [ -f /etc/os-release ] && grep -q OPENWRT_ARCH /etc/os-release; then + os="openwrt" + . /etc/os-release + arch="$OPENWRT_ARCH" + package_suffix=".apk" + package_install="apk add --allow-untrusted" +elif command -v apk >/dev/null 2>&1; then + os="linux" + arch=$(apk --print-arch) + package_suffix=".apk" + package_install="apk add --allow-untrusted" +elif command -v opkg >/dev/null 2>&1; then + os="openwrt" + . /etc/os-release + arch="$OPENWRT_ARCH" + package_suffix=".ipk" + package_install="opkg update && opkg install" +else + echo "Missing supported package manager." + exit 1 +fi + +if [ -z "$download_version" ]; then + if [ "$download_beta" != "true" ]; then + if [ -n "$GITHUB_TOKEN" ]; then + latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases/latest) + else + latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest) + fi + curl_exit_status=$? + if [ $curl_exit_status -ne 0 ]; then + exit $curl_exit_status + fi + if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then + echo "$latest_release" + exit 1 + fi + download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g') + else + if [ -n "$GITHUB_TOKEN" ]; then + latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases) + else + latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases) + fi + curl_exit_status=$? + if [ $curl_exit_status -ne 0 ]; then + exit $curl_exit_status + fi + if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then + echo "$latest_release" + exit 1 + fi + download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g') + fi +fi + +package_name="sing-box_${download_version}_${os}_${arch}${package_suffix}" +package_url="https://github.com/SagerNet/sing-box/releases/download/v${download_version}/${package_name}" + +echo "Downloading $package_url" +if [ -n "$GITHUB_TOKEN" ]; then + curl --fail -Lo "$package_name" -H "Authorization: token ${GITHUB_TOKEN}" "$package_url" +else + curl --fail -Lo "$package_name" "$package_url" +fi + +curl_exit_status=$? +if [ $curl_exit_status -ne 0 ]; then + exit $curl_exit_status +fi + +if command -v sudo >/dev/null 2>&1; then + package_install="sudo $package_install" +fi + +echo "$package_install $package_name" +sh -c "$package_install \"$package_name\"" +rm -f "$package_name" diff --git a/docs/installation/tools/rpm-install.sh b/docs/installation/tools/rpm-install.sh new file mode 100644 index 00000000..fc3d7314 --- /dev/null +++ b/docs/installation/tools/rpm-install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e -o pipefail + +ARCH_RAW=$(uname -m) +case "${ARCH_RAW}" in + 'x86_64') ARCH='amd64';; + 'x86' | 'i686' | 'i386') ARCH='386';; + 'aarch64' | 'arm64') ARCH='arm64';; + 'armv7l') ARCH='armv7';; + 's390x') ARCH='s390x';; + *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; +esac + +VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep tag_name \ + | cut -d ":" -f2 \ + | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') + +curl -Lo sing-box.rpm "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.rpm" +sudo rpm -i sing-box.rpm +rm sing-box.rpm diff --git a/docs/installation/tools/sing-box.repo b/docs/installation/tools/sing-box.repo new file mode 100644 index 00000000..bf874825 --- /dev/null +++ b/docs/installation/tools/sing-box.repo @@ -0,0 +1,7 @@ +[sing-box] +name=sing-box +baseurl=https://rpm.sagernet.org/ +enabled=1 +repo_gpgcheck=1 +gpgcheck=1 +gpgkey=https://sing-box.app/gpg.key diff --git a/docs/manual/misc/tunnelvision.md b/docs/manual/misc/tunnelvision.md new file mode 100644 index 00000000..0d6caf76 --- /dev/null +++ b/docs/manual/misc/tunnelvision.md @@ -0,0 +1,38 @@ +--- +icon: material/book-lock-open +--- + +# TunnelVision + +TunnelVision is an attack that uses DHCP option 121 to set higher priority routes +so that traffic does not go through the VPN. + +Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3661 + +## Status + +### Android + +Android does not handle DHCP option 121 and is not affected. + +### Apple platforms + +Update [sing-box graphical client](/clients/apple/#download) to `1.9.0-rc.16` or newer, +then enable `includeAllNetworks` in `Settings` — `Packet Tunnel` and you will be unaffected. + +Note: when `includeAllNetworks` is enabled, the default TUN stack is changed to `gvisor`, +and the `system` and `mixed` stacks are not available. + +### Linux + +Update sing-box to `1.9.0-rc.16` or newer, rules generated by `auto-route` are unaffected. + +### Windows + +No solution yet. + +## Workarounds + +* Don't connect to untrusted networks +* Relay untrusted network through another device +* Just ignore it diff --git a/docs/manual/proxy-protocol/hysteria2.md b/docs/manual/proxy-protocol/hysteria2.md new file mode 100644 index 00000000..fe605b45 --- /dev/null +++ b/docs/manual/proxy-protocol/hysteria2.md @@ -0,0 +1,212 @@ +--- +icon: material/lightning-bolt +--- + +# Hysteria 2 + +Hysteria 2 is a simple, Chinese-made protocol based on QUIC. +The selling point is Brutal, a congestion control algorithm that +tries to achieve a user-defined bandwidth despite packet loss. + +!!! warning + + Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more obvious characteristics than TCP based proxies. + +| Specification | Resists passive detection | Resists active probes | +|---------------------------------------------------------------------------|---------------------------|-----------------------| +| [hysteria.network](https://v2.hysteria.network/docs/developers/Protocol/) | :material-alert: | :material-check: | + +## :material-text-box-check: Password Generator + +| Generate Password | Action | +|----------------------------|-----------------------------------------------------------------| +| | | + + + +## :material-alert: Difference from official Hysteria + +The official program supports an authentication method called **userpass**, +which essentially uses a combination of `:` as the actual password, +while sing-box does not provide this alias. +To use sing-box with the official program, you need to fill in that combination as the actual password. + +## :material-server: Server Example + +!!! info "" + + Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your server. + +=== ":material-harddisk: With local certificate" + + ```json + { + "inbounds": [ + { + "type": "hysteria2", + "listen": "::", + "listen_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "key_path": "/path/to/key.pem", + "certificate_path": "/path/to/certificate.pem" + } + } + ] + } + ``` + +=== ":material-auto-fix: With ACME" + + ```json + { + "inbounds": [ + { + "type": "hysteria2", + "listen": "::", + "listen_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org" + } + } + } + ] + } + ``` + +=== ":material-cloud: With ACME and Cloudflare API" + + ```json + { + "inbounds": [ + { + "type": "hysteria2", + "listen": "::", + "listen_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org", + "dns01_challenge": { + "provider": "cloudflare", + "api_token": "my_token" + } + } + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +!!! info "" + + Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your client. + +=== ":material-web-check: With valid certificate" + + ```json + { + "outbounds": [ + { + "type": "hysteria2", + "server": "127.0.0.1", + "server_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "password": "", + "tls": { + "enabled": true, + "server_name": "example.org" + } + } + ] + } + ``` + +=== ":material-check: With self-sign certificate" + + !!! info "Tip" + + Use `sing-box merge` command to merge configuration and certificate into one file. + + ```json + { + "outbounds": [ + { + "type": "hysteria2", + "server": "127.0.0.1", + "server_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "password": "", + "tls": { + "enabled": true, + "server_name": "example.org", + "certificate_path": "/path/to/certificate.pem" + } + } + ] + } + ``` + +=== ":material-alert: Ignore certificate verification" + + ```json + { + "outbounds": [ + { + "type": "hysteria2", + "server": "127.0.0.1", + "server_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "password": "", + "tls": { + "enabled": true, + "server_name": "example.org", + "insecure": true + } + } + ] + } + ``` diff --git a/docs/manual/proxy-protocol/shadowsocks.md b/docs/manual/proxy-protocol/shadowsocks.md new file mode 100644 index 00000000..19f9d4cf --- /dev/null +++ b/docs/manual/proxy-protocol/shadowsocks.md @@ -0,0 +1,125 @@ +--- +icon: material/send +--- + +# Shadowsocks + +Shadowsocks is the most well-known Chinese-made proxy protocol. +It exists in multiple versions, but only AEAD 2022 ciphers +over TCP with multiplexing is recommended. + +| Ciphers | Specification | Cryptographically sound | Resists passive detection | Resists active probes | +|----------------|------------------------------------------------------------|-------------------------|---------------------------|-----------------------| +| Stream Ciphers | [shadowsocks.org](https://shadowsocks.org/doc/stream.html) | :material-alert: | :material-alert: | :material-alert: | +| AEAD | [shadowsocks.org](https://shadowsocks.org/doc/aead.html) | :material-check: | :material-alert: | :material-alert: | +| AEAD 2022 | [shadowsocks.org](https://shadowsocks.org/doc/sip022.html) | :material-check: | :material-check: | :material-help: | + +(We strongly recommend using multiplexing to send UDP traffic over TCP, because +doing otherwise is vulnerable to passive detection.) + +## :material-text-box-check: Password Generator + +| For `2022-blake3-aes-128-gcm` cipher | For other ciphers | Action | +|--------------------------------------|-------------------------------|-----------------------------------------------------------------| +| | | | + + + +## :material-server: Server Example + +=== ":material-account: Single-user" + + ```json + { + "inbounds": [ + { + "type": "shadowsocks", + "listen": "::", + "listen_port": 8080, + "network": "tcp", + "method": "2022-blake3-aes-128-gcm", + "password": "", + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-account-multiple: Multi-user" + + ```json + { + "inbounds": [ + { + "type": "shadowsocks", + "listen": "::", + "listen_port": 8080, + "network": "tcp", + "method": "2022-blake3-aes-128-gcm", + "password": "", + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +=== ":material-account: Single-user" + + ```json + { + "outbounds": [ + { + "type": "shadowsocks", + "server": "127.0.0.1", + "server_port": 8080, + "method": "2022-blake3-aes-128-gcm", + "password": "", + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-account-multiple: Multi-user" + + ```json + { + "outbounds": [ + { + "type": "shadowsocks", + "server": "127.0.0.1", + "server_port": 8080, + "method": "2022-blake3-aes-128-gcm", + "password": ":", + "multiplex": { + "enabled": true + } + } + ] + } + ``` diff --git a/docs/manual/proxy-protocol/trojan.md b/docs/manual/proxy-protocol/trojan.md new file mode 100644 index 00000000..2bd63f5c --- /dev/null +++ b/docs/manual/proxy-protocol/trojan.md @@ -0,0 +1,200 @@ +--- +icon: material/horse +--- + +# Trojan + +Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations. + +| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes | +|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------| +| Origin / trojan-gfw | [trojan-gfw.github.io](https://trojan-gfw.github.io/trojan/protocol) | :material-check: | :material-check: | +| Basic Go implementation | / | :material-alert: | :material-check: | +| with privates transport by V2Ray | No formal definition | :material-alert: | :material-alert: | +| with uTLS enabled | No formal definition | :material-help: | :material-check: | + +## :material-text-box-check: Password Generator + +| Generate Password | Action | +|----------------------------|-----------------------------------------------------------------| +| | | + + + +## :material-server: Server Example + +=== ":material-harddisk: With local certificate" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "example", + "password": "password" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "key_path": "/path/to/key.pem", + "certificate_path": "/path/to/certificate.pem" + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-auto-fix: With ACME" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "example", + "password": "password" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org" + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-cloud: With ACME and Cloudflare API" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "example", + "password": "password" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org", + "dns01_challenge": { + "provider": "cloudflare", + "api_token": "my_token" + } + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +=== ":material-web-check: With valid certificate" + + ```json + { + "outbounds": [ + { + "type": "trojan", + "server": "127.0.0.1", + "server_port": 8080, + "password": "password", + "tls": { + "enabled": true, + "server_name": "example.org" + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-check: With self-sign certificate" + + !!! info "Tip" + + Use `sing-box merge` command to merge configuration and certificate into one file. + + ```json + { + "outbounds": [ + { + "type": "trojan", + "server": "127.0.0.1", + "server_port": 8080, + "password": "password", + "tls": { + "enabled": true, + "server_name": "example.org", + "certificate_path": "/path/to/certificate.pem" + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-alert: Ignore certificate verification" + + ```json + { + "outbounds": [ + { + "type": "trojan", + "server": "127.0.0.1", + "server_port": 8080, + "password": "password", + "tls": { + "enabled": true, + "server_name": "example.org", + "insecure": true + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md new file mode 100644 index 00000000..e8c80523 --- /dev/null +++ b/docs/manual/proxy/client.md @@ -0,0 +1,503 @@ +--- +icon: material/cellphone-link +--- + +# Client + +### :material-ray-start: Introduction + +For a long time, the modern usage and principles of proxy clients +for graphical operating systems have not been clearly described. +However, we can categorize them into three types: +system proxy, firewall redirection, and virtual interface. + +### :material-web-refresh: System Proxy + +Almost all graphical environments support system-level proxies, +which are essentially ordinary HTTP proxies that only support TCP. + +| Operating System / Desktop Environment | System Proxy | Application Support | +|:---------------------------------------------|:-------------------------------------|:--------------------| +| Windows | :material-check: | :material-check: | +| macOS | :material-check: | :material-check: | +| GNOME/KDE | :material-check: | :material-check: | +| Android | ROOT or adb (permission) is required | :material-check: | +| Android/iOS (with sing-box graphical client) | via `tun.platform.http_proxy` | :material-check: | + +As one of the most well-known proxy methods, it has many shortcomings: +many TCP clients that are not based on HTTP do not check and use the system proxy. +Moreover, UDP and ICMP traffics bypass the proxy. + +```mermaid +flowchart LR + dns[DNS query] -- Is HTTP request? --> proxy[HTTP proxy] + dns --> leak[Leak] + tcp[TCP connection] -- Is HTTP request? --> proxy + tcp -- Check and use HTTP CONNECT? --> proxy + tcp --> leak + udp[UDP packet] --> leak +``` + +### :material-wall-fire: Firewall Redirection + +This type of usage typically relies on the firewall or hook interface provided by the operating system, +such as Windows’ WFP, Linux’s redirect, TProxy and eBPF, and macOS’s pf. +Although it is intrusive and cumbersome to configure, +it remains popular within the community of amateur proxy open source projects like V2Ray, +due to the low technical requirements it imposes on the software. + +### :material-expansion-card: Virtual Interface + +All L2/L3 proxies (seriously defined VPNs, such as OpenVPN, WireGuard) are based on virtual network interfaces, +which is also the only way for all L4 proxies to work as VPNs on mobile platforms like Android, iOS. + +The sing-box inherits and develops clash-premium’s TUN inbound (L3 to L4 conversion) +as the most reasonable method for performing transparent proxying. + +```mermaid +flowchart TB + packet[IP Packet] + packet --> windows[Windows / macOS] + packet --> linux[Linux] + tun[TUN interface] + windows -. route .-> tun + linux -. iproute2 route/rule .-> tun + tun --> gvisor[gVisor TUN stack] + tun --> system[system TUN stack] + assemble([L3 to L4 assemble]) + gvisor --> assemble + system --> assemble + assemble --> conn[TCP and UDP connections] + conn --> router[sing-box Router] + router --> direct[Direct outbound] + router --> proxy[Proxy outbounds] + router -- DNS hijack --> dns_out[DNS outbound] + dns_out --> dns_router[DNS router] + dns_router --> router + direct --> adi([auto detect interface]) + proxy --> adi + adi --> default[Default network interface in the system] + default --> destination[Destination server] + default --> proxy_server[Proxy server] + proxy_server --> destination +``` + +## :material-cellphone-link: Examples + +### Basic TUN usage for Chinese users + +=== ":material-numeric-4-box: IPv4 only" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "type": "tls", + "server": "8.8.8.8" + }, + { + "tag": "local", + "type": "udp", + "server": "223.5.5.5" + } + ], + "strategy": "ipv4_only" + }, + "inbounds": [ + { + "type": "tun", + "address": ["172.19.0.1/30"], + "auto_route": true, + // "auto_redirect": true, // On linux + "strict_route": true + } + ], + "outbounds": [ + // ... + { + "type": "direct", + "tag": "direct" + } + ], + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + } + ], + "default_domain_resolver": "local", + "auto_detect_interface": true + } + } + ``` + +=== ":material-numeric-6-box: IPv4 & IPv6" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "type": "tls", + "server": "8.8.8.8" + }, + { + "tag": "local", + "type": "udp", + "server": "223.5.5.5" + } + ] + }, + "inbounds": [ + { + "type": "tun", + "address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"], + "auto_route": true, + // "auto_redirect": true, // On linux + "strict_route": true + } + ], + "outbounds": [ + // ... + { + "type": "direct", + "tag": "direct" + } + ], + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + } + ], + "default_domain_resolver": "local", + "auto_detect_interface": true + } + } + ``` + +=== ":material-domain-switch: FakeIP" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "type": "tls", + "server": "8.8.8.8" + }, + { + "tag": "local", + "type": "udp", + "server": "223.5.5.5" + }, + { + "tag": "remote", + "type": "fakeip", + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + ], + "rules": [ + { + "query_type": [ + "A", + "AAAA" + ], + "server": "remote" + } + ], + "independent_cache": true + }, + "inbounds": [ + { + "type": "tun", + "address": ["172.19.0.1/30","fdfe:dcba:9876::1/126"], + "auto_route": true, + // "auto_redirect": true, // On linux + "strict_route": true + } + ], + "outbounds": [ + // ... + { + "type": "direct", + "tag": "direct" + } + ], + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + } + ], + "default_domain_resolver": "local", + "auto_detect_interface": true + } + } + ``` + +### Traffic bypass usage for Chinese users + +=== ":material-dns: DNS rules" + + === ":material-shield-off: With DNS leaks" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "type": "tls", + "server": "8.8.8.8" + }, + { + "tag": "local", + "type": "https", + "server": "223.5.5.5" + } + ], + "rules": [ + { + "rule_set": "geosite-geolocation-cn", + "server": "local" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geoip-cn" + } + ], + "server": "local" + } + ] + }, + "route": { + "default_domain_resolver": "local", + "rule_set": [ + { + "type": "remote", + "tag": "geosite-geolocation-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, + "clash_api": { + "default_mode": "Enhanced" + } + } + } + ``` + + === ":material-security: Without DNS leaks, but slower" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "type": "tls", + "server": "8.8.8.8" + }, + { + "tag": "local", + "type": "https", + "server": "223.5.5.5" + } + ], + "rules": [ + { + "rule_set": "geosite-geolocation-cn", + "server": "local" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geoip-cn" + } + ], + "server": "google", + "client_subnet": "114.114.114.114/24" // Any China client IP address + } + ] + }, + "route": { + "default_domain_resolver": "local", + "rule_set": [ + { + "type": "remote", + "tag": "geosite-geolocation-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, + "clash_api": { + "default_mode": "Enhanced" + } + } + } + ``` + +=== ":material-router-network: Route rules" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + } + ], + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "action": "hijack-dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "action": "reject" + }, + { + "rule_set": "geosite-geolocation-cn", + "outbound": "direct" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geoip-cn" + }, + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + } + ], + "outbound": "direct" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + } + ] + } + } + ``` diff --git a/docs/manual/proxy/server.md b/docs/manual/proxy/server.md new file mode 100644 index 00000000..4d2667e0 --- /dev/null +++ b/docs/manual/proxy/server.md @@ -0,0 +1,10 @@ +--- +icon: material/server +--- + +# Server + +To use sing-box as a proxy protocol server, you pretty much only need to configure the inbound for that protocol. + +The Proxy Protocol menu below contains descriptions and configuration examples +of recommended protocols for bypassing GFW. diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..867f903b --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,1438 @@ +--- +icon: material/arrange-bring-forward +--- + +## 1.14.0 + +### Migrate inline ACME to certificate provider + +Inline ACME options in TLS are deprecated and can be replaced by certificate providers. + +Most `tls.acme` fields can be moved into the ACME certificate provider unchanged. +See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly added in sing-box 1.14.0. + +!!! info "References" + + [TLS](/configuration/shared/tls/#certificate_provider) / + [Certificate Provider](/configuration/shared/certificate-provider/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "acme": { + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: Inline" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": { + "type": "acme", + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: Shared" + + ```json + { + "certificate_providers": [ + { + "type": "acme", + "tag": "my-cert", + "domain": ["example.com"], + "email": "admin@example.com" + } + ], + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": "my-cert" + } + } + ] + } + ``` + +### Migrate address filter fields to response matching + +Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) in DNS rules are deprecated, +along with the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item. + +In sing-box 1.14.0, use the [`evaluate`](/configuration/dns/rule_action/#evaluate) action +to fetch a DNS response, then match against it explicitly with `match_response`. + +!!! info "References" + + [DNS Rule](/configuration/dns/rule/) / + [DNS Rule Action](/configuration/dns/rule_action/#evaluate) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "rules": [ + { + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": { + "rules": [ + { + "action": "evaluate", + "server": "remote" + }, + { + "match_response": true, + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + +### Migrate independent DNS cache + +The DNS cache now always keys by transport name, making `independent_cache` unnecessary. +Simply remove the field. + +!!! info "References" + + [DNS](/configuration/dns/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "independent_cache": true + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": {} + } + ``` + +### Migrate store_rdrc + +`store_rdrc` is deprecated and can be replaced by `store_dns`, +which persists the full DNS cache to the cache file. + +!!! info "References" + + [Cache File](/configuration/experimental/cache-file/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_dns": true + } + } + } + ``` + +### ip_version and query_type behavior changes in DNS rules + +In sing-box 1.14.0, the behavior of +[`ip_version`](/configuration/dns/rule/#ip_version) and +[`query_type`](/configuration/dns/rule/#query_type) in DNS rules, together with +[`query_type`](/configuration/rule-set/headless-rule/#query_type) in referenced +rule-sets, changes in two ways. + +First, these fields now take effect on every DNS rule evaluation. In earlier +versions they were evaluated only for DNS queries received from a client +(for example, from a DNS inbound or intercepted by `tun`), and were silently +ignored when a DNS rule was matched from an internal domain resolution that +did not target a specific DNS server. Such internal resolutions include: + +- The [`resolve`](/configuration/route/rule_action/#resolve) route rule + action without a `server` set. +- ICMP traffic routed to a domain destination through a `direct` outbound. +- A [WireGuard](/configuration/endpoint/wireguard/) or + [Tailscale](/configuration/endpoint/tailscale/) endpoint used as an + outbound, when resolving its own destination address. +- A [SOCKS4](/configuration/outbound/socks/) outbound, which must resolve + the destination locally because the protocol has no in-protocol domain + support. +- The [DERP](/configuration/service/derp/) `bootstrap-dns` endpoint and the + [`resolved`](/configuration/service/resolved/) service (when resolving a + hostname or an SRV target). + +Resolutions that target a specific DNS server — via +[`domain_resolver`](/configuration/shared/dial/#domain_resolver) on a dial +field, [`default_domain_resolver`](/configuration/route/#default_domain_resolver) +in route options, or an explicit `server` on a DNS rule action or the +`resolve` route rule action — do not go through DNS rule matching and are +unaffected. + +Second, setting `ip_version` or `query_type` in a DNS rule, or referencing a +rule-set containing `query_type`, is no longer compatible in the same DNS +configuration with Legacy Address Filter Fields in DNS rules, the Legacy +`strategy` DNS rule action option, or the Legacy `rule_set_ip_cidr_accept_empty` +DNS rule item. Such a configuration will be rejected at startup. To combine +these fields with address-based filtering, migrate to response matching via +the [`evaluate`](/configuration/dns/rule_action/#evaluate) action and +[`match_response`](/configuration/dns/rule/#match_response), see +[Migrate address filter fields to response matching](#migrate-address-filter-fields-to-response-matching). + +!!! info "References" + + [DNS Rule](/configuration/dns/rule/) / + [Headless Rule](/configuration/rule-set/headless-rule/) / + [Route Rule Action](/configuration/route/rule_action/#resolve) + +## 1.12.0 + +### Migrate to new DNS server formats + +DNS servers are refactored for better performance and scalability. + +!!! info "References" + + [DNS Server](/configuration/dns/server/) / + [Legacy DNS Server](/configuration/dns/server/legacy/) + +=== "Local" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "local" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "local" + } + ] + } + } + ``` + +=== "TCP" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "tcp://1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "tcp", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "UDP" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "TLS" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "tls://1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "tls", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "HTTPS" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "https://1.1.1.1/dns-query" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "https", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "QUIC" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "quic://1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "quic", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "HTTP3" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "h3://1.1.1.1/dns-query" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "h3", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "DHCP" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "dhcp://auto" + }, + { + "address": "dhcp://en0" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "dhcp", + }, + { + "type": "dhcp", + "interface": "en0" + } + ] + } + } + ``` + +=== "FakeIP" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1" + }, + { + "address": "fakeip", + "tag": "fakeip" + } + ], + "rules": [ + { + "query_type": [ + "A", + "AAAA" + ], + "server": "fakeip" + } + ], + "fakeip": { + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + }, + { + "type": "fakeip", + "tag": "fakeip", + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + ], + "rules": [ + { + "query_type": [ + "A", + "AAAA" + ], + "server": "fakeip" + } + ] + } + } + ``` + +=== "RCode" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "rcode://refused" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "rules": [ + { + "domain": [ + "example.com" + ], + // other rules + + "action": "predefined", + "rcode": "REFUSED" + } + ] + } + } + ``` + +=== "Servers with domain address" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "https://dns.google/dns-query", + "address_resolver": "google" + }, + { + "tag": "google", + "address": "1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "https", + "server": "dns.google", + "domain_resolver": "google" + }, + { + "type": "udp", + "tag": "google", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "Servers with strategy" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1", + "strategy": "ipv4_only" + }, + { + "tag": "google", + "address": "8.8.8.8", + "strategy": "prefer_ipv6" + } + ], + "rules": [ + { + "domain": "google.com", + "server": "google" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + }, + { + "type": "udp", + "tag": "google", + "server": "8.8.8.8" + } + ], + "rules": [ + { + "domain": "google.com", + "server": "google", + "strategy": "prefer_ipv6" + } + ], + "strategy": "ipv4_only" + } + } + ``` + +=== "Servers with client subnet" + + === ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1" + }, + { + "tag": "google", + "address": "8.8.8.8", + "client_subnet": "1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + }, + { + "type": "udp", + "tag": "google", + "server": "8.8.8.8" + } + ], + "rules": [ + { + "domain": "google.com", + "server": "google", + "client_subnet": "1.1.1.1" + } + ] + } + } + ``` + +### Migrate outbound DNS rule items to domain resolver + +The legacy outbound DNS rules are deprecated and can be replaced by new domain resolver options. + +!!! info "References" + + [DNS rule](/configuration/dns/rule/#outbound) / + [Dial Fields](/configuration/shared/dial/#domain_resolver) / + [Route](/configuration/route/#domain_resolver) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "servers": [ + { + "address": "local", + "tag": "local" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + } + ] + }, + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080 + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + } + ] + }, + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080, + "domain_resolver": { + "server": "local", + "rewrite_ttl": 60, + "client_subnet": "1.1.1.1" + }, + // or "domain_resolver": "local", + } + ], + + // or + + "route": { + "default_domain_resolver": { + "server": "local", + "rewrite_ttl": 60, + "client_subnet": "1.1.1.1" + } + } + } + ``` + +### Migrate outbound domain strategy option to domain resolver + +!!! info "References" + + [Dial Fields](/configuration/shared/dial/#domain_strategy) + +The `domain_strategy` option in Dial Fields has been deprecated and can be replaced with the new domain resolver option. + +Note that due to the use of Dial Fields by some of the new DNS servers introduced in sing-box 1.12, +some people mistakenly believe that `domain_strategy` is the same feature as in the legacy DNS servers. + +=== ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080, + "domain_strategy": "prefer_ipv4", + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + } + ] + }, + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080, + "domain_resolver": { + "server": "local", + "strategy": "prefer_ipv4" + } + } + ] + } + ``` + +## 1.11.0 + +### Migrate legacy special outbounds to rule actions + +Legacy special outbounds are deprecated and can be replaced by rule actions. + +!!! info "References" + + [Rule Action](/configuration/route/rule_action/) / + [Block](/configuration/outbound/block/) / + [DNS](/configuration/outbound/dns) + +=== "Block" + + === ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + ..., + + "outbound": "block" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + ..., + + "action": "reject" + } + ] + } + } + ``` + +=== "DNS" + + === ":material-card-remove: Deprecated" + + ```json + { + "inbound": [ + { + ..., + + "sniff": true + } + ], + "outbounds": [ + { + "tag": "dns", + "type": "dns" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ] + } + } + ``` + +### Migrate legacy inbound fields to rule actions + +Inbound fields are deprecated and can be replaced by rule actions. + +!!! info "References" + + [Listen Fields](/configuration/shared/listen/) / + [Rule](/configuration/route/rule/) / + [Rule Action](/configuration/route/rule_action/) / + [DNS Rule](/configuration/dns/rule/) / + [DNS Rule Action](/configuration/dns/rule_action/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "sniff": true, + "sniff_timeout": "1s", + "domain_strategy": "prefer_ipv4" + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "tag": "in" + } + ], + "route": { + "rules": [ + { + "inbound": "in", + "action": "resolve", + "strategy": "prefer_ipv4" + }, + { + "inbound": "in", + "action": "sniff", + "timeout": "1s" + } + ] + } + } + ``` + +### Migrate destination override fields to route options + +Destination override fields in direct outbound are deprecated and can be replaced by route options. + +!!! info "References" + + [Rule Action](/configuration/route/rule_action/) / + [Direct](/configuration/outbound/direct/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "direct", + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "action": "route-options", // or route + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + ``` + +### Migrate WireGuard outbound to endpoint + +WireGuard outbound is deprecated and can be replaced by endpoint. + +!!! info "References" + + [Endpoint](/configuration/endpoint/) / + [WireGuard Endpoint](/configuration/endpoint/wireguard/) / + [WireGuard Outbound](/configuration/outbound/wireguard/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "wireguard", + "tag": "wg-out", + + "server": "127.0.0.1", + "server_port": 10001, + "system_interface": true, + "gso": true, + "interface_name": "wg0", + "local_address": [ + "10.0.0.1/32" + ], + "private_key": "", + "peer_public_key": "", + "pre_shared_key": "", + "reserved": [0, 0, 0], + "mtu": 1408 + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "endpoints": [ + { + "type": "wireguard", + "tag": "wg-ep", + "system": true, + "name": "wg0", + "mtu": 1408, + "address": [ + "10.0.0.2/32" + ], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [ + "0.0.0.0/0" + ], + "persistent_keepalive_interval": 30, + "reserved": [0, 0, 0] + } + ] + } + ] + } + ``` + +## 1.10.0 + +### TUN address fields are merged + +`inet4_address` and `inet6_address` are merged into `address`, +`inet4_route_address` and `inet6_route_address` are merged into `route_address`, +`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. + +!!! info "References" + + [TUN](/configuration/inbound/tun/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ] + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ] + } + ] + } + ``` + +## 1.9.5 + +### Bundle Identifier updates in Apple platform clients + +Due to problems with our old Apple developer account, +we can only change Bundle Identifiers to re-list sing-box apps, +which means the data will not be automatically inherited. + +For iOS, you need to back up your old data yourself (if you still have access to it); +for tvOS, you need to re-import profiles from your iPhone or iPad or create it manually; +for macOS, you can migrate the data folder using the following command: + +```bash +cd ~/Library/Group\ Containers && \ + mv group.io.nekohasekai.sfa group.io.nekohasekai.sfavt +``` + +## 1.9.0 + +### `domain_suffix` behavior update + +For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects. + +sing-box 1.9.0 modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`, +the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead. + +### `process_path` format update on Windows + +The `process_path` rule of sing-box is inherited from Clash, +the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`), +but when the device has multiple disks, the HarddiskVolume serial number is not stable. + +sing-box 1.9.0 make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`), +which will disrupt the existing `process_path` use cases in Windows. + +## 1.8.0 + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "References" + + [Clash API](/configuration/experimental/clash-api/) / + [Cache File](/configuration/experimental/cache-file/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule-sets + +!!! info "References" + + [GeoIP](/configuration/route/geoip/) / + [Route](/configuration/route/) / + [Route Rule](/configuration/route/rule/) / + [DNS Rule](/configuration/dns/rule/) / + [rule-set](/configuration/rule-set/) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule-sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save rule-set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule-sets + +!!! info "References" + + [Geosite](/configuration/route/geosite/) / + [Route](/configuration/route/) / + [Route Rule](/configuration/route/rule/) / + [DNS Rule](/configuration/dns/rule/) / + [rule-set](/configuration/rule-set/) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule-sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save rule-set cache + } + } + } + ``` diff --git a/docs/migration.zh.md b/docs/migration.zh.md new file mode 100644 index 00000000..54dec47e --- /dev/null +++ b/docs/migration.zh.md @@ -0,0 +1,1427 @@ +--- +icon: material/arrange-bring-forward +--- + +## 1.14.0 + +### 迁移内联 ACME 到证书提供者 + +TLS 中的内联 ACME 选项已废弃,且可以被证书提供者替代。 + +`tls.acme` 的大多数字段都可以原样迁移到 ACME 证书提供者中。 +sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-provider/acme/) 页面。 + +!!! info "参考" + + [TLS](/zh/configuration/shared/tls/#certificate_provider) / + [证书提供者](/zh/configuration/shared/certificate-provider/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "acme": { + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: 内联" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": { + "type": "acme", + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: 共享" + + ```json + { + "certificate_providers": [ + { + "type": "acme", + "tag": "my-cert", + "domain": ["example.com"], + "email": "admin@example.com" + } + ], + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": "my-cert" + } + } + ] + } + ``` + +### 迁移地址筛选字段到响应匹配 + +旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, +旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。 + +在 sing-box 1.14.0 中,请使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作 +获取 DNS 响应,然后通过 `match_response` 显式匹配。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/) / + [DNS 规则动作](/zh/configuration/dns/rule_action/#evaluate) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "rules": [ + { + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": { + "rules": [ + { + "action": "evaluate", + "server": "remote" + }, + { + "match_response": true, + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + +### 迁移 independent DNS cache + +DNS 缓存现在始终按传输名称分离,使 `independent_cache` 不再需要。 +直接移除该字段即可。 + +!!! info "参考" + + [DNS](/zh/configuration/dns/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "independent_cache": true + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": {} + } + ``` + +### 迁移 store_rdrc + +`store_rdrc` 已废弃,且可以被 `store_dns` 替代, +后者将完整的 DNS 缓存持久化到缓存文件中。 + +!!! info "参考" + + [缓存文件](/zh/configuration/experimental/cache-file/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_dns": true + } + } + } + ``` + +### DNS 规则中的 ip_version 和 query_type 行为更改 + +在 sing-box 1.14.0 中,DNS 规则中的 +[`ip_version`](/zh/configuration/dns/rule/#ip_version) 和 +[`query_type`](/zh/configuration/dns/rule/#query_type),以及被引用规则集中的 +[`query_type`](/zh/configuration/rule-set/headless-rule/#query_type), +行为有两项更改。 + +其一,这些字段现在对每一次 DNS 规则评估都会生效。此前它们仅对来自客户端的 DNS 查询 +(例如来自 DNS 入站或被 `tun` 截获的查询)生效,当 DNS 规则被未指定具体 DNS 服务器的 +内部域名解析匹配时,会被静默忽略。此类内部解析包括: + +- 未设置 `server` 的 [`resolve`](/zh/configuration/route/rule_action/#resolve) 路由规则动作。 +- 通过 `direct` 出站路由到域名目标的 ICMP 流量。 +- 作为出站使用的 [WireGuard](/zh/configuration/endpoint/wireguard/) 或 + [Tailscale](/zh/configuration/endpoint/tailscale/) 端点在解析自身目标地址时。 +- [SOCKS4](/zh/configuration/outbound/socks/) 出站,因为协议本身不支持域名, + 必须在本地解析目标。 +- [DERP](/zh/configuration/service/derp/) 的 `bootstrap-dns` 端点,以及 + [`resolved`](/zh/configuration/service/resolved/) 服务在解析主机名或 SRV 目标时。 + +通过拨号字段中的 +[`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver)、 +路由选项中的 [`default_domain_resolver`](/zh/configuration/route/#default_domain_resolver), +或 DNS 规则动作与 `resolve` 路由规则动作上显式的 `server` 指定具体 DNS 服务器的 +解析,不会经过 DNS 规则匹配,不受此次更改影响。 + +其二,设置了 `ip_version` 或 `query_type` 的 DNS 规则,或引用了包含 `query_type` 的 +规则集的 DNS 规则,在同一 DNS 配置中不再能与旧版地址筛选字段 (DNS 规则)、旧版 +DNS 规则动作 `strategy` 选项,或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。 +此类配置将在启动时被拒绝。如需将这些字段与基于地址的筛选组合,请通过 +[`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作和 +[`match_response`](/zh/configuration/dns/rule/#match_response) 迁移到响应匹配, +参阅 [迁移地址筛选字段到响应匹配](#迁移地址筛选字段到响应匹配)。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/) / + [Headless 规则](/zh/configuration/rule-set/headless-rule/) / + [路由规则动作](/zh/configuration/route/rule_action/#resolve) + +## 1.12.0 + +### 迁移到新的 DNS 服务器格式 + +DNS 服务器已经重构。 + +!!! info "引用" + + [DNS 服务器](/zh/configuration/dns/server/) / + [旧 DNS 服务器](/zh/configuration/dns/server/legacy/) + +=== "Local" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "local" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "local" + } + ] + } + } + ``` + +=== "TCP" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "tcp://1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "tcp", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "UDP" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "TLS" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "tls://1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "tls", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "HTTPS" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "https://1.1.1.1/dns-query" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "https", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "QUIC" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "quic://1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "quic", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "HTTP3" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "h3://1.1.1.1/dns-query" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "h3", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "DHCP" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "dhcp://auto" + }, + { + "address": "dhcp://en0" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "dhcp", + }, + { + "type": "dhcp", + "interface": "en0" + } + ] + } + } + ``` + +=== "FakeIP" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1" + }, + { + "address": "fakeip", + "tag": "fakeip" + } + ], + "rules": [ + { + "query_type": [ + "A", + "AAAA" + ], + "server": "fakeip" + } + ], + "fakeip": { + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + }, + { + "type": "fakeip", + "tag": "fakeip", + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + } + ], + "rules": [ + { + "query_type": [ + "A", + "AAAA" + ], + "server": "fakeip" + } + ] + } + } + ``` + +=== "RCode" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "rcode://refused" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "rules": [ + { + "domain": [ + "example.com" + ], + // 其它规则 + + "action": "predefined", + "rcode": "REFUSED" + } + ] + } + } + ``` + +=== "带有域名地址的服务器" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "https://dns.google/dns-query", + "address_resolver": "google" + }, + { + "tag": "google", + "address": "1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "https", + "server": "dns.google", + "domain_resolver": "google" + }, + { + "type": "udp", + "tag": "google", + "server": "1.1.1.1" + } + ] + } + } + ``` + +=== "带有域策略的服务器" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1", + "strategy": "ipv4_only" + }, + { + "tag": "google", + "address": "8.8.8.8", + "strategy": "prefer_ipv6" + } + ], + "rules": [ + { + "domain": "google.com", + "server": "google" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + }, + { + "type": "udp", + "tag": "google", + "server": "8.8.8.8" + } + ], + "rules": [ + { + "domain": "google.com", + "server": "google", + "strategy": "prefer_ipv6" + } + ], + "strategy": "ipv4_only" + } + } + ``` + +=== "带有客户端子网的服务器" + + === ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "servers": [ + { + "address": "1.1.1.1" + }, + { + "tag": "google", + "address": "8.8.8.8", + "client_subnet": "1.1.1.1" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "udp", + "server": "1.1.1.1" + }, + { + "type": "udp", + "tag": "google", + "server": "8.8.8.8" + } + ], + "rules": [ + { + "domain": "google.com", + "server": "google", + "client_subnet": "1.1.1.1" + } + ] + } + } + ``` + +### 迁移 outbound DNS 规则项到域解析选项 + +旧的 `outbound` DNS 规则已废弃,且可新的域解析选项代替。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/#outbound) / + [拨号字段](/zh/configuration/shared/dial/#domain_resolver) / + [路由](/zh/configuration/route/#default_domain_resolver) + +=== ":material-card-remove: 废弃的" + + ```json + { + "dns": { + "servers": [ + { + "address": "local", + "tag": "local" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + } + ] + }, + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080 + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + } + ] + }, + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080, + "domain_resolver": { + "server": "local", + "rewrite_ttl": 60, + "client_subnet": "1.1.1.1" + }, + // 或 "domain_resolver": "local", + } + ], + + // 或 + + "route": { + "default_domain_resolver": { + "server": "local", + "rewrite_ttl": 60, + "client_subnet": "1.1.1.1" + } + } + } + ``` + +### 迁移出站域名策略选项到域名解析器 + +拨号字段中的 `domain_strategy` 选项已被弃用,可以用新的域名解析器选项替代。 + +请注意,由于 sing-box 1.12 中引入的一些新 DNS 服务器使用了拨号字段,一些人错误地认为 `domain_strategy` 与旧 DNS 服务器中的功能相同。 + +!!! info "参考" + + [拨号字段](/zh/configuration/shared/dial/#domain_strategy) + +=== ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080, + "domain_strategy": "prefer_ipv4", + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + } + ] + }, + "outbounds": [ + { + "type": "socks", + "server": "example.org", + "server_port": 2080, + "domain_resolver": { + "server": "local", + "strategy": "prefer_ipv4" + } + } + ] + } + ``` + +## 1.11.0 + +### 迁移旧的特殊出站到规则动作 + +旧的特殊出站已被弃用,且可以被规则动作替代。 + +!!! info "参考" + + [规则动作](/zh/configuration/route/rule_action/) / + [Block](/zh/configuration/outbound/block/) / + [DNS](/zh/configuration/outbound/dns) + +=== "Block" + + === ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + ..., + + "outbound": "block" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + ..., + + "action": "reject" + } + ] + } + } + ``` + +=== "DNS" + + === ":material-card-remove: 弃用的" + + ```json + { + "inbound": [ + { + ..., + + "sniff": true + } + ], + "outbounds": [ + { + "tag": "dns", + "type": "dns" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ] + } + } + ``` + +### 迁移旧的入站字段到规则动作 + +入站选项已被弃用,且可以被规则动作替代。 + +!!! info "参考" + + [监听字段](/zh/configuration/shared/listen/) / + [规则](/zh/configuration/route/rule/) / + [规则动作](/zh/configuration/route/rule_action/) / + [DNS 规则](/zh/configuration/dns/rule/) / + [DNS 规则动作](/zh/configuration/dns/rule_action/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "sniff": true, + "sniff_timeout": "1s", + "domain_strategy": "prefer_ipv4" + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "tag": "in" + } + ], + "route": { + "rules": [ + { + "inbound": "in", + "action": "resolve", + "strategy": "prefer_ipv4" + }, + { + "inbound": "in", + "action": "sniff", + "timeout": "1s" + } + ] + } + } + ``` + +### 迁移 direct 出站中的目标地址覆盖字段到路由字段 + +direct 出站中的目标地址覆盖字段已废弃,且可以被路由字段替代。 + +!!! info "参考" + + [Rule Action](/zh/configuration/route/rule_action/) / + [Direct](/zh/configuration/outbound/direct/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "direct", + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "action": "route-options", // 或 route + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + } + ``` + +### 迁移 WireGuard 出站到端点 + +WireGuard 出站已被弃用,且可以被端点替代。 + +!!! info "参考" + + [端点](/zh/configuration/endpoint/) / + [WireGuard 端点](/zh/configuration/endpoint/wireguard/) / + [WireGuard 出站](/zh/configuration/outbound/wireguard/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "wireguard", + "tag": "wg-out", + + "server": "127.0.0.1", + "server_port": 10001, + "system_interface": true, + "gso": true, + "interface_name": "wg0", + "local_address": [ + "10.0.0.1/32" + ], + "private_key": "", + "peer_public_key": "", + "pre_shared_key": "", + "reserved": [0, 0, 0], + "mtu": 1408 + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "endpoints": [ + { + "type": "wireguard", + "tag": "wg-ep", + "system": true, + "name": "wg0", + "mtu": 1408, + "address": [ + "10.0.0.2/32" + ], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [ + "0.0.0.0/0" + ], + "persistent_keepalive_interval": 30, + "reserved": [0, 0, 0] + } + ] + } + ] + } + ``` + +## 1.10.0 + +### TUN 地址字段已合并 + +`inet4_address` 和 `inet6_address` 已合并为 `address`, +`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, +`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 + +!!! info "参考" + + [TUN](/zh/configuration/inbound/tun/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ] + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ] + } + ] + } + ``` + +## 1.9.5 + +### Apple 平台客户端的 Bundle Identifier 更新 + +由于我们旧的苹果开发者账户存在问题,我们只能通过更新 Bundle Identifiers +来重新上架 sing-box 应用, 这意味着数据不会自动继承。 + +对于 iOS,您需要自行备份旧的数据(如果您仍然可以访问); +对于 Apple tvOS,您需要从 iPhone 或 iPad 重新导入配置或者手动创建; +对于 macOS,您可以使用以下命令迁移数据文件夹: + +```bash +cd ~/Library/Group\ Containers && \ + mv group.io.nekohasekai.sfa group.io.nekohasekai.sfavt +``` + +## 1.9.0 + +### `domain_suffix` 行为更新 + +由于历史原因,sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 + +sing-box 1.9.0 修改了 `domain_suffix` 的行为:如果规则值以 `.` 为前缀则行为不变,否则改为匹配 `(domain|.+\.domain)`。 + +### 对 Windows 上 `process_path` 格式的更新 + +sing-box 的 `process_path` 规则继承自Clash, +原始代码使用本地系统的路径格式(例如 `\Device\HarddiskVolume1\folder\program.exe`), +但是当设备有多个硬盘时,该 HarddiskVolume 系列号并不稳定。 + +sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\folder\program.exe`), +这将会破坏现有的 Windows `process_path` 用例。 + +## 1.8.0 + +### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项 + +!!! info "参考" + + [Clash API](/zh/configuration/experimental/clash-api/) / + [Cache File](/zh/configuration/experimental/cache-file/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // 默认值 + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // 默认值 + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: 迁移 GeoIP 到规则集 + +!!! info "参考" + + [GeoIP](/zh/configuration/route/geoip/) / + [路由](/zh/configuration/route/) / + [路由规则](/zh/configuration/route/rule/) / + [DNS 规则](/zh/configuration/dns/rule/) / + [规则集](/zh/configuration/rule-set/) + +!!! tip + + `sing-box geoip` 命令可以帮助您将自定义 GeoIP 转换为规则集。 + +=== ":material-card-remove: 弃用的" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save rule-set cache + } + } + } + ``` + +### :material-checkbox-intermediate: 迁移 Geosite 到规则集 + +!!! info "参考" + + [Geosite](/zh/configuration/route/geosite/) / + [路由](/zh/configuration/route/) / + [路由规则](/zh/configuration/route/rule/) / + [DNS 规则](/zh/configuration/dns/rule/) / + [规则集](/zh/configuration/rule-set/) + +!!! tip + + `sing-box geosite` 命令可以帮助您将自定义 Geosite 转换为规则集。 + +=== ":material-card-remove: 弃用的" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save rule-set cache + } + } + } + ``` diff --git a/docs/sponsors.md b/docs/sponsors.md new file mode 100644 index 00000000..33898928 --- /dev/null +++ b/docs/sponsors.md @@ -0,0 +1,32 @@ +--- +icon: material/hand-coin +--- + +# Sponsors + +Do you or your friends use sing-box? + +You can help keep the project bug-free and feature rich by sponsoring +the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohasekai). + +![](https://nekohasekai.github.io/sponsor-images/sponsors.svg) + +## Commercial Sponsors + +> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents. + +[![](https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png)](https://go.warp.dev/sing-box) + +## Special Sponsors + +> Viral Tech, Inc. + +Helping us re-list sing-box apps on the Apple Store. + +--- + +> [JetBrains](https://www.jetbrains.com) + +Free license for the amazing IDEs. + +[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com) diff --git a/docs/support.md b/docs/support.md new file mode 100644 index 00000000..8443ffaa --- /dev/null +++ b/docs/support.md @@ -0,0 +1,12 @@ +--- +icon: material/forum +--- + +# Support + +| Channel | Link | +| :---------------------------- | :------------------------------------------ | +| GitHub Issues | https://github.com/SagerNet/sing-box/issues | +| Telegram notification channel | https://t.me/yapnc | +| Telegram user group | https://t.me/yapug | +| Email | contact@sagernet.org | diff --git a/docs/support.zh.md b/docs/support.zh.md new file mode 100644 index 00000000..ff1978ee --- /dev/null +++ b/docs/support.zh.md @@ -0,0 +1,13 @@ +--- +icon: material/forum +--- + +# 支持 + +| 通道 | 链接 | +| :---------------- | :------------------------------------------ | +| GitHub Issues | https://github.com/SagerNet/sing-box/issues | +| Telegram 通知频道 | https://t.me/yapnc | +| Telegram 用户组 | https://t.me/yapug | +| 邮件 | contact@sagernet.org | + diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go new file mode 100644 index 00000000..3198fc6a --- /dev/null +++ b/experimental/cachefile/cache.go @@ -0,0 +1,417 @@ +package cachefile + +import ( + "context" + "errors" + "net/netip" + "os" + "strings" + "sync" + "time" + + "github.com/sagernet/bbolt" + bboltErrors "github.com/sagernet/bbolt/errors" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service/filemanager" +) + +var ( + bucketSelected = []byte("selected") + bucketExpand = []byte("group_expand") + bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") + + bucketNameList = []string{ + string(bucketSelected), + string(bucketExpand), + string(bucketMode), + string(bucketRuleSet), + string(bucketRDRC), + string(bucketDNSCache), + } + + cacheIDDefault = []byte("default") +) + +var _ adapter.CacheFile = (*CacheFile)(nil) + +type CacheFile struct { + ctx context.Context + logger logger.Logger + path string + cacheID []byte + storeFakeIP bool + storeRDRC bool + storeDNS bool + disableExpire bool + rdrcTimeout time.Duration + optimisticTimeout time.Duration + DB *bbolt.DB + resetAccess sync.Mutex + saveMetadataTimer *time.Timer + saveFakeIPAccess sync.RWMutex + saveDomain map[netip.Addr]string + saveAddress4 map[string]netip.Addr + saveAddress6 map[string]netip.Addr + saveRDRCAccess sync.RWMutex + saveRDRC map[saveCacheKey]bool + saveDNSCacheAccess sync.RWMutex + saveDNSCache map[saveCacheKey]saveDNSCacheEntry +} + +type saveCacheKey struct { + TransportName string + QuestionName string + QType uint16 +} + +type saveDNSCacheEntry struct { + rawMessage []byte + expireAt time.Time + sequence uint64 + saving bool +} + +func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile { + var path string + if options.Path != "" { + path = options.Path + } else { + path = "cache.db" + } + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + if options.StoreRDRC { + deprecated.Report(ctx, deprecated.OptionStoreRDRC) + } + var rdrcTimeout time.Duration + if options.StoreRDRC { + if options.RDRCTimeout > 0 { + rdrcTimeout = time.Duration(options.RDRCTimeout) + } else { + rdrcTimeout = 7 * 24 * time.Hour + } + } + return &CacheFile{ + ctx: ctx, + logger: logger, + path: filemanager.BasePath(ctx, path), + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + storeRDRC: options.StoreRDRC, + storeDNS: options.StoreDNS, + rdrcTimeout: rdrcTimeout, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + saveRDRC: make(map[saveCacheKey]bool), + saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry), + } +} + +func (c *CacheFile) Name() string { + return "cache-file" +} + +func (c *CacheFile) Dependencies() []string { + return nil +} + +func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) { + c.optimisticTimeout = timeout +} + +func (c *CacheFile) SetDisableExpire(disableExpire bool) { + c.disableExpire = disableExpire +} + +func (c *CacheFile) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateInitialize: + return c.start() + case adapter.StartStateStart: + c.startCacheCleanup() + } + return nil +} + +func (c *CacheFile) startCacheCleanup() { + if c.storeDNS { + c.clearRDRC() + c.cleanupDNSCache() + interval := c.optimisticTimeout / 2 + if interval <= 0 { + interval = time.Hour + } + go c.loopCacheCleanup(interval, c.cleanupDNSCache) + } else if c.storeRDRC { + c.cleanupRDRC() + interval := c.rdrcTimeout / 2 + if interval <= 0 { + interval = time.Hour + } + go c.loopCacheCleanup(interval, c.cleanupRDRC) + } +} + +func (c *CacheFile) start() error { + const fileMode = 0o666 + options := bbolt.Options{Timeout: time.Second} + var ( + db *bbolt.DB + err error + ) + for i := 0; i < 10; i++ { + db, err = bbolt.Open(c.path, fileMode, &options) + if err == nil { + break + } + if errors.Is(err, bboltErrors.ErrTimeout) { + continue + } + if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { + rmErr := os.Remove(c.path) + if rmErr != nil { + return err + } + } + time.Sleep(100 * time.Millisecond) + } + if err != nil { + return err + } + err = filemanager.Chown(c.ctx, c.path) + if err != nil { + db.Close() + return E.Cause(err, "platform chown") + } + err = db.Batch(func(tx *bbolt.Tx) error { + return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { + if name[0] == 0 { + return b.ForEachBucket(func(k []byte) error { + bucketName := string(k) + if !(common.Contains(bucketNameList, bucketName)) { + _ = b.DeleteBucket(name) + } + return nil + }) + } else { + bucketName := string(name) + if !(common.Contains(bucketNameList, bucketName) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) { + _ = tx.DeleteBucket(name) + } + } + return nil + }) + }) + if err != nil { + db.Close() + return err + } + c.DB = db + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) { + defer func() { + if r := recover(); r != nil { + c.resetDB() + err = E.New("database corrupted: ", r) + } + }() + return c.DB.View(fn) +} + +func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) { + defer func() { + if r := recover(); r != nil { + c.resetDB() + err = E.New("database corrupted: ", r) + } + }() + return c.DB.Batch(fn) +} + +func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) { + defer func() { + if r := recover(); r != nil { + c.resetDB() + err = E.New("database corrupted: ", r) + } + }() + return c.DB.Update(fn) +} + +func (c *CacheFile) resetDB() { + c.resetAccess.Lock() + defer c.resetAccess.Unlock() + c.DB.Close() + os.Remove(c.path) + db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second}) + if err == nil { + _ = filemanager.Chown(c.ctx, c.path) + c.DB = db + } +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP +} + +func (c *CacheFile) LoadMode() string { + var mode string + c.view(func(t *bbolt.Tx) error { + bucket := t.Bucket(bucketMode) + if bucket == nil { + return nil + } + var modeBytes []byte + if len(c.cacheID) > 0 { + modeBytes = bucket.Get(c.cacheID) + } else { + modeBytes = bucket.Get(cacheIDDefault) + } + mode = string(modeBytes) + return nil + }) + return mode +} + +func (c *CacheFile) StoreMode(mode string) error { + return c.batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketMode) + if err != nil { + return err + } + if len(c.cacheID) > 0 { + return bucket.Put(c.cacheID, []byte(mode)) + } else { + return bucket.Put(cacheIDDefault, []byte(mode)) + } + }) +} + +func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket { + if c.cacheID == nil { + return t.Bucket(key) + } + bucket := t.Bucket(c.cacheID) + if bucket == nil { + return nil + } + return bucket.Bucket(key) +} + +func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) { + if c.cacheID == nil { + return t.CreateBucketIfNotExists(key) + } + bucket, err := t.CreateBucketIfNotExists(c.cacheID) + if bucket == nil { + return nil, err + } + return bucket.CreateBucketIfNotExists(key) +} + +func (c *CacheFile) LoadSelected(group string) string { + var selected string + c.view(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketSelected) + if bucket == nil { + return nil + } + selectedBytes := bucket.Get([]byte(group)) + if len(selectedBytes) > 0 { + selected = string(selectedBytes) + } + return nil + }) + return selected +} + +func (c *CacheFile) StoreSelected(group, selected string) error { + return c.batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketSelected) + if err != nil { + return err + } + return bucket.Put([]byte(group), []byte(selected)) + }) +} + +func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) { + c.view(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketExpand) + if bucket == nil { + return nil + } + expandBytes := bucket.Get([]byte(group)) + if len(expandBytes) == 1 { + isExpand = expandBytes[0] == 1 + loaded = true + } + return nil + }) + return +} + +func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { + return c.batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketExpand) + if err != nil { + return err + } + if isExpand { + return bucket.Put([]byte(group), []byte{1}) + } else { + return bucket.Put([]byte(group), []byte{0}) + } + }) +} + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary { + var savedSet adapter.SavedBinary + err := c.view(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error { + return c.batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/dns_cache.go b/experimental/cachefile/dns_cache.go new file mode 100644 index 00000000..914c7e5a --- /dev/null +++ b/experimental/cachefile/dns_cache.go @@ -0,0 +1,299 @@ +package cachefile + +import ( + "encoding/binary" + "time" + + "github.com/sagernet/bbolt" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/logger" +) + +var bucketDNSCache = []byte("dns_cache") + +func (c *CacheFile) StoreDNS() bool { + return c.storeDNS +} + +func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) { + c.saveDNSCacheAccess.RLock() + entry, cached := c.saveDNSCache[saveCacheKey{transportName, qName, qType}] + c.saveDNSCacheAccess.RUnlock() + if cached { + return entry.rawMessage, entry.expireAt, true + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + err := c.view(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketDNSCache) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + content := bucket.Get(key) + if len(content) < 8 { + return nil + } + expireAt = time.Unix(int64(binary.BigEndian.Uint64(content[:8])), 0) + rawMessage = make([]byte, len(content)-8) + copy(rawMessage, content[8:]) + loaded = true + return nil + }) + if err != nil { + return nil, time.Time{}, false + } + return +} + +func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error { + return c.batch(func(tx *bbolt.Tx) error { + bucket, err := c.createBucket(tx, bucketDNSCache) + if err != nil { + return err + } + bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName)) + if err != nil { + return err + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + value := buf.Get(8 + len(rawMessage)) + defer buf.Put(value) + binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix())) + copy(value[8:], rawMessage) + return bucket.Put(key, value) + }) +} + +func (c *CacheFile) SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) { + saveKey := saveCacheKey{transportName, qName, qType} + if !c.queueDNSCacheSave(saveKey, rawMessage, expireAt) { + return + } + go c.flushPendingDNSCache(saveKey, logger) +} + +func (c *CacheFile) queueDNSCacheSave(saveKey saveCacheKey, rawMessage []byte, expireAt time.Time) bool { + c.saveDNSCacheAccess.Lock() + defer c.saveDNSCacheAccess.Unlock() + entry := c.saveDNSCache[saveKey] + entry.rawMessage = append([]byte(nil), rawMessage...) + entry.expireAt = expireAt + entry.sequence++ + startFlush := !entry.saving + entry.saving = true + c.saveDNSCache[saveKey] = entry + return startFlush +} + +func (c *CacheFile) flushPendingDNSCache(saveKey saveCacheKey, logger logger.Logger) { + c.flushPendingDNSCacheWith(saveKey, logger, func(entry saveDNSCacheEntry) error { + return c.SaveDNSCache(saveKey.TransportName, saveKey.QuestionName, saveKey.QType, entry.rawMessage, entry.expireAt) + }) +} + +func (c *CacheFile) flushPendingDNSCacheWith(saveKey saveCacheKey, logger logger.Logger, save func(saveDNSCacheEntry) error) { + for { + c.saveDNSCacheAccess.RLock() + entry, loaded := c.saveDNSCache[saveKey] + c.saveDNSCacheAccess.RUnlock() + if !loaded { + return + } + err := save(entry) + if err != nil { + logger.Warn("save DNS cache: ", err) + } + c.saveDNSCacheAccess.Lock() + currentEntry, loaded := c.saveDNSCache[saveKey] + if !loaded { + c.saveDNSCacheAccess.Unlock() + return + } + if currentEntry.sequence != entry.sequence { + c.saveDNSCacheAccess.Unlock() + continue + } + delete(c.saveDNSCache, saveKey) + c.saveDNSCacheAccess.Unlock() + return + } +} + +func (c *CacheFile) ClearDNSCache() error { + c.saveDNSCacheAccess.Lock() + clear(c.saveDNSCache) + c.saveDNSCacheAccess.Unlock() + return c.batch(func(tx *bbolt.Tx) error { + if c.cacheID == nil { + bucket := tx.Bucket(bucketDNSCache) + if bucket == nil { + return nil + } + return tx.DeleteBucket(bucketDNSCache) + } + bucket := tx.Bucket(c.cacheID) + if bucket == nil || bucket.Bucket(bucketDNSCache) == nil { + return nil + } + return bucket.DeleteBucket(bucketDNSCache) + }) +} + +func (c *CacheFile) loopCacheCleanup(interval time.Duration, cleanupFunc func()) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-c.ctx.Done(): + return + case <-ticker.C: + cleanupFunc() + } + } +} + +func (c *CacheFile) cleanupDNSCache() { + now := time.Now() + err := c.batch(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketDNSCache) + if bucket == nil { + return nil + } + var emptyTransports [][]byte + err := bucket.ForEachBucket(func(transportName []byte) error { + transportBucket := bucket.Bucket(transportName) + if transportBucket == nil { + return nil + } + var expiredKeys [][]byte + err := transportBucket.ForEach(func(key, value []byte) error { + if len(value) < 8 { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + return nil + } + if c.disableExpire { + return nil + } + expireAt := time.Unix(int64(binary.BigEndian.Uint64(value[:8])), 0) + if now.After(expireAt.Add(c.optimisticTimeout)) { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + } + return nil + }) + if err != nil { + return err + } + for _, key := range expiredKeys { + err = transportBucket.Delete(key) + if err != nil { + return err + } + } + first, _ := transportBucket.Cursor().First() + if first == nil { + emptyTransports = append(emptyTransports, append([]byte(nil), transportName...)) + } + return nil + }) + if err != nil { + return err + } + for _, name := range emptyTransports { + err = bucket.DeleteBucket(name) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + c.logger.Warn("cleanup DNS cache: ", err) + } +} + +func (c *CacheFile) clearRDRC() { + c.saveRDRCAccess.Lock() + clear(c.saveRDRC) + c.saveRDRCAccess.Unlock() + err := c.batch(func(tx *bbolt.Tx) error { + if c.cacheID == nil { + if tx.Bucket(bucketRDRC) == nil { + return nil + } + return tx.DeleteBucket(bucketRDRC) + } + bucket := tx.Bucket(c.cacheID) + if bucket == nil || bucket.Bucket(bucketRDRC) == nil { + return nil + } + return bucket.DeleteBucket(bucketRDRC) + }) + if err != nil { + c.logger.Warn("clear RDRC: ", err) + } +} + +func (c *CacheFile) cleanupRDRC() { + now := time.Now() + err := c.batch(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + var emptyTransports [][]byte + err := bucket.ForEachBucket(func(transportName []byte) error { + transportBucket := bucket.Bucket(transportName) + if transportBucket == nil { + return nil + } + var expiredKeys [][]byte + err := transportBucket.ForEach(func(key, value []byte) error { + if len(value) < 8 { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + return nil + } + expiresAt := time.Unix(int64(binary.BigEndian.Uint64(value)), 0) + if now.After(expiresAt) { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + } + return nil + }) + if err != nil { + return err + } + for _, key := range expiredKeys { + err = transportBucket.Delete(key) + if err != nil { + return err + } + } + first, _ := transportBucket.Cursor().First() + if first == nil { + emptyTransports = append(emptyTransports, append([]byte(nil), transportName...)) + } + return nil + }) + if err != nil { + return err + } + for _, name := range emptyTransports { + err = bucket.DeleteBucket(name) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + c.logger.Warn("cleanup RDRC: ", err) + } +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go new file mode 100644 index 00000000..7a4bd384 --- /dev/null +++ b/experimental/cachefile/fakeip.go @@ -0,0 +1,194 @@ +package cachefile + +import ( + "net/netip" + "os" + "time" + + "github.com/sagernet/bbolt" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" +) + +const fakeipBucketPrefix = "fakeip_" + +var ( + bucketFakeIP = []byte(fakeipBucketPrefix + "address") + bucketFakeIPDomain4 = []byte(fakeipBucketPrefix + "domain4") + bucketFakeIPDomain6 = []byte(fakeipBucketPrefix + "domain6") + keyMetadata = []byte(fakeipBucketPrefix + "metadata") +) + +func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { + var metadata adapter.FakeIPMetadata + err := c.batch(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(bucketFakeIP) + if bucket == nil { + return os.ErrNotExist + } + metadataBinary := bucket.Get(keyMetadata) + if len(metadataBinary) == 0 { + return os.ErrInvalid + } + err := bucket.Delete(keyMetadata) + if err != nil { + return err + } + return metadata.UnmarshalBinary(metadataBinary) + }) + if err != nil { + return nil + } + return &metadata +} + +func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { + return c.batch(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) + if err != nil { + return err + } + metadataBinary, err := metadata.MarshalBinary() + if err != nil { + return err + } + return bucket.Put(keyMetadata, metadataBinary) + }) +} + +func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) { + if c.saveMetadataTimer == nil { + c.saveMetadataTimer = time.AfterFunc(C.FakeIPMetadataSaveInterval, func() { + _ = c.FakeIPSaveMetadata(metadata) + }) + } else { + c.saveMetadataTimer.Reset(C.FakeIPMetadataSaveInterval) + } +} + +func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { + return c.batch(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) + if err != nil { + return err + } + oldDomain := bucket.Get(address.AsSlice()) + err = bucket.Put(address.AsSlice(), []byte(domain)) + if err != nil { + return err + } + if address.Is4() { + bucket, err = tx.CreateBucketIfNotExists(bucketFakeIPDomain4) + } else { + bucket, err = tx.CreateBucketIfNotExists(bucketFakeIPDomain6) + } + if err != nil { + return err + } + if oldDomain != nil { + if err := bucket.Delete(oldDomain); err != nil { + return err + } + } + return bucket.Put([]byte(domain), address.AsSlice()) + }) +} + +func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) { + c.saveFakeIPAccess.Lock() + if oldDomain, loaded := c.saveDomain[address]; loaded { + if address.Is4() { + delete(c.saveAddress4, oldDomain) + } else { + delete(c.saveAddress6, oldDomain) + } + } + c.saveDomain[address] = domain + if address.Is4() { + c.saveAddress4[domain] = address + } else { + c.saveAddress6[domain] = address + } + c.saveFakeIPAccess.Unlock() + go func() { + err := c.FakeIPStore(address, domain) + if err != nil { + logger.Warn("save FakeIP cache: ", err) + } + c.saveFakeIPAccess.Lock() + delete(c.saveDomain, address) + if address.Is4() { + delete(c.saveAddress4, domain) + } else { + delete(c.saveAddress6, domain) + } + c.saveFakeIPAccess.Unlock() + }() +} + +func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { + c.saveFakeIPAccess.RLock() + cachedDomain, cached := c.saveDomain[address] + c.saveFakeIPAccess.RUnlock() + if cached { + return cachedDomain, true + } + var domain string + _ = c.view(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(bucketFakeIP) + if bucket == nil { + return nil + } + domain = string(bucket.Get(address.AsSlice())) + return nil + }) + return domain, domain != "" +} + +func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) { + var ( + cachedAddress netip.Addr + cached bool + ) + c.saveFakeIPAccess.RLock() + if !isIPv6 { + cachedAddress, cached = c.saveAddress4[domain] + } else { + cachedAddress, cached = c.saveAddress6[domain] + } + c.saveFakeIPAccess.RUnlock() + if cached { + return cachedAddress, true + } + var address netip.Addr + _ = c.view(func(tx *bbolt.Tx) error { + var bucket *bbolt.Bucket + if isIPv6 { + bucket = tx.Bucket(bucketFakeIPDomain6) + } else { + bucket = tx.Bucket(bucketFakeIPDomain4) + } + if bucket == nil { + return nil + } + address = M.AddrFromIP(bucket.Get([]byte(domain))) + return nil + }) + return address, address.IsValid() +} + +func (c *CacheFile) FakeIPReset() error { + return c.batch(func(tx *bbolt.Tx) error { + err := tx.DeleteBucket(bucketFakeIP) + if err != nil { + return err + } + err = tx.DeleteBucket(bucketFakeIPDomain4) + if err != nil { + return err + } + return tx.DeleteBucket(bucketFakeIPDomain6) + }) +} diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go new file mode 100644 index 00000000..26056f57 --- /dev/null +++ b/experimental/cachefile/rdrc.go @@ -0,0 +1,109 @@ +package cachefile + +import ( + "encoding/binary" + "time" + + "github.com/sagernet/bbolt" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/logger" +) + +var bucketRDRC = []byte("rdrc2") + +func (c *CacheFile) StoreRDRC() bool { + return c.storeRDRC +} + +func (c *CacheFile) RDRCTimeout() time.Duration { + return c.rdrcTimeout +} + +func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) { + c.saveRDRCAccess.RLock() + rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}] + c.saveRDRCAccess.RUnlock() + if cached { + return + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + var deleteCache bool + err := c.view(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + content := bucket.Get(key) + if content == nil { + return nil + } + expiresAt := time.Unix(int64(binary.BigEndian.Uint64(content)), 0) + if time.Now().After(expiresAt) { + deleteCache = true + return nil + } + rejected = true + return nil + }) + if err != nil { + return + } + if deleteCache { + c.update(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + return bucket.Delete(key) + }) + } + return +} + +func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error { + return c.batch(func(tx *bbolt.Tx) error { + bucket, err := c.createBucket(tx, bucketRDRC) + if err != nil { + return err + } + bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName)) + if err != nil { + return err + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + expiresAt := buf.Get(8) + defer buf.Put(expiresAt) + binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) + return bucket.Put(key, expiresAt) + }) +} + +func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) { + saveKey := saveCacheKey{transportName, qName, qType} + c.saveRDRCAccess.Lock() + c.saveRDRC[saveKey] = true + c.saveRDRCAccess.Unlock() + go func() { + err := c.SaveRDRC(transportName, qName, qType) + if err != nil { + logger.Warn("save RDRC: ", err) + } + c.saveRDRCAccess.Lock() + delete(c.saveRDRC, saveKey) + c.saveRDRCAccess.Unlock() + }() +} diff --git a/experimental/clashapi.go b/experimental/clashapi.go new file mode 100644 index 00000000..4ad07c8b --- /dev/null +++ b/experimental/clashapi.go @@ -0,0 +1,81 @@ +package experimental + +import ( + "context" + "os" + "sort" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" +) + +type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) + +var clashServerConstructor ClashServerConstructor + +func RegisterClashServerConstructor(constructor ClashServerConstructor) { + clashServerConstructor = constructor +} + +func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { + if clashServerConstructor == nil { + return nil, os.ErrInvalid + } + return clashServerConstructor(ctx, logFactory, options) +} + +func CalculateClashModeList(options option.Options) []string { + var clashModes []string + clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashModes = common.FilterNotDefault(common.Uniq(clashModes)) + predefinedOrder := []string{ + "Rule", "Global", "Direct", + } + var newClashModes []string + for _, mode := range clashModes { + if !common.Contains(predefinedOrder, mode) { + newClashModes = append(newClashModes, mode) + } + } + sort.Strings(newClashModes) + for _, mode := range predefinedOrder { + if common.Contains(clashModes, mode) { + newClashModes = append(newClashModes, mode) + } + } + return newClashModes +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) + } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) + } + } + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) + } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) + } + } + return clashMode +} diff --git a/experimental/clashapi/api_meta.go b/experimental/clashapi/api_meta.go new file mode 100644 index 00000000..8b31d8a4 --- /dev/null +++ b/experimental/clashapi/api_meta.go @@ -0,0 +1,96 @@ +package clashapi + +import ( + "bytes" + "context" + "net" + "net/http" + "runtime/debug" + "time" + + "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" +) + +// API created by Clash.Meta + +func (s *Server) setupMetaAPI(r chi.Router) { + if s.logDebug { + r := chi.NewRouter() + r.Put("/gc", func(w http.ResponseWriter, r *http.Request) { + debug.FreeOSMemory() + }) + r.Mount("/", middleware.Profiler()) + } + r.Get("/memory", memory(s.ctx, s.trafficManager)) + r.Mount("/group", groupRouter(s)) + r.Mount("/upgrade", upgradeRouter(s)) +} + +type Memory struct { + Inuse uint64 `json:"inuse"` + OSLimit uint64 `json:"oslimit"` // maybe we need it in the future +} + +func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var conn net.Conn + if r.Header.Get("Upgrade") == "websocket" { + var err error + conn, _, _, err = ws.UpgradeHTTP(r, w) + if err != nil { + return + } + defer conn.Close() + } + + if conn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + tick := time.NewTicker(time.Second) + defer tick.Stop() + buf := &bytes.Buffer{} + var err error + first := true + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + } + buf.Reset() + + inuse := trafficManager.Snapshot().Memory + + // make chat.js begin with zero + // this is shit var,but we need output 0 for first time + if first { + first = false + inuse = 0 + } + if err := json.NewEncoder(buf).Encode(Memory{ + Inuse: inuse, + OSLimit: 0, + }); err != nil { + break + } + if conn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsutil.WriteServerText(conn, buf.Bytes()) + } + if err != nil { + break + } + } + } +} diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go new file mode 100644 index 00000000..31dbdaf6 --- /dev/null +++ b/experimental/clashapi/api_meta_group.go @@ -0,0 +1,136 @@ +package clashapi + +import ( + "context" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/batch" + "github.com/sagernet/sing/common/json/badjson" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func groupRouter(server *Server) http.Handler { + r := chi.NewRouter() + r.Get("/", getGroups(server)) + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProxyName, findProxyByName(server)) + r.Get("/", getGroup(server)) + r.Get("/delay", getGroupDelay(server)) + }) + return r +} + +func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + groups := common.Map(common.Filter(server.outbound.Outbounds(), func(it adapter.Outbound) bool { + _, isGroup := it.(adapter.OutboundGroup) + return isGroup + }), func(it adapter.Outbound) *badjson.JSONObject { + return proxyInfo(server, it) + }) + render.JSON(w, r, render.M{ + "proxies": groups, + }) + } +} + +func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) + if _, ok := proxy.(adapter.OutboundGroup); ok { + render.JSON(w, r, proxyInfo(server, proxy)) + return + } + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + } +} + +func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) + outboundGroup, ok := proxy.(adapter.OutboundGroup) + if !ok { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } + + query := r.URL.Query() + url := query.Get("url") + if strings.HasPrefix(url, "http://") { + url = "" + } + timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout)) + defer cancel() + + var result map[string]uint16 + if urlTestGroup, isURLTestGroup := outboundGroup.(adapter.URLTestGroup); isURLTestGroup { + result, err = urlTestGroup.URLTest(ctx) + } else { + outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { + itOutbound, _ := server.outbound.Outbound(it) + return itOutbound + })) + b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) + checked := make(map[string]bool) + result = make(map[string]uint16) + var resultAccess sync.Mutex + for _, detour := range outbounds { + tag := detour.Tag() + realTag := group.RealTag(detour) + if checked[realTag] { + continue + } + checked[realTag] = true + p, loaded := server.outbound.Outbound(realTag) + if !loaded { + continue + } + b.Go(realTag, func() (any, error) { + t, err := urltest.URLTest(ctx, url, p) + if err != nil { + server.logger.Debug("outbound ", tag, " unavailable: ", err) + server.urlTestHistory.DeleteURLTestHistory(realTag) + } else { + server.logger.Debug("outbound ", tag, " available: ", t, "ms") + server.urlTestHistory.StoreURLTestHistory(realTag, &adapter.URLTestHistory{ + Time: time.Now(), + Delay: t, + }) + resultAccess.Lock() + result[tag] = t + resultAccess.Unlock() + } + return nil, nil + }) + } + b.Wait() + } + + if err != nil { + render.Status(r, http.StatusGatewayTimeout) + render.JSON(w, r, newError(err.Error())) + return + } + + render.JSON(w, r, result) + } +} diff --git a/experimental/clashapi/api_meta_upgrade.go b/experimental/clashapi/api_meta_upgrade.go new file mode 100644 index 00000000..df70088e --- /dev/null +++ b/experimental/clashapi/api_meta_upgrade.go @@ -0,0 +1,36 @@ +package clashapi + +import ( + "net/http" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func upgradeRouter(server *Server) http.Handler { + r := chi.NewRouter() + r.Post("/ui", updateExternalUI(server)) + return r +} + +func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if server.externalUI == "" { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, newError("external UI not enabled")) + return + } + server.logger.Info("upgrading external UI") + err := server.downloadExternalUI() + if err != nil { + server.logger.Error(E.Cause(err, "upgrade external ui")) + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + server.logger.Info("updated external UI") + render.JSON(w, r, render.M{"status": "ok"}) + } +} diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go new file mode 100644 index 00000000..4df1f890 --- /dev/null +++ b/experimental/clashapi/cache.go @@ -0,0 +1,44 @@ +package clashapi + +import ( + "context" + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func cacheRouter(ctx context.Context) http.Handler { + r := chi.NewRouter() + r.Post("/fakeip/flush", flushFakeip(ctx)) + r.Post("/dns/flush", flushDNS(ctx)) + return r +} + +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { + err := cacheFile.FakeIPReset() + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + } + render.NoContent(w, r) + } +} + +func flushDNS(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + if dnsRouter != nil { + dnsRouter.ClearCache() + } + render.NoContent(w, r) + } +} diff --git a/experimental/clashapi/common.go b/experimental/clashapi/common.go new file mode 100644 index 00000000..416289c0 --- /dev/null +++ b/experimental/clashapi/common.go @@ -0,0 +1,17 @@ +package clashapi + +import ( + "net/http" + "net/url" + + "github.com/go-chi/chi/v5" +) + +// When name is composed of a partial escape string, Golang does not unescape it +func getEscapeParam(r *http.Request, paramName string) string { + param := chi.URLParam(r, paramName) + if newParam, err := url.PathUnescape(param); err == nil { + param = newParam + } + return param +} diff --git a/experimental/clashapi/configs.go b/experimental/clashapi/configs.go new file mode 100644 index 00000000..8ae1d258 --- /dev/null +++ b/experimental/clashapi/configs.go @@ -0,0 +1,71 @@ +package clashapi + +import ( + "net/http" + + "github.com/sagernet/sing-box/log" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func configRouter(server *Server, logFactory log.Factory) http.Handler { + r := chi.NewRouter() + r.Get("/", getConfigs(server, logFactory)) + r.Put("/", updateConfigs) + r.Patch("/", patchConfigs(server)) + return r +} + +type configSchema struct { + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` + AllowLan bool `json:"allow-lan"` + BindAddress string `json:"bind-address"` + Mode string `json:"mode"` + // sing-box added + ModeList []string `json:"mode-list"` + LogLevel string `json:"log-level"` + IPv6 bool `json:"ipv6"` + Tun map[string]any `json:"tun"` +} + +func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + logLevel := logFactory.Level() + if logLevel == log.LevelTrace { + logLevel = log.LevelDebug + } else if logLevel < log.LevelError { + logLevel = log.LevelError + } + render.JSON(w, r, &configSchema{ + Mode: server.mode, + ModeList: server.modeList, + BindAddress: "*", + LogLevel: log.FormatLevel(logLevel), + }) + } +} + +func patchConfigs(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var newConfig configSchema + err := render.DecodeJSON(r.Body, &newConfig) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + if newConfig.Mode != "" { + server.SetMode(newConfig.Mode) + } + render.NoContent(w, r) + } +} + +func updateConfigs(w http.ResponseWriter, r *http.Request) { + render.NoContent(w, r) +} diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go new file mode 100644 index 00000000..14274b31 --- /dev/null +++ b/experimental/clashapi/connections.go @@ -0,0 +1,108 @@ +package clashapi + +import ( + "bytes" + "context" + "net/http" + "strconv" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/gofrs/uuid/v5" +) + +func connectionRouter(ctx context.Context, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { + r := chi.NewRouter() + r.Get("/", getConnections(ctx, trafficManager)) + r.Delete("/", closeAllConnections(router, trafficManager)) + r.Delete("/{id}", closeConnection(trafficManager)) + return r +} + +func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Upgrade") != "websocket" { + snapshot := trafficManager.Snapshot() + render.JSON(w, r, snapshot) + return + } + + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + return + } + defer conn.Close() + + intervalStr := r.URL.Query().Get("interval") + interval := 1000 + if intervalStr != "" { + t, err := strconv.Atoi(intervalStr) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + interval = t + } + + buf := &bytes.Buffer{} + sendSnapshot := func() error { + buf.Reset() + snapshot := trafficManager.Snapshot() + if err := json.NewEncoder(buf).Encode(snapshot); err != nil { + return err + } + return wsutil.WriteServerText(conn, buf.Bytes()) + } + + if err = sendSnapshot(); err != nil { + return + } + + tick := time.NewTicker(time.Millisecond * time.Duration(interval)) + defer tick.Stop() + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + } + if err = sendSnapshot(); err != nil { + break + } + } + } +} + +func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + id := uuid.FromStringOrNil(chi.URLParam(r, "id")) + snapshot := trafficManager.Snapshot() + for _, c := range snapshot.Connections { + if id == c.Metadata().ID { + c.Close() + break + } + } + render.NoContent(w, r) + } +} + +func closeAllConnections(router adapter.Router, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + snapshot := trafficManager.Snapshot() + for _, c := range snapshot.Connections { + c.Close() + } + router.ResetNetwork() + render.NoContent(w, r) + } +} diff --git a/experimental/clashapi/ctxkeys.go b/experimental/clashapi/ctxkeys.go new file mode 100644 index 00000000..3a888026 --- /dev/null +++ b/experimental/clashapi/ctxkeys.go @@ -0,0 +1,14 @@ +package clashapi + +var ( + CtxKeyProxyName = contextKey("proxy name") + CtxKeyProviderName = contextKey("provider name") + CtxKeyProxy = contextKey("proxy") + CtxKeyProvider = contextKey("provider") +) + +type contextKey string + +func (c contextKey) String() string { + return "clash context key " + string(c) +} diff --git a/experimental/clashapi/dns.go b/experimental/clashapi/dns.go new file mode 100644 index 00000000..4f850f82 --- /dev/null +++ b/experimental/clashapi/dns.go @@ -0,0 +1,82 @@ +package clashapi + +import ( + "context" + "net/http" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/miekg/dns" +) + +func dnsRouter(router adapter.DNSRouter) http.Handler { + r := chi.NewRouter() + r.Get("/query", queryDNS(router)) + return r +} + +func queryDNS(router adapter.DNSRouter) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + name := r.URL.Query().Get("name") + qTypeStr := r.URL.Query().Get("type") + if qTypeStr == "" { + qTypeStr = "A" + } + + qType, exist := dns.StringToType[qTypeStr] + if !exist { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("invalid query type")) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), C.DNSTimeout) + defer cancel() + + msg := dns.Msg{} + msg.SetQuestion(dns.Fqdn(name), qType) + resp, err := router.Exchange(ctx, &msg, adapter.DNSQueryOptions{}) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + + responseData := render.M{ + "Status": resp.Rcode, + "Question": resp.Question, + "Server": "internal", + "TC": resp.Truncated, + "RD": resp.RecursionDesired, + "RA": resp.RecursionAvailable, + "AD": resp.AuthenticatedData, + "CD": resp.CheckingDisabled, + } + + rr2Json := func(rr dns.RR) render.M { + header := rr.Header() + return render.M{ + "name": header.Name, + "type": header.Rrtype, + "TTL": header.Ttl, + "data": rr.String()[len(header.String()):], + } + } + + if len(resp.Answer) > 0 { + responseData["Answer"] = common.Map(resp.Answer, rr2Json) + } + if len(resp.Ns) > 0 { + responseData["Authority"] = common.Map(resp.Ns, rr2Json) + } + if len(resp.Extra) > 0 { + responseData["Additional"] = common.Map(resp.Extra, rr2Json) + } + + render.JSON(w, r, responseData) + } +} diff --git a/experimental/clashapi/errors.go b/experimental/clashapi/errors.go new file mode 100644 index 00000000..7aaf76b7 --- /dev/null +++ b/experimental/clashapi/errors.go @@ -0,0 +1,22 @@ +package clashapi + +var ( + ErrUnauthorized = newError("Unauthorized") + ErrBadRequest = newError("Body invalid") + ErrForbidden = newError("Forbidden") + ErrNotFound = newError("Resource not found") + ErrRequestTimeout = newError("Timeout") +) + +// HTTPError is custom HTTP error for API +type HTTPError struct { + Message string `json:"message"` +} + +func (e *HTTPError) Error() string { + return e.Message +} + +func newError(msg string) *HTTPError { + return &HTTPError{Message: msg} +} diff --git a/experimental/clashapi/profile.go b/experimental/clashapi/profile.go new file mode 100644 index 00000000..4e20754f --- /dev/null +++ b/experimental/clashapi/profile.go @@ -0,0 +1,53 @@ +package clashapi + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func profileRouter() http.Handler { + r := chi.NewRouter() + r.Get("/tracing", subscribeTracing) + return r +} + +func subscribeTracing(w http.ResponseWriter, r *http.Request) { + // if !profile.Tracing.Load() { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + //return + //} + + /*wsConn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + + ch := make(chan map[string]any, 1024) + sub := event.Subscribe() + defer event.UnSubscribe(sub) + buf := &bytes.Buffer{} + + go func() { + for elm := range sub { + select { + case ch <- elm: + default: + } + } + close(ch) + }() + + for elm := range ch { + buf.Reset() + if err := json.NewEncoder(buf).Encode(elm); err != nil { + break + } + + if err := wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil { + break + } + }*/ +} diff --git a/experimental/clashapi/provider.go b/experimental/clashapi/provider.go new file mode 100644 index 00000000..352b2894 --- /dev/null +++ b/experimental/clashapi/provider.go @@ -0,0 +1,74 @@ +package clashapi + +import ( + "context" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func proxyProviderRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getProviders) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProviderName, findProviderByName) + r.Get("/", getProvider) + r.Put("/", updateProvider) + r.Get("/healthcheck", healthCheckProvider) + }) + return r +} + +func getProviders(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{ + "providers": render.M{}, + }) +} + +func getProvider(w http.ResponseWriter, r *http.Request) { + /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + render.JSON(w, r, provider)*/ + render.NoContent(w, r) +} + +func updateProvider(w http.ResponseWriter, r *http.Request) { + /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + if err := provider.Update(); err != nil { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, newError(err.Error())) + return + }*/ + render.NoContent(w, r) +} + +func healthCheckProvider(w http.ResponseWriter, r *http.Request) { + /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + provider.HealthCheck()*/ + render.NoContent(w, r) +} + +func parseProviderName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := getEscapeParam(r, "name") + ctx := context.WithValue(r.Context(), CtxKeyProviderName, name) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProviderByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + /*name := r.Context().Value(CtxKeyProviderName).(string) + providers := tunnel.ProxyProviders() + provider, exist := providers[name] + if !exist {*/ + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + //return + //} + + // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) + // next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go new file mode 100644 index 00000000..ef88ff37 --- /dev/null +++ b/experimental/clashapi/proxies.go @@ -0,0 +1,234 @@ +package clashapi + +import ( + "context" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badjson" + N "github.com/sagernet/sing/common/network" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func proxyRouter(server *Server, router adapter.Router) http.Handler { + r := chi.NewRouter() + r.Get("/", getProxies(server)) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProxyName, findProxyByName(server)) + r.Get("/", getProxy(server)) + r.Get("/delay", getProxyDelay(server)) + r.Put("/", updateProxy) + }) + return r +} + +func parseProxyName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := getEscapeParam(r, "name") + ctx := context.WithValue(r.Context(), CtxKeyProxyName, name) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProxyByName(server *Server) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := r.Context().Value(CtxKeyProxyName).(string) + proxy, exist := server.outbound.Outbound(name) + if !exist { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } + ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { + var info badjson.JSONObject + var clashType string + switch detour.Type() { + case C.TypeBlock: + clashType = "Reject" + default: + clashType = C.ProxyDisplayName(detour.Type()) + } + info.Put("type", clashType) + info.Put("name", detour.Tag()) + info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP)) + delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour)) + if delayHistory != nil { + info.Put("history", []*adapter.URLTestHistory{delayHistory}) + } else { + info.Put("history", []*adapter.URLTestHistory{}) + } + if group, isGroup := detour.(adapter.OutboundGroup); isGroup { + info.Put("now", group.Now()) + info.Put("all", group.All()) + } + return &info +} + +func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var proxyMap badjson.JSONObject + outbounds := common.Filter(server.outbound.Outbounds(), func(detour adapter.Outbound) bool { + return detour.Tag() != "" + }) + outbounds = append(outbounds, common.Map(common.Filter(server.endpoint.Endpoints(), func(detour adapter.Endpoint) bool { + return detour.Tag() != "" + }), func(it adapter.Endpoint) adapter.Outbound { + return it + })...) + + allProxies := make([]string, 0, len(outbounds)) + + for _, detour := range outbounds { + switch detour.Type() { + case C.TypeDirect, C.TypeBlock, C.TypeDNS: + continue + } + allProxies = append(allProxies, detour.Tag()) + } + + defaultTag := server.outbound.Default().Tag() + + sort.SliceStable(allProxies, func(i, j int) bool { + return allProxies[i] == defaultTag + }) + + // fix clash dashboard + proxyMap.Put("GLOBAL", map[string]any{ + "type": "Fallback", + "name": "GLOBAL", + "udp": true, + "history": []*adapter.URLTestHistory{}, + "all": allProxies, + "now": defaultTag, + }) + + for i, detour := range outbounds { + var tag string + if detour.Tag() == "" { + tag = F.ToString(i) + } else { + tag = detour.Tag() + } + proxyMap.Put(tag, proxyInfo(server, detour)) + } + var responseMap badjson.JSONObject + responseMap.Put("proxies", &proxyMap) + response, err := responseMap.MarshalJSON() + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + w.Write(response) + } +} + +func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) + response, err := proxyInfo(server, proxy).MarshalJSON() + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + w.Write(response) + } +} + +type UpdateProxyRequest struct { + Name string `json:"name"` +} + +func updateProxy(w http.ResponseWriter, r *http.Request) { + req := UpdateProxyRequest{} + if err := render.DecodeJSON(r.Body, &req); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) + selector, ok := proxy.(*group.Selector) + if !ok { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("Must be a Selector")) + return + } + + if !selector.SelectOutbound(req.Name) { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("Selector update error: not found")) + return + } + + render.NoContent(w, r) +} + +func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + url := query.Get("url") + if strings.HasPrefix(url, "http://") { + url = "" + } + timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) + defer cancel() + + delay, err := urltest.URLTest(ctx, url, proxy) + defer func() { + realTag := group.RealTag(proxy) + if err != nil { + server.urlTestHistory.DeleteURLTestHistory(realTag) + } else { + server.urlTestHistory.StoreURLTestHistory(realTag, &adapter.URLTestHistory{ + Time: time.Now(), + Delay: delay, + }) + } + }() + + if ctx.Err() != nil { + render.Status(r, http.StatusGatewayTimeout) + render.JSON(w, r, ErrRequestTimeout) + return + } + + if err != nil || delay == 0 { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, newError("An error occurred in the delay test")) + return + } + + render.JSON(w, r, render.M{ + "delay": delay, + }) + } +} diff --git a/experimental/clashapi/ruleprovider.go b/experimental/clashapi/ruleprovider.go new file mode 100644 index 00000000..4a410854 --- /dev/null +++ b/experimental/clashapi/ruleprovider.go @@ -0,0 +1,58 @@ +package clashapi + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func ruleProviderRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getRuleProviders) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProviderName, findRuleProviderByName) + r.Get("/", getRuleProvider) + r.Put("/", updateRuleProvider) + }) + return r +} + +func getRuleProviders(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{ + "providers": []string{}, + }) +} + +func getRuleProvider(w http.ResponseWriter, r *http.Request) { + // provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) + // render.JSON(w, r, provider) + render.NoContent(w, r) +} + +func updateRuleProvider(w http.ResponseWriter, r *http.Request) { + /*provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) + if err := provider.Update(); err != nil { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, newError(err.Error())) + return + }*/ + render.NoContent(w, r) +} + +func findRuleProviderByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + /*name := r.Context().Value(CtxKeyProviderName).(string) + providers := tunnel.RuleProviders() + provider, exist := providers[name] + if !exist {*/ + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + //return + //} + + // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) + // next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/experimental/clashapi/rules.go b/experimental/clashapi/rules.go new file mode 100644 index 00000000..bc8fbb2b --- /dev/null +++ b/experimental/clashapi/rules.go @@ -0,0 +1,40 @@ +package clashapi + +import ( + "net/http" + + "github.com/sagernet/sing-box/adapter" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func ruleRouter(router adapter.Router) http.Handler { + r := chi.NewRouter() + r.Get("/", getRules(router)) + return r +} + +type Rule struct { + Type string `json:"type"` + Payload string `json:"payload"` + Proxy string `json:"proxy"` +} + +func getRules(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + rawRules := router.Rules() + + var rules []Rule + for _, rule := range rawRules { + rules = append(rules, Rule{ + Type: rule.Type(), + Payload: rule.String(), + Proxy: rule.Action().String(), + }) + } + render.JSON(w, r, render.M{ + "rules": rules, + }) + } +} diff --git a/experimental/clashapi/script.go b/experimental/clashapi/script.go new file mode 100644 index 00000000..a7b52f97 --- /dev/null +++ b/experimental/clashapi/script.go @@ -0,0 +1,98 @@ +package clashapi + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func scriptRouter() http.Handler { + r := chi.NewRouter() + r.Post("/", testScript) + r.Patch("/", patchScript) + return r +} + +/*type TestScriptRequest struct { + Script *string `json:"script"` + Metadata C.Metadata `json:"metadata"` +}*/ + +func testScript(w http.ResponseWriter, r *http.Request) { + /* req := TestScriptRequest{} + if err := render.DecodeJSON(r.Body, &req); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + fn := tunnel.ScriptFn() + if req.Script == nil && fn == nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("should send `script`")) + return + } + + if !req.Metadata.Valid() { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("metadata not valid")) + return + } + + if req.Script != nil { + var err error + fn, err = script.ParseScript(*req.Script) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + } + + ctx, _ := script.MakeContext(tunnel.ProxyProviders(), tunnel.RuleProviders()) + + thread := &starlark.Thread{} + ret, err := starlark.Call(thread, fn, starlark.Tuple{ctx, script.MakeMetadata(&req.Metadata)}, nil) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + + elm, ok := ret.(starlark.String) + if !ok { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, "script fn must return a string") + return + } + + render.JSON(w, r, render.M{ + "result": string(elm), + })*/ + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("not implemented")) +} + +type PatchScriptRequest struct { + Script string `json:"script"` +} + +func patchScript(w http.ResponseWriter, r *http.Request) { + /*req := PatchScriptRequest{} + if err := render.DecodeJSON(r.Body, &req); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + fn, err := script.ParseScript(req.Script) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + + tunnel.UpdateScript(fn)*/ + render.NoContent(w, r) +} diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go new file mode 100644 index 00000000..ec40a95f --- /dev/null +++ b/experimental/clashapi/server.go @@ -0,0 +1,435 @@ +package clashapi + +import ( + "bytes" + "context" + "errors" + "net" + "net/http" + "os" + "runtime" + "strings" + "syscall" + "time" + + "github.com/sagernet/cors" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func init() { + experimental.RegisterClashServerConstructor(NewServer) +} + +var _ adapter.ClashServer = (*Server)(nil) + +type Server struct { + ctx context.Context + router adapter.Router + dnsRouter adapter.DNSRouter + outbound adapter.OutboundManager + endpoint adapter.EndpointManager + logger log.Logger + httpServer *http.Server + trafficManager *trafficontrol.Manager + urlTestHistory adapter.URLTestHistoryStorage + logDebug bool + + mode string + modeList []string + modeUpdateHook *observable.Subscriber[struct{}] + + externalController bool + externalUI string + externalUIDownloadURL string + externalUIDownloadDetour string +} + +func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { + trafficManager := trafficontrol.NewManager() + chiRouter := chi.NewRouter() + s := &Server{ + ctx: ctx, + router: service.FromContext[adapter.Router](ctx), + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + endpoint: service.FromContext[adapter.EndpointManager](ctx), + logger: logFactory.NewLogger("clash-api"), + httpServer: &http.Server{ + Addr: options.ExternalController, + Handler: chiRouter, + }, + trafficManager: trafficManager, + logDebug: logFactory.Level() >= log.LevelDebug, + modeList: options.ModeList, + externalController: options.ExternalController != "", + externalUIDownloadURL: options.ExternalUIDownloadURL, + externalUIDownloadDetour: options.ExternalUIDownloadDetour, + } + s.urlTestHistory = service.FromContext[adapter.URLTestHistoryStorage](ctx) + if s.urlTestHistory == nil { + s.urlTestHistory = urltest.NewHistoryStorage() + } + defaultMode := "Rule" + if options.DefaultMode != "" { + defaultMode = options.DefaultMode + } + if !common.Contains(s.modeList, defaultMode) { + s.modeList = append([]string{defaultMode}, s.modeList...) + } + s.mode = defaultMode + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") + } + allowedOrigins := options.AccessControlAllowOrigin + if len(allowedOrigins) == 0 { + allowedOrigins = []string{"*"} + } + cors := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork, + MaxAge: 300, + }) + chiRouter.Use(cors.Handler) + chiRouter.Group(func(r chi.Router) { + r.Use(authentication(options.Secret)) + r.Get("/", hello(options.ExternalUI != "")) + r.Get("/logs", getLogs(s.ctx, logFactory)) + r.Get("/traffic", traffic(s.ctx, trafficManager)) + r.Get("/version", version) + r.Mount("/configs", configRouter(s, logFactory)) + r.Mount("/proxies", proxyRouter(s, s.router)) + r.Mount("/rules", ruleRouter(s.router)) + r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager)) + r.Mount("/providers/proxies", proxyProviderRouter()) + r.Mount("/providers/rules", ruleProviderRouter()) + r.Mount("/script", scriptRouter()) + r.Mount("/profile", profileRouter()) + r.Mount("/cache", cacheRouter(ctx)) + r.Mount("/dns", dnsRouter(s.dnsRouter)) + + s.setupMetaAPI(r) + }) + if options.ExternalUI != "" { + s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) + chiRouter.Group(func(r chi.Router) { + r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP) + r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(Dir(s.externalUI)))) + }) + } + return s, nil +} + +func (s *Server) Name() string { + return "clash server" +} + +func (s *Server) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode + } + } + case adapter.StartStateStarted: + if s.externalController { + s.checkAndDownloadExternalUI() + var ( + listener net.Listener + err error + ) + for i := 0; i < 3; i++ { + listener, err = net.Listen("tcp", s.httpServer.Addr) + if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) { + time.Sleep(100 * time.Millisecond) + continue + } + break + } + if err != nil { + return E.Cause(err, "external controller listen error") + } + s.logger.Info("restful api listening at ", listener.Addr()) + go func() { + err = s.httpServer.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("external controller serve error: ", err) + } + }() + } + } + + return nil +} + +func (s *Server) Close() error { + return common.Close( + common.PtrOrNil(s.httpServer), + s.trafficManager, + s.urlTestHistory, + ) +} + +func (s *Server) Mode() string { + return s.mode +} + +func (s *Server) ModeList() []string { + return s.modeList +} + +func (s *Server) SetModeUpdateHook(hook *observable.Subscriber[struct{}]) { + s.modeUpdateHook = hook +} + +func (s *Server) SetMode(newMode string) { + if !common.Contains(s.modeList, newMode) { + newMode = common.Find(s.modeList, func(it string) bool { + return strings.EqualFold(it, newMode) + }) + } + if !common.Contains(s.modeList, newMode) { + return + } + if newMode == s.mode { + return + } + s.mode = newMode + if s.modeUpdateHook != nil { + s.modeUpdateHook.Emit(struct{}{}) + } + s.dnsRouter.ClearCache() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) + if err != nil { + s.logger.Error(E.Cause(err, "save mode")) + } + } + s.logger.Info("updated mode: ", newMode) +} + +func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage { + return s.urlTestHistory +} + +func (s *Server) TrafficManager() *trafficontrol.Manager { + return s.trafficManager +} + +func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) +} + +func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { + return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) +} + +func authentication(serverSecret string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if serverSecret == "" { + next.ServeHTTP(w, r) + return + } + + // Browser websocket not support custom header + if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { + token := r.URL.Query().Get("token") + if token != serverSecret { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + return + } + + header := r.Header.Get("Authorization") + bearer, token, found := strings.Cut(header, " ") + + hasInvalidHeader := bearer != "Bearer" + hasInvalidSecret := !found || token != serverSecret + if hasInvalidHeader || hasInvalidSecret { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} + +func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + if !redirect || contentType == "application/json" { + render.JSON(w, r, render.M{"hello": "clash"}) + } else { + http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect) + } + } +} + +type Traffic struct { + Up int64 `json:"up"` + Down int64 `json:"down"` +} + +func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var conn net.Conn + if r.Header.Get("Upgrade") == "websocket" { + var err error + conn, _, _, err = ws.UpgradeHTTP(r, w) + if err != nil { + return + } + defer conn.Close() + } + + if conn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + tick := time.NewTicker(time.Second) + defer tick.Stop() + buf := &bytes.Buffer{} + uploadTotal, downloadTotal := trafficManager.Total() + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + } + buf.Reset() + uploadTotalNew, downloadTotalNew := trafficManager.Total() + err := json.NewEncoder(buf).Encode(Traffic{ + Up: uploadTotalNew - uploadTotal, + Down: downloadTotalNew - downloadTotal, + }) + if err != nil { + break + } + if conn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsutil.WriteServerText(conn, buf.Bytes()) + } + if err != nil { + break + } + + uploadTotal = uploadTotalNew + downloadTotal = downloadTotalNew + } + } +} + +type Log struct { + Type string `json:"type"` + Payload string `json:"payload"` +} + +func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + levelText := r.URL.Query().Get("level") + if levelText == "" { + levelText = "info" + } + + level, ok := log.ParseLevel(levelText) + if ok != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + subscription, done, err := logFactory.Subscribe() + if err != nil { + render.Status(r, http.StatusNoContent) + return + } + defer logFactory.UnSubscribe(subscription) + + var conn net.Conn + if r.Header.Get("Upgrade") == "websocket" { + conn, _, _, err = ws.UpgradeHTTP(r, w) + if err != nil { + return + } + defer conn.Close() + } + + if conn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + buf := &bytes.Buffer{} + var logEntry log.Entry + for { + select { + case <-ctx.Done(): + return + case <-done: + return + case logEntry = <-subscription: + } + if logEntry.Level > level { + continue + } + buf.Reset() + err = json.NewEncoder(buf).Encode(Log{ + Type: log.FormatLevel(logEntry.Level), + Payload: logEntry.Message, + }) + if err != nil { + break + } + if conn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsutil.WriteServerText(conn, buf.Bytes()) + } + + if err != nil { + break + } + } + } +} + +func version(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true}) +} diff --git a/experimental/clashapi/server_fs.go b/experimental/clashapi/server_fs.go new file mode 100644 index 00000000..afb4cca5 --- /dev/null +++ b/experimental/clashapi/server_fs.go @@ -0,0 +1,18 @@ +package clashapi + +import "net/http" + +type Dir http.Dir + +func (d Dir) Open(name string) (http.File, error) { + file, err := http.Dir(d).Open(name) + if err != nil { + return nil, err + } + return &fileWrapper{file}, nil +} + +// workaround for #2345 #2596 +type fileWrapper struct { + http.File +} diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go new file mode 100644 index 00000000..ad9fff53 --- /dev/null +++ b/experimental/clashapi/server_resources.go @@ -0,0 +1,170 @@ +package clashapi + +import ( + "archive/zip" + "context" + "crypto/tls" + "io" + "net" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service/filemanager" +) + +func (s *Server) checkAndDownloadExternalUI() { + if s.externalUI == "" { + return + } + entries, err := os.ReadDir(s.externalUI) + if err != nil { + os.MkdirAll(s.externalUI, 0o755) + } + if len(entries) == 0 { + err = s.downloadExternalUI() + if err != nil { + s.logger.Error("download external ui error: ", err) + } + } +} + +func (s *Server) downloadExternalUI() error { + var downloadURL string + if s.externalUIDownloadURL != "" { + downloadURL = s.externalUIDownloadURL + } else { + downloadURL = "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip" + } + var detour adapter.Outbound + if s.externalUIDownloadDetour != "" { + outbound, loaded := s.outbound.Outbound(s.externalUIDownloadDetour) + if !loaded { + return E.New("detour outbound not found: ", s.externalUIDownloadDetour) + } + detour = outbound + } else { + outbound := s.outbound.Default() + detour = outbound + } + s.logger.Info("downloading external ui using outbound/", detour.Type(), "[", detour.Tag(), "]") + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return detour.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + Time: ntp.TimeFuncFromContext(s.ctx), + RootCAs: adapter.RootPoolFromContext(s.ctx), + }, + }, + } + defer httpClient.CloseIdleConnections() + response, err := httpClient.Get(downloadURL) + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return E.New("download external ui failed: ", response.Status) + } + err = s.downloadZIP(response.Body, s.externalUI) + if err != nil { + removeAllInDirectory(s.externalUI) + } + return err +} + +func (s *Server) downloadZIP(body io.Reader, output string) error { + tempFile, err := filemanager.CreateTemp(s.ctx, "external-ui.zip") + if err != nil { + return err + } + defer os.Remove(tempFile.Name()) + _, err = io.Copy(tempFile, body) + tempFile.Close() + if err != nil { + return err + } + reader, err := zip.OpenReader(tempFile.Name()) + if err != nil { + return err + } + defer reader.Close() + trimDir := zipIsInSingleDirectory(reader.File) + for _, file := range reader.File { + if file.FileInfo().IsDir() { + continue + } + pathElements := strings.Split(file.Name, "/") + if trimDir { + pathElements = pathElements[1:] + } + saveDirectory := output + if len(pathElements) > 1 { + saveDirectory = filepath.Join(saveDirectory, filepath.Join(pathElements[:len(pathElements)-1]...)) + } + err = os.MkdirAll(saveDirectory, 0o755) + if err != nil { + return err + } + savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1]) + err = downloadZIPEntry(s.ctx, file, savePath) + if err != nil { + return err + } + } + return nil +} + +func downloadZIPEntry(ctx context.Context, zipFile *zip.File, savePath string) error { + saveFile, err := filemanager.Create(ctx, savePath) + if err != nil { + return err + } + defer saveFile.Close() + reader, err := zipFile.Open() + if err != nil { + return err + } + defer reader.Close() + return common.Error(io.Copy(saveFile, reader)) +} + +func removeAllInDirectory(directory string) { + dirEntries, err := os.ReadDir(directory) + if err != nil { + return + } + for _, dirEntry := range dirEntries { + os.RemoveAll(filepath.Join(directory, dirEntry.Name())) + } +} + +func zipIsInSingleDirectory(files []*zip.File) bool { + var singleDirectory string + for _, file := range files { + if file.FileInfo().IsDir() { + continue + } + pathElements := strings.Split(file.Name, "/") + if len(pathElements) == 0 { + return false + } + if singleDirectory == "" { + singleDirectory = pathElements[0] + } else if singleDirectory != pathElements[0] { + return false + } + } + return true +} diff --git a/experimental/clashapi/trafficontrol/manager.go b/experimental/clashapi/trafficontrol/manager.go new file mode 100644 index 00000000..6763436d --- /dev/null +++ b/experimental/clashapi/trafficontrol/manager.go @@ -0,0 +1,181 @@ +package trafficontrol + +import ( + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/common/compatible" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/common/x/list" + + "github.com/gofrs/uuid/v5" +) + +type ConnectionEventType int + +const ( + ConnectionEventNew ConnectionEventType = iota + ConnectionEventUpdate + ConnectionEventClosed +) + +type ConnectionEvent struct { + Type ConnectionEventType + ID uuid.UUID + Metadata *TrackerMetadata + UplinkDelta int64 + DownlinkDelta int64 + ClosedAt time.Time +} + +const closedConnectionsLimit = 1000 + +type Manager struct { + uploadTotal atomic.Int64 + downloadTotal atomic.Int64 + + connections compatible.Map[uuid.UUID, Tracker] + closedConnectionsAccess sync.Mutex + closedConnections list.List[TrackerMetadata] + memory uint64 + + eventSubscriber *observable.Subscriber[ConnectionEvent] +} + +func NewManager() *Manager { + return &Manager{} +} + +func (m *Manager) SetEventHook(subscriber *observable.Subscriber[ConnectionEvent]) { + m.eventSubscriber = subscriber +} + +func (m *Manager) Join(c Tracker) { + metadata := c.Metadata() + m.connections.Store(metadata.ID, c) + if m.eventSubscriber != nil { + m.eventSubscriber.Emit(ConnectionEvent{ + Type: ConnectionEventNew, + ID: metadata.ID, + Metadata: metadata, + }) + } +} + +func (m *Manager) Leave(c Tracker) { + metadata := c.Metadata() + _, loaded := m.connections.LoadAndDelete(metadata.ID) + if loaded { + closedAt := time.Now() + metadata.ClosedAt = closedAt + metadataCopy := *metadata + m.closedConnectionsAccess.Lock() + if m.closedConnections.Len() >= closedConnectionsLimit { + m.closedConnections.PopFront() + } + m.closedConnections.PushBack(metadataCopy) + m.closedConnectionsAccess.Unlock() + if m.eventSubscriber != nil { + m.eventSubscriber.Emit(ConnectionEvent{ + Type: ConnectionEventClosed, + ID: metadata.ID, + Metadata: &metadataCopy, + ClosedAt: closedAt, + }) + } + } +} + +func (m *Manager) PushUploaded(size int64) { + m.uploadTotal.Add(size) +} + +func (m *Manager) PushDownloaded(size int64) { + m.downloadTotal.Add(size) +} + +func (m *Manager) Total() (up int64, down int64) { + return m.uploadTotal.Load(), m.downloadTotal.Load() +} + +func (m *Manager) ConnectionsLen() int { + return m.connections.Len() +} + +func (m *Manager) Connections() []*TrackerMetadata { + var connections []*TrackerMetadata + m.connections.Range(func(_ uuid.UUID, value Tracker) bool { + connections = append(connections, value.Metadata()) + return true + }) + return connections +} + +func (m *Manager) ClosedConnections() []*TrackerMetadata { + m.closedConnectionsAccess.Lock() + values := m.closedConnections.Array() + m.closedConnectionsAccess.Unlock() + if len(values) == 0 { + return nil + } + connections := make([]*TrackerMetadata, len(values)) + for i := range values { + connections[i] = &values[i] + } + return connections +} + +func (m *Manager) Connection(id uuid.UUID) Tracker { + connection, loaded := m.connections.Load(id) + if !loaded { + return nil + } + return connection +} + +func (m *Manager) Snapshot() *Snapshot { + var connections []Tracker + m.connections.Range(func(_ uuid.UUID, value Tracker) bool { + if value.Metadata().OutboundType != C.TypeDNS { + connections = append(connections, value) + } + return true + }) + + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased + + return &Snapshot{ + Upload: m.uploadTotal.Load(), + Download: m.downloadTotal.Load(), + Connections: connections, + Memory: m.memory, + } +} + +func (m *Manager) ResetStatistic() { + m.uploadTotal.Store(0) + m.downloadTotal.Store(0) +} + +type Snapshot struct { + Download int64 + Upload int64 + Connections []Tracker + Memory uint64 +} + +func (s *Snapshot) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "downloadTotal": s.Download, + "uploadTotal": s.Upload, + "connections": common.Map(s.Connections, func(t Tracker) *TrackerMetadata { return t.Metadata() }), + "memory": s.Memory, + }) +} diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go new file mode 100644 index 00000000..f001b77b --- /dev/null +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -0,0 +1,254 @@ +package trafficontrol + +import ( + "net" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" + N "github.com/sagernet/sing/common/network" + + "github.com/gofrs/uuid/v5" +) + +type TrackerMetadata struct { + ID uuid.UUID + Metadata adapter.InboundContext + CreatedAt time.Time + ClosedAt time.Time + Upload *atomic.Int64 + Download *atomic.Int64 + Chain []string + Rule adapter.Rule + Outbound string + OutboundType string +} + +func (t TrackerMetadata) MarshalJSON() ([]byte, error) { + var inbound string + if t.Metadata.Inbound != "" { + inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound + } else { + inbound = t.Metadata.InboundType + } + var domain string + if t.Metadata.Domain != "" { + domain = t.Metadata.Domain + } else { + domain = t.Metadata.Destination.Fqdn + } + var processPath string + if t.Metadata.ProcessInfo != nil { + if t.Metadata.ProcessInfo.ProcessPath != "" { + processPath = t.Metadata.ProcessInfo.ProcessPath + } else if len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 { + processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0] + } + if processPath == "" { + if t.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(t.Metadata.ProcessInfo.UserId) + } + } else if t.Metadata.ProcessInfo.UserName != "" { + processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserName, ")") + } else if t.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")") + } + } + var rule string + if t.Rule != nil { + rule = F.ToString(t.Rule, " => ", t.Rule.Action()) + } else { + rule = "final" + } + return json.Marshal(map[string]any{ + "id": t.ID, + "metadata": map[string]any{ + "network": t.Metadata.Network, + "type": inbound, + "sourceIP": t.Metadata.Source.Addr, + "destinationIP": t.Metadata.Destination.Addr, + "sourcePort": F.ToString(t.Metadata.Source.Port), + "destinationPort": F.ToString(t.Metadata.Destination.Port), + "host": domain, + "dnsMode": "normal", + "processPath": processPath, + }, + "upload": t.Upload.Load(), + "download": t.Download.Load(), + "start": t.CreatedAt, + "chains": t.Chain, + "rule": rule, + "rulePayload": "", + }) +} + +type Tracker interface { + Metadata() *TrackerMetadata + Close() error +} + +type TCPConn struct { + N.ExtendedConn + metadata TrackerMetadata + manager *Manager +} + +func (tt *TCPConn) Metadata() *TrackerMetadata { + return &tt.metadata +} + +func (tt *TCPConn) Close() error { + tt.manager.Leave(tt) + return tt.ExtendedConn.Close() +} + +func (tt *TCPConn) Upstream() any { + return tt.ExtendedConn +} + +func (tt *TCPConn) ReaderReplaceable() bool { + return true +} + +func (tt *TCPConn) WriterReplaceable() bool { + return true +} + +func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *TCPConn { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) + if matchOutbound != nil { + next = matchOutbound.Tag() + } else { + next = outboundManager.Default().Tag() + } + for { + detour, loaded := outboundManager.Outbound(next) + if !loaded { + break + } + chain = append(chain, next) + outbound = detour.Tag() + outboundType = detour.Type() + group, isGroup := detour.(adapter.OutboundGroup) + if !isGroup { + break + } + next = group.Now() + } + upload := new(atomic.Int64) + download := new(atomic.Int64) + tracker := &TCPConn{ + ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { + upload.Add(n) + manager.PushUploaded(n) + }}, []N.CountFunc{func(n int64) { + download.Add(n) + manager.PushDownloaded(n) + }}), + metadata: TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: matchRule, + Outbound: outbound, + OutboundType: outboundType, + }, + manager: manager, + } + manager.Join(tracker) + return tracker +} + +type UDPConn struct { + N.PacketConn `json:"-"` + metadata TrackerMetadata + manager *Manager +} + +func (ut *UDPConn) Metadata() *TrackerMetadata { + return &ut.metadata +} + +func (ut *UDPConn) Close() error { + ut.manager.Leave(ut) + return ut.PacketConn.Close() +} + +func (ut *UDPConn) Upstream() any { + return ut.PacketConn +} + +func (ut *UDPConn) ReaderReplaceable() bool { + return true +} + +func (ut *UDPConn) WriterReplaceable() bool { + return true +} + +func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *UDPConn { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) + if matchOutbound != nil { + next = matchOutbound.Tag() + } else { + next = outboundManager.Default().Tag() + } + for { + detour, loaded := outboundManager.Outbound(next) + if !loaded { + break + } + chain = append(chain, next) + outbound = detour.Tag() + outboundType = detour.Type() + group, isGroup := detour.(adapter.OutboundGroup) + if !isGroup { + break + } + next = group.Now() + } + upload := new(atomic.Int64) + download := new(atomic.Int64) + trackerConn := &UDPConn{ + PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { + upload.Add(n) + manager.PushUploaded(n) + }}, []N.CountFunc{func(n int64) { + download.Add(n) + manager.PushDownloaded(n) + }}), + metadata: TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: matchRule, + Outbound: outbound, + OutboundType: outboundType, + }, + manager: manager, + } + manager.Join(trackerConn) + return trackerConn +} diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go new file mode 100644 index 00000000..108eba57 --- /dev/null +++ b/experimental/deprecated/constants.go @@ -0,0 +1,151 @@ +package deprecated + +import ( + "fmt" + + "github.com/sagernet/sing-box/common/badversion" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/locale" + F "github.com/sagernet/sing/common/format" + + "golang.org/x/mod/semver" +) + +type Note struct { + Name string + Description string + DeprecatedVersion string + ScheduledVersion string + EnvName string + MigrationLink string +} + +func (n Note) Impending() bool { + if n.ScheduledVersion == "" { + return false + } + if !semver.IsValid("v" + C.Version) { + return false + } + versionCurrent := badversion.Parse(C.Version) + versionMinor := badversion.Parse(n.ScheduledVersion).Minor - versionCurrent.Minor + if versionCurrent.PreReleaseIdentifier == "" && versionMinor < 0 { + panic("invalid deprecated note: " + n.Name) + } + return versionMinor <= 1 +} + +func (n Note) Message() string { + if n.MigrationLink != "" { + return fmt.Sprintf(locale.Current().DeprecatedMessage, n.Description, n.DeprecatedVersion, n.ScheduledVersion) + } else { + return fmt.Sprintf(locale.Current().DeprecatedMessageNoLink, n.Description, n.DeprecatedVersion, n.ScheduledVersion) + } +} + +func (n Note) MessageWithLink() string { + if n.MigrationLink != "" { + return F.ToString( + n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, + " and will be removed in sing-box ", n.ScheduledVersion, ", checkout documentation for migration: ", n.MigrationLink, + ) + } else { + return F.ToString( + n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, + " and will be removed in sing-box ", n.ScheduledVersion, ".", + ) + } +} + +var OptionOutboundDNSRuleItem = Note{ + Name: "outbound-dns-rule-item", + Description: "outbound DNS rule item", + DeprecatedVersion: "1.12.0", + ScheduledVersion: "1.14.0", + EnvName: "OUTBOUND_DNS_RULE_ITEM", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver", +} + +var OptionMissingDomainResolver = Note{ + Name: "missing-domain-resolver", + Description: "missing `route.default_domain_resolver` or `domain_resolver` in dial fields", + DeprecatedVersion: "1.12.0", + ScheduledVersion: "1.14.0", + EnvName: "MISSING_DOMAIN_RESOLVER", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver", +} + +var OptionLegacyDomainStrategyOptions = Note{ + Name: "legacy-domain-strategy-options", + Description: "legacy domain strategy options", + DeprecatedVersion: "1.12.0", + ScheduledVersion: "1.14.0", + EnvName: "LEGACY_DOMAIN_STRATEGY_OPTIONS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-domain-strategy-options", +} + +var OptionInlineACME = Note{ + Name: "inline-acme-options", + Description: "inline ACME options in TLS", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "INLINE_ACME_OPTIONS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider", +} + +var OptionRuleSetIPCIDRAcceptEmpty = Note{ + Name: "dns-rule-rule-set-ip-cidr-accept-empty", + Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "DNS_RULE_RULE_SET_IP_CIDR_ACCEPT_EMPTY", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching", +} + +var OptionLegacyDNSAddressFilter = Note{ + Name: "legacy-dns-address-filter", + Description: "Legacy Address Filter Fields in DNS rules", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "LEGACY_DNS_ADDRESS_FILTER", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching", +} + +var OptionLegacyDNSRuleStrategy = Note{ + Name: "legacy-dns-rule-strategy", + Description: "Legacy `strategy` DNS rule action option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "LEGACY_DNS_RULE_STRATEGY", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items", +} + +var OptionIndependentDNSCache = Note{ + Name: "independent-dns-cache", + Description: "`independent_cache` DNS option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "INDEPENDENT_DNS_CACHE", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-independent-dns-cache", +} + +var OptionStoreRDRC = Note{ + Name: "store-rdrc", + Description: "`store_rdrc` cache file option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "STORE_RDRC", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc", +} + +var Options = []Note{ + OptionOutboundDNSRuleItem, + OptionMissingDomainResolver, + OptionLegacyDomainStrategyOptions, + OptionInlineACME, + OptionRuleSetIPCIDRAcceptEmpty, + OptionLegacyDNSAddressFilter, + OptionLegacyDNSRuleStrategy, + OptionIndependentDNSCache, + OptionStoreRDRC, +} diff --git a/experimental/deprecated/manager.go b/experimental/deprecated/manager.go new file mode 100644 index 00000000..d12acf48 --- /dev/null +++ b/experimental/deprecated/manager.go @@ -0,0 +1,19 @@ +package deprecated + +import ( + "context" + + "github.com/sagernet/sing/service" +) + +type Manager interface { + ReportDeprecated(feature Note) +} + +func Report(ctx context.Context, feature Note) { + manager := service.FromContext[Manager](ctx) + if manager == nil { + return + } + manager.ReportDeprecated(feature) +} diff --git a/experimental/deprecated/stderr.go b/experimental/deprecated/stderr.go new file mode 100644 index 00000000..0dfb9354 --- /dev/null +++ b/experimental/deprecated/stderr.go @@ -0,0 +1,42 @@ +package deprecated + +import ( + "os" + "strconv" + + "github.com/sagernet/sing/common/logger" +) + +type stderrManager struct { + logger logger.Logger + reported map[string]bool +} + +func NewStderrManager(logger logger.Logger) Manager { + return &stderrManager{ + logger: logger, + reported: make(map[string]bool), + } +} + +func (f *stderrManager) ReportDeprecated(feature Note) { + if f.reported[feature.Name] { + return + } + f.reported[feature.Name] = true + if !feature.Impending() { + f.logger.Warn(feature.MessageWithLink()) + return + } + if feature.EnvName != "" { + enable, enableErr := strconv.ParseBool(os.Getenv("ENABLE_DEPRECATED_" + feature.EnvName)) + if enableErr == nil && enable { + f.logger.Warn(feature.MessageWithLink()) + return + } + f.logger.Error(feature.MessageWithLink()) + f.logger.Fatal("to continuing using this feature, set environment variable ENABLE_DEPRECATED_" + feature.EnvName + "=true") + } else { + f.logger.Error(feature.MessageWithLink()) + } +} diff --git a/experimental/libbox/build_info.go b/experimental/libbox/build_info.go new file mode 100644 index 00000000..5a02593c --- /dev/null +++ b/experimental/libbox/build_info.go @@ -0,0 +1,234 @@ +//go:build android + +package libbox + +import ( + "archive/zip" + "bytes" + "debug/buildinfo" + "io" + "runtime/debug" + "strings" + + "github.com/sagernet/sing/common" +) + +const ( + androidVPNCoreTypeOpenVPN = "OpenVPN" + androidVPNCoreTypeShadowsocks = "Shadowsocks" + androidVPNCoreTypeClash = "Clash" + androidVPNCoreTypeV2Ray = "V2Ray" + androidVPNCoreTypeWireGuard = "WireGuard" + androidVPNCoreTypeSingBox = "sing-box" + androidVPNCoreTypeUnknown = "Unknown" +) + +type AndroidVPNType struct { + CoreType string + CorePath string + GoVersion string +} + +func ReadAndroidVPNType(publicSourceDirList StringIterator) (*AndroidVPNType, error) { + apkPathList := iteratorToArray[string](publicSourceDirList) + var lastError error + for _, apkPath := range apkPathList { + androidVPNType, err := readAndroidVPNType(apkPath) + if androidVPNType == nil { + if err != nil { + lastError = err + } + continue + } + return androidVPNType, nil + } + return nil, lastError +} + +func readAndroidVPNType(publicSourceDir string) (*AndroidVPNType, error) { + reader, err := zip.OpenReader(publicSourceDir) + if err != nil { + return nil, err + } + defer reader.Close() + var lastError error + for _, file := range reader.File { + if !strings.HasPrefix(file.Name, "lib/") { + continue + } + vpnType, err := readAndroidVPNTypeEntry(file) + if err != nil { + lastError = err + continue + } + return vpnType, nil + } + for _, file := range reader.File { + if !strings.HasPrefix(file.Name, "lib/") { + continue + } + if strings.Contains(file.Name, androidVPNCoreTypeOpenVPN) || strings.Contains(file.Name, "ovpn") { + return &AndroidVPNType{CoreType: androidVPNCoreTypeOpenVPN}, nil + } + if strings.Contains(file.Name, androidVPNCoreTypeShadowsocks) { + return &AndroidVPNType{CoreType: androidVPNCoreTypeShadowsocks}, nil + } + } + return nil, lastError +} + +func readAndroidVPNTypeEntry(zipFile *zip.File) (*AndroidVPNType, error) { + readCloser, err := zipFile.Open() + if err != nil { + return nil, err + } + libContent := make([]byte, zipFile.UncompressedSize64) + _, err = io.ReadFull(readCloser, libContent) + readCloser.Close() + if err != nil { + return nil, err + } + buildInfo, err := buildinfo.Read(bytes.NewReader(libContent)) + if err != nil { + return nil, err + } + var vpnType AndroidVPNType + vpnType.GoVersion = buildInfo.GoVersion + if !strings.HasPrefix(vpnType.GoVersion, "go") { + vpnType.GoVersion = "obfuscated" + } else { + vpnType.GoVersion = vpnType.GoVersion[2:] + } + vpnType.CoreType = androidVPNCoreTypeUnknown + if len(buildInfo.Deps) == 0 { + vpnType.CoreType = "obfuscated" + return &vpnType, nil + } + + dependencies := make(map[string]bool) + dependencies[buildInfo.Path] = true + for _, module := range buildInfo.Deps { + dependencies[module.Path] = true + if module.Replace != nil { + dependencies[module.Replace.Path] = true + } + } + for dependency := range dependencies { + pkgType, loaded := determinePkgType(dependency) + if loaded { + vpnType.CoreType = pkgType + } + } + if vpnType.CoreType == androidVPNCoreTypeUnknown { + for dependency := range dependencies { + pkgType, loaded := determinePkgTypeSecondary(dependency) + if loaded { + vpnType.CoreType = pkgType + return &vpnType, nil + } + } + } + if vpnType.CoreType != androidVPNCoreTypeUnknown { + vpnType.CorePath, _ = determineCorePath(buildInfo, vpnType.CoreType) + return &vpnType, nil + } + if dependencies["github.com/golang/protobuf"] && dependencies["github.com/v2fly/ss-bloomring"] { + vpnType.CoreType = androidVPNCoreTypeV2Ray + return &vpnType, nil + } + return &vpnType, nil +} + +func determinePkgType(pkgName string) (string, bool) { + pkgNameLower := strings.ToLower(pkgName) + if strings.Contains(pkgNameLower, "clash") { + return androidVPNCoreTypeClash, true + } + if strings.Contains(pkgNameLower, "v2ray") || strings.Contains(pkgNameLower, "xray") { + return androidVPNCoreTypeV2Ray, true + } + + if strings.Contains(pkgNameLower, "sing-box") { + return androidVPNCoreTypeSingBox, true + } + return "", false +} + +func determinePkgTypeSecondary(pkgName string) (string, bool) { + pkgNameLower := strings.ToLower(pkgName) + if strings.Contains(pkgNameLower, "wireguard") { + return androidVPNCoreTypeWireGuard, true + } + return "", false +} + +func determineCorePath(pkgInfo *buildinfo.BuildInfo, pkgType string) (string, bool) { + switch pkgType { + case androidVPNCoreTypeClash: + return determineCorePathForPkgs(pkgInfo, []string{"github.com/Dreamacro/clash"}, []string{"clash"}) + case androidVPNCoreTypeV2Ray: + if v2rayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{ + "github.com/v2fly/v2ray-core", + "github.com/v2fly/v2ray-core/v4", + "github.com/v2fly/v2ray-core/v5", + }, []string{ + "v2ray", + }); loaded { + return v2rayVersion, true + } + if xrayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{ + "github.com/xtls/xray-core", + }, []string{ + "xray", + }); loaded { + return xrayVersion, true + } + return "", false + case androidVPNCoreTypeSingBox: + return determineCorePathForPkgs(pkgInfo, []string{"github.com/sagernet/sing-box"}, []string{"sing-box"}) + case androidVPNCoreTypeWireGuard: + return determineCorePathForPkgs(pkgInfo, []string{"golang.zx2c4.com/wireguard"}, []string{"wireguard"}) + default: + return "", false + } +} + +func determineCorePathForPkgs(pkgInfo *buildinfo.BuildInfo, pkgs []string, names []string) (string, bool) { + for _, pkg := range pkgs { + if pkgInfo.Path == pkg { + return pkg, true + } + strictDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool { + return module.Path == pkg + }) + if strictDependency != nil { + if isValidVersion(strictDependency.Version) { + return strictDependency.Path + " " + strictDependency.Version, true + } else { + return strictDependency.Path, true + } + } + } + for _, name := range names { + if strings.Contains(pkgInfo.Path, name) { + return pkgInfo.Path, true + } + looseDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool { + return strings.Contains(module.Path, name) || (module.Replace != nil && strings.Contains(module.Replace.Path, name)) + }) + if looseDependency != nil { + return looseDependency.Path, true + } + } + return "", false +} + +func isValidVersion(version string) bool { + if version == "(devel)" { + return false + } + if strings.Contains(version, "v0.0.0") { + return false + } + return true +} diff --git a/experimental/libbox/command.go b/experimental/libbox/command.go new file mode 100644 index 00000000..8a43bc95 --- /dev/null +++ b/experimental/libbox/command.go @@ -0,0 +1,10 @@ +package libbox + +const ( + CommandLog int32 = iota + CommandStatus + CommandGroup + CommandClashMode + CommandConnections + CommandOutbounds +) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go new file mode 100644 index 00000000..d4347e10 --- /dev/null +++ b/experimental/libbox/command_client.go @@ -0,0 +1,807 @@ +package libbox + +import ( + "context" + "net" + "os" + "path/filepath" + "strconv" + "sync" + "time" + + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +type CommandClient struct { + handler CommandClientHandler + grpcConn *grpc.ClientConn + grpcClient daemon.StartedServiceClient + options CommandClientOptions + ctx context.Context + cancel context.CancelFunc + clientMutex sync.RWMutex + standalone bool +} + +type CommandClientOptions struct { + commands []int32 + StatusInterval int64 +} + +func (o *CommandClientOptions) AddCommand(command int32) { + o.commands = append(o.commands, command) +} + +type CommandClientHandler interface { + Connected() + Disconnected(message string) + SetDefaultLogLevel(level int32) + ClearLogs() + WriteLogs(messageList LogIterator) + WriteStatus(message *StatusMessage) + WriteGroups(message OutboundGroupIterator) + WriteOutbounds(message OutboundGroupItemIterator) + InitializeClashMode(modeList StringIterator, currentMode string) + UpdateClashMode(newMode string) + WriteConnectionEvents(events *ConnectionEvents) +} + +type LogEntry struct { + Level int32 + Message string +} + +type LogIterator interface { + Len() int32 + HasNext() bool + Next() *LogEntry +} + +type XPCDialer interface { + DialXPC() (int32, error) +} + +var sXPCDialer XPCDialer + +func SetXPCDialer(dialer XPCDialer) { + sXPCDialer = dialer +} + +func NewStandaloneCommandClient() *CommandClient { + return &CommandClient{standalone: true} +} + +func NewCommandClient(handler CommandClientHandler, options *CommandClientOptions) *CommandClient { + return &CommandClient{ + handler: handler, + options: common.PtrValueOrDefault(options), + } +} + +func unaryClientAuthInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + if sCommandServerSecret != "" { + ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret) + } + return invoker(ctx, method, req, reply, cc, opts...) +} + +func streamClientAuthInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + if sCommandServerSecret != "" { + ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret) + } + return streamer(ctx, desc, cc, method, opts...) +} + +const ( + commandClientDialAttempts = 10 + commandClientDialBaseDelay = 100 * time.Millisecond + commandClientDialStepDelay = 50 * time.Millisecond +) + +func commandClientDialDelay(attempt int) time.Duration { + return commandClientDialBaseDelay + time.Duration(attempt)*commandClientDialStepDelay +} + +func dialTarget() (string, func(context.Context, string) (net.Conn, error)) { + if sXPCDialer != nil { + return "passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) { + fileDescriptor, err := sXPCDialer.DialXPC() + if err != nil { + return nil, E.Cause(err, "dial xpc") + } + return networkConnectionFromFileDescriptor(fileDescriptor) + } + } + if sCommandServerListenPort == 0 { + socketPath := filepath.Join(sBasePath, "command.sock") + return "passthrough:///command-socket", func(ctx context.Context, _ string) (net.Conn, error) { + var networkDialer net.Dialer + return networkDialer.DialContext(ctx, "unix", socketPath) + } + } + return net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort))), nil +} + +func networkConnectionFromFileDescriptor(fileDescriptor int32) (net.Conn, error) { + file := os.NewFile(uintptr(fileDescriptor), "xpc-command-socket") + if file == nil { + return nil, E.New("invalid file descriptor") + } + networkConnection, err := net.FileConn(file) + if err != nil { + file.Close() + return nil, E.Cause(err, "create connection from fd") + } + file.Close() + return networkConnection, nil +} + +func (c *CommandClient) dialWithRetry(target string, contextDialer func(context.Context, string) (net.Conn, error), retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) { + var connection *grpc.ClientConn + var client daemon.StartedServiceClient + var lastError error + + for attempt := 0; attempt < commandClientDialAttempts; attempt++ { + if connection == nil { + options := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(unaryClientAuthInterceptor), + grpc.WithStreamInterceptor(streamClientAuthInterceptor), + } + if contextDialer != nil { + options = append(options, grpc.WithContextDialer(contextDialer)) + } + var err error + connection, err = grpc.NewClient(target, options...) + if err != nil { + lastError = err + if !retryDial { + return nil, nil, E.Cause(err, "create command client") + } + time.Sleep(commandClientDialDelay(attempt)) + continue + } + client = daemon.NewStartedServiceClient(connection) + } + waitDuration := commandClientDialDelay(attempt) + ctx, cancel := context.WithTimeout(context.Background(), waitDuration) + _, err := client.GetStartedAt(ctx, &emptypb.Empty{}, grpc.WaitForReady(true)) + cancel() + if err == nil { + return connection, client, nil + } + lastError = err + } + + if connection != nil { + connection.Close() + } + return nil, nil, E.Cause(lastError, "probe command server") +} + +func (c *CommandClient) Connect() error { + c.clientMutex.Lock() + common.Close(common.PtrOrNil(c.grpcConn)) + + target, contextDialer := dialTarget() + connection, client, err := c.dialWithRetry(target, contextDialer, true) + if err != nil { + c.clientMutex.Unlock() + return err + } + c.grpcConn = connection + c.grpcClient = client + c.ctx, c.cancel = context.WithCancel(context.Background()) + c.clientMutex.Unlock() + + c.handler.Connected() + return c.dispatchCommands() +} + +func (c *CommandClient) ConnectWithFD(fd int32) error { + c.clientMutex.Lock() + common.Close(common.PtrOrNil(c.grpcConn)) + + networkConnection, err := networkConnectionFromFileDescriptor(fd) + if err != nil { + c.clientMutex.Unlock() + return err + } + connection, client, err := c.dialWithRetry("passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) { + return networkConnection, nil + }, false) + if err != nil { + networkConnection.Close() + c.clientMutex.Unlock() + return err + } + c.grpcConn = connection + c.grpcClient = client + c.ctx, c.cancel = context.WithCancel(context.Background()) + c.clientMutex.Unlock() + + c.handler.Connected() + return c.dispatchCommands() +} + +func (c *CommandClient) dispatchCommands() error { + for _, command := range c.options.commands { + switch command { + case CommandLog: + go c.handleLogStream() + case CommandStatus: + go c.handleStatusStream() + case CommandGroup: + go c.handleGroupStream() + case CommandClashMode: + go c.handleClashModeStream() + case CommandConnections: + go c.handleConnectionsStream() + case CommandOutbounds: + go c.handleOutboundsStream() + default: + return E.New("unknown command: ", command) + } + } + return nil +} + +func (c *CommandClient) Disconnect() error { + c.clientMutex.Lock() + defer c.clientMutex.Unlock() + if c.cancel != nil { + c.cancel() + } + return common.Close(common.PtrOrNil(c.grpcConn)) +} + +func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error) { + c.clientMutex.RLock() + if c.grpcClient != nil { + defer c.clientMutex.RUnlock() + return c.grpcClient, nil + } + c.clientMutex.RUnlock() + + c.clientMutex.Lock() + defer c.clientMutex.Unlock() + + if c.grpcClient != nil { + return c.grpcClient, nil + } + + target, contextDialer := dialTarget() + connection, client, err := c.dialWithRetry(target, contextDialer, true) + if err != nil { + return nil, E.Cause(err, "get command client") + } + c.grpcConn = connection + c.grpcClient = client + if c.ctx == nil { + c.ctx, c.cancel = context.WithCancel(context.Background()) + } + return c.grpcClient, nil +} + +func (c *CommandClient) closeConnection() { + c.clientMutex.Lock() + defer c.clientMutex.Unlock() + if c.grpcConn != nil { + c.grpcConn.Close() + c.grpcConn = nil + c.grpcClient = nil + } +} + +func callWithResult[T any](c *CommandClient, call func(client daemon.StartedServiceClient) (T, error)) (T, error) { + client, err := c.getClientForCall() + if err != nil { + var zero T + return zero, err + } + if c.standalone { + defer c.closeConnection() + } + return call(client) +} + +func (c *CommandClient) getStreamContext() (daemon.StartedServiceClient, context.Context) { + c.clientMutex.RLock() + defer c.clientMutex.RUnlock() + return c.grpcClient, c.ctx +} + +func (c *CommandClient) handleLogStream() { + client, ctx := c.getStreamContext() + stream, err := client.SubscribeLog(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(E.Cause(err, "subscribe log").Error()) + return + } + defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(E.Cause(err, "get default log level").Error()) + return + } + c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level)) + for { + logMessage, err := stream.Recv() + if err != nil { + c.handler.Disconnected(E.Cause(err, "log stream recv").Error()) + return + } + if logMessage.Reset_ { + c.handler.ClearLogs() + } + var messages []*LogEntry + for _, msg := range logMessage.Messages { + messages = append(messages, &LogEntry{ + Level: int32(msg.Level), + Message: msg.Message, + }) + } + c.handler.WriteLogs(newIterator(messages)) + } +} + +func (c *CommandClient) handleStatusStream() { + client, ctx := c.getStreamContext() + interval := c.options.StatusInterval + + stream, err := client.SubscribeStatus(ctx, &daemon.SubscribeStatusRequest{ + Interval: interval, + }) + if err != nil { + c.handler.Disconnected(E.Cause(err, "subscribe status").Error()) + return + } + + for { + status, err := stream.Recv() + if err != nil { + c.handler.Disconnected(E.Cause(err, "status stream recv").Error()) + return + } + c.handler.WriteStatus(statusMessageFromGRPC(status)) + } +} + +func (c *CommandClient) handleGroupStream() { + client, ctx := c.getStreamContext() + + stream, err := client.SubscribeGroups(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(E.Cause(err, "subscribe groups").Error()) + return + } + + for { + groups, err := stream.Recv() + if err != nil { + c.handler.Disconnected(E.Cause(err, "groups stream recv").Error()) + return + } + c.handler.WriteGroups(outboundGroupIteratorFromGRPC(groups)) + } +} + +func (c *CommandClient) handleClashModeStream() { + client, ctx := c.getStreamContext() + + modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(E.Cause(err, "get clash mode status").Error()) + return + } + + if sFixAndroidStack { + go func() { + c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) + if len(modeStatus.ModeList) == 0 { + c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error()) + } + }() + } else { + c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) + if len(modeStatus.ModeList) == 0 { + c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error()) + return + } + } + + if len(modeStatus.ModeList) == 0 { + return + } + + stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(E.Cause(err, "subscribe clash mode").Error()) + return + } + + for { + mode, err := stream.Recv() + if err != nil { + c.handler.Disconnected(E.Cause(err, "clash mode stream recv").Error()) + return + } + c.handler.UpdateClashMode(mode.Mode) + } +} + +func (c *CommandClient) handleConnectionsStream() { + client, ctx := c.getStreamContext() + interval := c.options.StatusInterval + + stream, err := client.SubscribeConnections(ctx, &daemon.SubscribeConnectionsRequest{ + Interval: interval, + }) + if err != nil { + c.handler.Disconnected(E.Cause(err, "subscribe connections").Error()) + return + } + + for { + events, err := stream.Recv() + if err != nil { + c.handler.Disconnected(E.Cause(err, "connections stream recv").Error()) + return + } + libboxEvents := connectionEventsFromGRPC(events) + c.handler.WriteConnectionEvents(libboxEvents) + } +} + +func (c *CommandClient) handleOutboundsStream() { + client, ctx := c.getStreamContext() + + stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(E.Cause(err, "subscribe outbounds").Error()) + return + } + + for { + list, err := stream.Recv() + if err != nil { + c.handler.Disconnected(E.Cause(err, "outbounds stream recv").Error()) + return + } + c.handler.WriteOutbounds(outboundGroupItemListFromGRPC(list)) + } +} + +func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{ + GroupTag: groupTag, + OutboundTag: outboundTag, + }) + }) + if err != nil { + return E.Cause(err, "select outbound") + } + return nil +} + +func (c *CommandClient) URLTest(groupTag string) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.URLTest(context.Background(), &daemon.URLTestRequest{ + OutboundTag: groupTag, + }) + }) + if err != nil { + return E.Cause(err, "url test") + } + return nil +} + +func (c *CommandClient) SetClashMode(newMode string) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SetClashMode(context.Background(), &daemon.ClashMode{ + Mode: newMode, + }) + }) + if err != nil { + return E.Cause(err, "set clash mode") + } + return nil +} + +func (c *CommandClient) CloseConnection(connId string) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{ + Id: connId, + }) + }) + if err != nil { + return E.Cause(err, "close connection") + } + return nil +} + +func (c *CommandClient) CloseConnections() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.CloseAllConnections(context.Background(), &emptypb.Empty{}) + }) + if err != nil { + return E.Cause(err, "close all connections") + } + return nil +} + +func (c *CommandClient) ServiceReload() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.ReloadService(context.Background(), &emptypb.Empty{}) + }) + if err != nil { + return E.Cause(err, "reload service") + } + return nil +} + +func (c *CommandClient) ServiceClose() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.StopService(context.Background(), &emptypb.Empty{}) + }) + if err != nil { + return E.Cause(err, "stop service") + } + return nil +} + +func (c *CommandClient) ClearLogs() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.ClearLogs(context.Background(), &emptypb.Empty{}) + }) + if err != nil { + return E.Cause(err, "clear logs") + } + return nil +} + +func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) { + return callWithResult(c, func(client daemon.StartedServiceClient) (*SystemProxyStatus, error) { + status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{}) + if err != nil { + return nil, E.Cause(err, "get system proxy status") + } + return systemProxyStatusFromGRPC(status), nil + }) +} + +func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{ + Enabled: isEnabled, + }) + }) + if err != nil { + return E.Cause(err, "set system proxy enabled") + } + return nil +} + +func (c *CommandClient) TriggerGoCrash() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{ + Type: daemon.DebugCrashRequest_GO, + }) + }) + if err != nil { + return E.Cause(err, "trigger debug crash") + } + return nil +} + +func (c *CommandClient) TriggerNativeCrash() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{ + Type: daemon.DebugCrashRequest_NATIVE, + }) + }) + if err != nil { + return E.Cause(err, "trigger native crash") + } + return nil +} + +func (c *CommandClient) TriggerOOMReport() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.TriggerOOMReport(context.Background(), &emptypb.Empty{}) + }) + if err != nil { + return E.Cause(err, "trigger oom report") + } + return nil +} + +func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) { + return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) { + warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{}) + if err != nil { + return nil, E.Cause(err, "get deprecated warnings") + } + var notes []*DeprecatedNote + for _, warning := range warnings.Warnings { + notes = append(notes, &DeprecatedNote{ + Description: warning.Description, + DeprecatedVersion: warning.DeprecatedVersion, + ScheduledVersion: warning.ScheduledVersion, + MigrationLink: warning.MigrationLink, + }) + } + return newIterator(notes), nil + }) +} + +func (c *CommandClient) GetStartedAt() (int64, error) { + return callWithResult(c, func(client daemon.StartedServiceClient) (int64, error) { + startedAt, err := client.GetStartedAt(context.Background(), &emptypb.Empty{}) + if err != nil { + return 0, E.Cause(err, "get started at") + } + return startedAt.StartedAt, nil + }) +} + +func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{ + GroupTag: groupTag, + IsExpand: isExpand, + }) + }) + if err != nil { + return E.Cause(err, "set group expand") + } + return nil +} + +func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) error { + client, err := c.getClientForCall() + if err != nil { + return E.Cause(err, "start network quality test") + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.StartNetworkQualityTest(context.Background(), &daemon.NetworkQualityTestRequest{ + ConfigURL: configURL, + OutboundTag: outboundTag, + Serial: serial, + MaxRuntimeSeconds: maxRuntimeSeconds, + Http3: http3, + }) + if err != nil { + return E.Cause(err, "start network quality test") + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + recvErr = E.Cause(recvErr, "network quality test recv") + handler.OnError(recvErr.Error()) + return recvErr + } + if event.IsFinal { + if event.Error != "" { + handler.OnError(event.Error) + } else { + handler.OnResult(&NetworkQualityResult{ + DownloadCapacity: event.DownloadCapacity, + UploadCapacity: event.UploadCapacity, + DownloadRPM: event.DownloadRPM, + UploadRPM: event.UploadRPM, + IdleLatencyMs: event.IdleLatencyMs, + DownloadCapacityAccuracy: event.DownloadCapacityAccuracy, + UploadCapacityAccuracy: event.UploadCapacityAccuracy, + DownloadRPMAccuracy: event.DownloadRPMAccuracy, + UploadRPMAccuracy: event.UploadRPMAccuracy, + }) + } + return nil + } + handler.OnProgress(networkQualityProgressFromGRPC(event)) + } +} + +func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler STUNTestHandler) error { + client, err := c.getClientForCall() + if err != nil { + return E.Cause(err, "start stun test") + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.StartSTUNTest(context.Background(), &daemon.STUNTestRequest{ + Server: server, + OutboundTag: outboundTag, + }) + if err != nil { + return E.Cause(err, "start stun test") + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + recvErr = E.Cause(recvErr, "stun test recv") + handler.OnError(recvErr.Error()) + return recvErr + } + if event.IsFinal { + if event.Error != "" { + handler.OnError(event.Error) + } else { + handler.OnResult(&STUNTestResult{ + ExternalAddr: event.ExternalAddr, + LatencyMs: event.LatencyMs, + NATMapping: event.NatMapping, + NATFiltering: event.NatFiltering, + NATTypeSupported: event.NatTypeSupported, + }) + } + return nil + } + handler.OnProgress(stunTestProgressFromGRPC(event)) + } +} + +func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) error { + client, err := c.getClientForCall() + if err != nil { + return E.Cause(err, "subscribe tailscale status") + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.SubscribeTailscaleStatus(context.Background(), &emptypb.Empty{}) + if err != nil { + return E.Cause(err, "subscribe tailscale status") + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable { + return nil + } + recvErr = E.Cause(recvErr, "tailscale status recv") + handler.OnError(recvErr.Error()) + return recvErr + } + handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(event)) + } +} + +func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, handler TailscalePingHandler) error { + client, err := c.getClientForCall() + if err != nil { + return E.Cause(err, "start tailscale ping") + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.StartTailscalePing(context.Background(), &daemon.TailscalePingRequest{ + EndpointTag: endpointTag, + PeerIP: peerIP, + }) + if err != nil { + return E.Cause(err, "start tailscale ping") + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + recvErr = E.Cause(recvErr, "tailscale ping recv") + handler.OnError(recvErr.Error()) + return recvErr + } + handler.OnPingResult(tailscalePingResultFromGRPC(event)) + } +} diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go new file mode 100644 index 00000000..60ec17a8 --- /dev/null +++ b/experimental/libbox/command_server.go @@ -0,0 +1,288 @@ +package libbox + +import ( + "context" + "errors" + "net" + "os" + "path/filepath" + "strconv" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type CommandServer struct { + *daemon.StartedService + handler CommandServerHandler + platformInterface PlatformInterface + platformWrapper *platformInterfaceWrapper + grpcServer *grpc.Server + listener net.Listener + endPauseTimer *time.Timer +} + +type CommandServerHandler interface { + ServiceStop() error + ServiceReload() error + GetSystemProxyStatus() (*SystemProxyStatus, error) + SetSystemProxyEnabled(enabled bool) error + TriggerNativeCrash() error + WriteDebugMessage(message string) +} + +func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) { + ctx := baseContext(platformInterface) + platformWrapper := &platformInterfaceWrapper{ + iif: platformInterface, + useProcFS: platformInterface.UseProcFS(), + } + service.MustRegister[adapter.PlatformInterface](ctx, platformWrapper) + server := &CommandServer{ + handler: handler, + platformInterface: platformInterface, + platformWrapper: platformWrapper, + } + server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{ + Context: ctx, + // Platform: platformWrapper, + Handler: (*platformHandler)(server), + Debug: sDebug, + LogMaxLines: sLogMaxLines, + OOMKillerEnabled: sOOMKillerEnabled, + OOMKillerDisabled: sOOMKillerDisabled, + OOMMemoryLimit: uint64(sOOMMemoryLimit), + // WorkingDirectory: sWorkingPath, + // TempDirectory: sTempPath, + // UserID: sUserID, + // GroupID: sGroupID, + // SystemProxyEnabled: false, + }) + return server, nil +} + +func unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + if sCommandServerSecret == "" { + return handler(ctx, req) + } + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "missing metadata") + } + values := md.Get("x-command-secret") + if len(values) == 0 { + return nil, status.Error(codes.Unauthenticated, "missing authentication secret") + } + if values[0] != sCommandServerSecret { + return nil, status.Error(codes.Unauthenticated, "invalid authentication secret") + } + return handler(ctx, req) +} + +func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if sCommandServerSecret == "" { + return handler(srv, ss) + } + md, ok := metadata.FromIncomingContext(ss.Context()) + if !ok { + return status.Error(codes.Unauthenticated, "missing metadata") + } + values := md.Get("x-command-secret") + if len(values) == 0 { + return status.Error(codes.Unauthenticated, "missing authentication secret") + } + if values[0] != sCommandServerSecret { + return status.Error(codes.Unauthenticated, "invalid authentication secret") + } + return handler(srv, ss) +} + +func (s *CommandServer) Start() error { + var ( + listener net.Listener + err error + ) + if sCommandServerListenPort == 0 { + sockPath := filepath.Join(sBasePath, "command.sock") + os.Remove(sockPath) + for i := 0; i < 30; i++ { + listener, err = net.ListenUnix("unix", &net.UnixAddr{ + Name: sockPath, + Net: "unix", + }) + if err == nil { + break + } + if !errors.Is(err, syscall.EROFS) { + break + } + time.Sleep(time.Second) + } + if err != nil { + return E.Cause(err, "listen command server") + } + if sUserID != os.Getuid() { + err = os.Chown(sockPath, sUserID, sGroupID) + if err != nil { + listener.Close() + os.Remove(sockPath) + return E.Cause(err, "chown") + } + } + } else { + listener, err = net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort)))) + if err != nil { + return E.Cause(err, "listen command server") + } + } + s.listener = listener + serverOptions := []grpc.ServerOption{ + grpc.UnaryInterceptor(unaryAuthInterceptor), + grpc.StreamInterceptor(streamAuthInterceptor), + } + s.grpcServer = grpc.NewServer(serverOptions...) + daemon.RegisterStartedServiceServer(s.grpcServer, s.StartedService) + go s.grpcServer.Serve(listener) + return nil +} + +func (s *CommandServer) Close() { + if s.grpcServer != nil { + s.grpcServer.Stop() + } + common.Close(s.listener) + s.StartedService.Close() +} + +type OverrideOptions struct { + AutoRedirect bool + IncludePackage StringIterator + ExcludePackage StringIterator +} + +func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error { + saveConfigSnapshot(configContent) + err := s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{ + AutoRedirect: options.AutoRedirect, + IncludePackage: iteratorToArray(options.IncludePackage), + ExcludePackage: iteratorToArray(options.ExcludePackage), + }) + if err != nil { + return E.Cause(err, "start or reload service") + } + return nil +} + +func (s *CommandServer) CloseService() error { + return s.StartedService.CloseService() +} + +func (s *CommandServer) WriteMessage(level int32, message string) { + s.StartedService.WriteMessage(log.Level(level), message) +} + +func (s *CommandServer) SetError(message string) { + s.StartedService.SetError(E.New(message)) +} + +func (s *CommandServer) NeedWIFIState() bool { + instance := s.StartedService.Instance() + if instance == nil || instance.Box() == nil { + return false + } + return instance.Box().Network().NeedWIFIState() +} + +func (s *CommandServer) NeedFindProcess() bool { + instance := s.StartedService.Instance() + if instance == nil || instance.Box() == nil { + return false + } + return instance.Box().Router().NeedFindProcess() +} + +func (s *CommandServer) Pause() { + instance := s.StartedService.Instance() + if instance == nil || instance.PauseManager() == nil { + return + } + instance.PauseManager().DevicePause() + if C.IsIos { + if s.endPauseTimer == nil { + s.endPauseTimer = time.AfterFunc(time.Minute, instance.PauseManager().DeviceWake) + } else { + s.endPauseTimer.Reset(time.Minute) + } + } +} + +func (s *CommandServer) Wake() { + instance := s.StartedService.Instance() + if instance == nil || instance.PauseManager() == nil { + return + } + if !C.IsIos { + instance.PauseManager().DeviceWake() + } +} + +func (s *CommandServer) ResetNetwork() { + instance := s.StartedService.Instance() + if instance == nil || instance.Box() == nil { + return + } + instance.Box().Router().ResetNetwork() +} + +func (s *CommandServer) UpdateWIFIState() { + instance := s.StartedService.Instance() + if instance == nil || instance.Box() == nil { + return + } + instance.Box().Network().UpdateWIFIState() +} + +type platformHandler CommandServer + +func (h *platformHandler) ServiceStop() error { + return (*CommandServer)(h).handler.ServiceStop() +} + +func (h *platformHandler) ServiceReload() error { + return (*CommandServer)(h).handler.ServiceReload() +} + +func (h *platformHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) { + status, err := (*CommandServer)(h).handler.GetSystemProxyStatus() + if err != nil { + return nil, E.Cause(err, "get system proxy status") + } + return &daemon.SystemProxyStatus{ + Enabled: status.Enabled, + Available: status.Available, + }, nil +} + +func (h *platformHandler) SetSystemProxyEnabled(enabled bool) error { + return (*CommandServer)(h).handler.SetSystemProxyEnabled(enabled) +} + +func (h *platformHandler) TriggerNativeCrash() error { + return (*CommandServer)(h).handler.TriggerNativeCrash() +} + +func (h *platformHandler) WriteDebugMessage(message string) { + (*CommandServer)(h).handler.WriteDebugMessage(message) +} diff --git a/experimental/libbox/command_types.go b/experimental/libbox/command_types.go new file mode 100644 index 00000000..61634b01 --- /dev/null +++ b/experimental/libbox/command_types.go @@ -0,0 +1,446 @@ +package libbox + +import ( + "slices" + "strings" + "time" + + "github.com/sagernet/sing-box/daemon" + M "github.com/sagernet/sing/common/metadata" +) + +type StatusMessage struct { + Memory int64 + Goroutines int32 + ConnectionsIn int32 + ConnectionsOut int32 + TrafficAvailable bool + Uplink int64 + Downlink int64 + UplinkTotal int64 + DownlinkTotal int64 +} + +type SystemProxyStatus struct { + Available bool + Enabled bool +} + +type OutboundGroup struct { + Tag string + Type string + Selectable bool + Selected string + IsExpand bool + itemList []*OutboundGroupItem +} + +func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { + return newIterator(g.itemList) +} + +type OutboundGroupIterator interface { + Next() *OutboundGroup + HasNext() bool +} + +type OutboundGroupItem struct { + Tag string + Type string + URLTestTime int64 + URLTestDelay int32 +} + +type OutboundGroupItemIterator interface { + Next() *OutboundGroupItem + HasNext() bool +} + +const ( + ConnectionStateAll = iota + ConnectionStateActive + ConnectionStateClosed +) + +const ( + ConnectionEventNew = iota + ConnectionEventUpdate + ConnectionEventClosed +) + +const ( + closedConnectionMaxAge = int64((5 * time.Minute) / time.Millisecond) +) + +type ConnectionEvent struct { + Type int32 + ID string + Connection *Connection + UplinkDelta int64 + DownlinkDelta int64 + ClosedAt int64 +} + +type ConnectionEvents struct { + Reset bool + events []*ConnectionEvent +} + +func (c *ConnectionEvents) Iterator() ConnectionEventIterator { + return newIterator(c.events) +} + +type ConnectionEventIterator interface { + Next() *ConnectionEvent + HasNext() bool +} + +type Connections struct { + connectionMap map[string]*Connection + input []Connection + filtered []Connection + filterState int32 + filterApplied bool +} + +func NewConnections() *Connections { + return &Connections{ + connectionMap: make(map[string]*Connection), + } +} + +func (c *Connections) ApplyEvents(events *ConnectionEvents) { + if events == nil { + return + } + if events.Reset { + c.connectionMap = make(map[string]*Connection) + } + + for _, event := range events.events { + switch event.Type { + case ConnectionEventNew: + if event.Connection != nil { + conn := *event.Connection + c.connectionMap[event.ID] = &conn + } + case ConnectionEventUpdate: + if conn, ok := c.connectionMap[event.ID]; ok { + conn.Uplink = event.UplinkDelta + conn.Downlink = event.DownlinkDelta + conn.UplinkTotal += event.UplinkDelta + conn.DownlinkTotal += event.DownlinkDelta + } + case ConnectionEventClosed: + if event.Connection != nil { + conn := *event.Connection + conn.ClosedAt = event.ClosedAt + conn.Uplink = 0 + conn.Downlink = 0 + c.connectionMap[event.ID] = &conn + continue + } + if conn, ok := c.connectionMap[event.ID]; ok { + conn.ClosedAt = event.ClosedAt + conn.Uplink = 0 + conn.Downlink = 0 + } + } + } + + c.evictClosedConnections(time.Now().UnixMilli()) + c.input = c.input[:0] + for _, conn := range c.connectionMap { + c.input = append(c.input, *conn) + } + if c.filterApplied { + c.FilterState(c.filterState) + } else { + c.filtered = c.filtered[:0] + c.filtered = append(c.filtered, c.input...) + } +} + +func (c *Connections) evictClosedConnections(nowMilliseconds int64) { + for id, conn := range c.connectionMap { + if conn.ClosedAt == 0 { + continue + } + if nowMilliseconds-conn.ClosedAt > closedConnectionMaxAge { + delete(c.connectionMap, id) + } + } +} + +func (c *Connections) FilterState(state int32) { + c.filterApplied = true + c.filterState = state + c.filtered = c.filtered[:0] + switch state { + case ConnectionStateAll: + c.filtered = append(c.filtered, c.input...) + case ConnectionStateActive: + for _, connection := range c.input { + if connection.ClosedAt == 0 { + c.filtered = append(c.filtered, connection) + } + } + case ConnectionStateClosed: + for _, connection := range c.input { + if connection.ClosedAt != 0 { + c.filtered = append(c.filtered, connection) + } + } + } +} + +func (c *Connections) SortByDate() { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { + if x.CreatedAt < y.CreatedAt { + return 1 + } else if x.CreatedAt > y.CreatedAt { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) SortByTraffic() { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { + xTraffic := x.Uplink + x.Downlink + yTraffic := y.Uplink + y.Downlink + if xTraffic < yTraffic { + return 1 + } else if xTraffic > yTraffic { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) SortByTrafficTotal() { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { + xTraffic := x.UplinkTotal + x.DownlinkTotal + yTraffic := y.UplinkTotal + y.DownlinkTotal + if xTraffic < yTraffic { + return 1 + } else if xTraffic > yTraffic { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) Iterator() ConnectionIterator { + return newPtrIterator(c.filtered) +} + +type ProcessInfo struct { + ProcessID int64 + UserID int32 + UserName string + ProcessPath string + packageNames []string +} + +func (p *ProcessInfo) PackageNames() StringIterator { + return newIterator(p.packageNames) +} + +type Connection struct { + ID string + Inbound string + InboundType string + IPVersion int32 + Network string + Source string + Destination string + Domain string + Protocol string + User string + FromOutbound string + CreatedAt int64 + ClosedAt int64 + Uplink int64 + Downlink int64 + UplinkTotal int64 + DownlinkTotal int64 + Rule string + Outbound string + OutboundType string + chainList []string + ProcessInfo *ProcessInfo +} + +func (c *Connection) Chain() StringIterator { + return newIterator(c.chainList) +} + +func (c *Connection) DisplayDestination() string { + destination := M.ParseSocksaddr(c.Destination) + if destination.IsIP() && c.Domain != "" { + destination = M.Socksaddr{ + Fqdn: c.Domain, + Port: destination.Port, + } + return destination.String() + } + return c.Destination +} + +type ConnectionIterator interface { + Next() *Connection + HasNext() bool +} + +func statusMessageFromGRPC(status *daemon.Status) *StatusMessage { + if status == nil { + return nil + } + return &StatusMessage{ + Memory: int64(status.Memory), + Goroutines: status.Goroutines, + ConnectionsIn: status.ConnectionsIn, + ConnectionsOut: status.ConnectionsOut, + TrafficAvailable: status.TrafficAvailable, + Uplink: status.Uplink, + Downlink: status.Downlink, + UplinkTotal: status.UplinkTotal, + DownlinkTotal: status.DownlinkTotal, + } +} + +func outboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator { + if groups == nil || len(groups.Group) == 0 { + return newIterator([]*OutboundGroup{}) + } + var libboxGroups []*OutboundGroup + for _, g := range groups.Group { + libboxGroup := &OutboundGroup{ + Tag: g.Tag, + Type: g.Type, + Selectable: g.Selectable, + Selected: g.Selected, + IsExpand: g.IsExpand, + } + for _, item := range g.Items { + libboxGroup.itemList = append(libboxGroup.itemList, &OutboundGroupItem{ + Tag: item.Tag, + Type: item.Type, + URLTestTime: item.UrlTestTime, + URLTestDelay: item.UrlTestDelay, + }) + } + libboxGroups = append(libboxGroups, libboxGroup) + } + return newIterator(libboxGroups) +} + +func outboundGroupItemListFromGRPC(list *daemon.OutboundList) OutboundGroupItemIterator { + if list == nil || len(list.Outbounds) == 0 { + return newIterator([]*OutboundGroupItem{}) + } + var items []*OutboundGroupItem + for _, ob := range list.Outbounds { + items = append(items, &OutboundGroupItem{ + Tag: ob.Tag, + Type: ob.Type, + URLTestTime: ob.UrlTestTime, + URLTestDelay: ob.UrlTestDelay, + }) + } + return newIterator(items) +} + +func connectionFromGRPC(conn *daemon.Connection) Connection { + var processInfo *ProcessInfo + if conn.ProcessInfo != nil { + processInfo = &ProcessInfo{ + ProcessID: int64(conn.ProcessInfo.ProcessId), + UserID: conn.ProcessInfo.UserId, + UserName: conn.ProcessInfo.UserName, + ProcessPath: conn.ProcessInfo.ProcessPath, + packageNames: conn.ProcessInfo.PackageNames, + } + } + return Connection{ + ID: conn.Id, + Inbound: conn.Inbound, + InboundType: conn.InboundType, + IPVersion: conn.IpVersion, + Network: conn.Network, + Source: conn.Source, + Destination: conn.Destination, + Domain: conn.Domain, + Protocol: conn.Protocol, + User: conn.User, + FromOutbound: conn.FromOutbound, + CreatedAt: conn.CreatedAt, + ClosedAt: conn.ClosedAt, + Uplink: conn.Uplink, + Downlink: conn.Downlink, + UplinkTotal: conn.UplinkTotal, + DownlinkTotal: conn.DownlinkTotal, + Rule: conn.Rule, + Outbound: conn.Outbound, + OutboundType: conn.OutboundType, + chainList: conn.ChainList, + ProcessInfo: processInfo, + } +} + +func connectionEventFromGRPC(event *daemon.ConnectionEvent) *ConnectionEvent { + if event == nil { + return nil + } + libboxEvent := &ConnectionEvent{ + Type: int32(event.Type), + ID: event.Id, + UplinkDelta: event.UplinkDelta, + DownlinkDelta: event.DownlinkDelta, + ClosedAt: event.ClosedAt, + } + if event.Connection != nil { + conn := connectionFromGRPC(event.Connection) + libboxEvent.Connection = &conn + } + return libboxEvent +} + +func connectionEventsFromGRPC(events *daemon.ConnectionEvents) *ConnectionEvents { + if events == nil { + return nil + } + libboxEvents := &ConnectionEvents{ + Reset: events.Reset_, + } + for _, event := range events.Events { + if libboxEvent := connectionEventFromGRPC(event); libboxEvent != nil { + libboxEvents.events = append(libboxEvents.events, libboxEvent) + } + } + return libboxEvents +} + +func systemProxyStatusFromGRPC(status *daemon.SystemProxyStatus) *SystemProxyStatus { + if status == nil { + return nil + } + return &SystemProxyStatus{ + Available: status.Available, + Enabled: status.Enabled, + } +} + +func systemProxyStatusToGRPC(status *SystemProxyStatus) *daemon.SystemProxyStatus { + if status == nil { + return nil + } + return &daemon.SystemProxyStatus{ + Available: status.Available, + Enabled: status.Enabled, + } +} diff --git a/experimental/libbox/command_types_nq.go b/experimental/libbox/command_types_nq.go new file mode 100644 index 00000000..fc8957e2 --- /dev/null +++ b/experimental/libbox/command_types_nq.go @@ -0,0 +1,51 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type NetworkQualityProgress struct { + Phase int32 + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + ElapsedMs int64 + DownloadCapacityAccuracy int32 + UploadCapacityAccuracy int32 + DownloadRPMAccuracy int32 + UploadRPMAccuracy int32 +} + +type NetworkQualityResult struct { + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + DownloadCapacityAccuracy int32 + UploadCapacityAccuracy int32 + DownloadRPMAccuracy int32 + UploadRPMAccuracy int32 +} + +type NetworkQualityTestHandler interface { + OnProgress(progress *NetworkQualityProgress) + OnResult(result *NetworkQualityResult) + OnError(message string) +} + +func networkQualityProgressFromGRPC(event *daemon.NetworkQualityTestProgress) *NetworkQualityProgress { + return &NetworkQualityProgress{ + Phase: event.Phase, + DownloadCapacity: event.DownloadCapacity, + UploadCapacity: event.UploadCapacity, + DownloadRPM: event.DownloadRPM, + UploadRPM: event.UploadRPM, + IdleLatencyMs: event.IdleLatencyMs, + ElapsedMs: event.ElapsedMs, + DownloadCapacityAccuracy: event.DownloadCapacityAccuracy, + UploadCapacityAccuracy: event.UploadCapacityAccuracy, + DownloadRPMAccuracy: event.DownloadRPMAccuracy, + UploadRPMAccuracy: event.UploadRPMAccuracy, + } +} diff --git a/experimental/libbox/command_types_stun.go b/experimental/libbox/command_types_stun.go new file mode 100644 index 00000000..22846c32 --- /dev/null +++ b/experimental/libbox/command_types_stun.go @@ -0,0 +1,35 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type STUNTestProgress struct { + Phase int32 + ExternalAddr string + LatencyMs int32 + NATMapping int32 + NATFiltering int32 +} + +type STUNTestResult struct { + ExternalAddr string + LatencyMs int32 + NATMapping int32 + NATFiltering int32 + NATTypeSupported bool +} + +type STUNTestHandler interface { + OnProgress(progress *STUNTestProgress) + OnResult(result *STUNTestResult) + OnError(message string) +} + +func stunTestProgressFromGRPC(event *daemon.STUNTestProgress) *STUNTestProgress { + return &STUNTestProgress{ + Phase: event.Phase, + ExternalAddr: event.ExternalAddr, + LatencyMs: event.LatencyMs, + NATMapping: event.NatMapping, + NATFiltering: event.NatFiltering, + } +} diff --git a/experimental/libbox/command_types_tailscale.go b/experimental/libbox/command_types_tailscale.go new file mode 100644 index 00000000..dc17639d --- /dev/null +++ b/experimental/libbox/command_types_tailscale.go @@ -0,0 +1,132 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type TailscaleStatusUpdate struct { + endpoints []*TailscaleEndpointStatus +} + +func (u *TailscaleStatusUpdate) Endpoints() TailscaleEndpointStatusIterator { + return newIterator(u.endpoints) +} + +type TailscaleEndpointStatusIterator interface { + Next() *TailscaleEndpointStatus + HasNext() bool +} + +type TailscaleEndpointStatus struct { + EndpointTag string + BackendState string + AuthURL string + NetworkName string + MagicDNSSuffix string + Self *TailscalePeer + userGroups []*TailscaleUserGroup +} + +func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator { + return newIterator(s.userGroups) +} + +type TailscaleUserGroupIterator interface { + Next() *TailscaleUserGroup + HasNext() bool +} + +type TailscaleUserGroup struct { + UserID int64 + LoginName string + DisplayName string + ProfilePicURL string + peers []*TailscalePeer +} + +func (g *TailscaleUserGroup) Peers() TailscalePeerIterator { + return newIterator(g.peers) +} + +type TailscalePeerIterator interface { + Next() *TailscalePeer + HasNext() bool +} + +type TailscalePeer struct { + HostName string + DNSName string + OS string + tailscaleIPs []string + Online bool + ExitNode bool + ExitNodeOption bool + Active bool + RxBytes int64 + TxBytes int64 + KeyExpiry int64 +} + +func (p *TailscalePeer) TailscaleIPs() StringIterator { + return newIterator(p.tailscaleIPs) +} + +type TailscaleStatusHandler interface { + OnStatusUpdate(status *TailscaleStatusUpdate) + OnError(message string) +} + +func tailscaleStatusUpdateFromGRPC(update *daemon.TailscaleStatusUpdate) *TailscaleStatusUpdate { + endpoints := make([]*TailscaleEndpointStatus, len(update.Endpoints)) + for i, endpoint := range update.Endpoints { + endpoints[i] = tailscaleEndpointStatusFromGRPC(endpoint) + } + return &TailscaleStatusUpdate{endpoints: endpoints} +} + +func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *TailscaleEndpointStatus { + userGroups := make([]*TailscaleUserGroup, len(status.UserGroups)) + for i, group := range status.UserGroups { + userGroups[i] = tailscaleUserGroupFromGRPC(group) + } + result := &TailscaleEndpointStatus{ + EndpointTag: status.EndpointTag, + BackendState: status.BackendState, + AuthURL: status.AuthURL, + NetworkName: status.NetworkName, + MagicDNSSuffix: status.MagicDNSSuffix, + userGroups: userGroups, + } + if status.Self != nil { + result.Self = tailscalePeerFromGRPC(status.Self) + } + return result +} + +func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUserGroup { + peers := make([]*TailscalePeer, len(group.Peers)) + for i, peer := range group.Peers { + peers[i] = tailscalePeerFromGRPC(peer) + } + return &TailscaleUserGroup{ + UserID: group.UserID, + LoginName: group.LoginName, + DisplayName: group.DisplayName, + ProfilePicURL: group.ProfilePicURL, + peers: peers, + } +} + +func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer { + return &TailscalePeer{ + HostName: peer.HostName, + DNSName: peer.DnsName, + OS: peer.Os, + tailscaleIPs: peer.TailscaleIPs, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + KeyExpiry: peer.KeyExpiry, + } +} diff --git a/experimental/libbox/command_types_tailscale_ping.go b/experimental/libbox/command_types_tailscale_ping.go new file mode 100644 index 00000000..666789d0 --- /dev/null +++ b/experimental/libbox/command_types_tailscale_ping.go @@ -0,0 +1,28 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type TailscalePingResult struct { + LatencyMs float64 + IsDirect bool + Endpoint string + DERPRegionID int32 + DERPRegionCode string + Error string +} + +type TailscalePingHandler interface { + OnPingResult(result *TailscalePingResult) + OnError(message string) +} + +func tailscalePingResultFromGRPC(response *daemon.TailscalePingResponse) *TailscalePingResult { + return &TailscalePingResult{ + LatencyMs: response.LatencyMs, + IsDirect: response.IsDirect, + Endpoint: response.Endpoint, + DERPRegionID: response.DerpRegionID, + DERPRegionCode: response.DerpRegionCode, + Error: response.Error, + } +} diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go new file mode 100644 index 00000000..4b21e505 --- /dev/null +++ b/experimental/libbox/config.go @@ -0,0 +1,222 @@ +package libbox + +import ( + "bytes" + "context" + "os" + + box "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/service/oomkiller" + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" +) + +var sOOMReporter oomkiller.OOMReporter + +func baseContext(platformInterface PlatformInterface) context.Context { + dnsRegistry := include.DNSTransportRegistry() + if platformInterface != nil { + if localTransport := platformInterface.LocalDNSTransport(); localTransport != nil { + dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, func(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { + return newPlatformTransport(localTransport, tag, options), nil + }) + } + } + ctx := context.Background() + ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) + if sOOMReporter != nil { + ctx = service.ContextWith[oomkiller.OOMReporter](ctx, sOOMReporter) + } + return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry()) +} + +func parseConfig(ctx context.Context, configContent string) (option.Options, error) { + options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent)) + if err != nil { + return option.Options{}, E.Cause(err, "decode config") + } + return options, nil +} + +func CheckConfig(configContent string) error { + ctx := baseContext(nil) + options, err := parseConfig(ctx, configContent) + if err != nil { + return err + } + ctx, cancel := context.WithCancel(ctx) + defer cancel() + ctx = service.ContextWith[adapter.PlatformInterface](ctx, (*platformInterfaceStub)(nil)) + instance, err := box.New(box.Options{ + Context: ctx, + Options: options, + }) + if err == nil { + instance.Close() + } + return err +} + +type platformInterfaceStub struct{} + +func (s *platformInterfaceStub) Initialize(networkManager adapter.NetworkManager) error { + return nil +} + +func (s *platformInterfaceStub) UsePlatformAutoDetectInterfaceControl() bool { + return true +} + +func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error { + return nil +} + +func (s *platformInterfaceStub) UsePlatformInterface() bool { + return false +} + +func (s *platformInterfaceStub) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { + return nil, os.ErrInvalid +} + +func (s *platformInterfaceStub) UsePlatformDefaultInterfaceMonitor() bool { + return true +} + +func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { + return (*interfaceMonitorStub)(nil) +} + +func (s *platformInterfaceStub) UsePlatformNetworkInterfaces() bool { + return false +} + +func (s *platformInterfaceStub) NetworkInterfaces() ([]adapter.NetworkInterface, error) { + return nil, os.ErrInvalid +} + +func (s *platformInterfaceStub) UnderNetworkExtension() bool { + return false +} + +func (s *platformInterfaceStub) NetworkExtensionIncludeAllNetworks() bool { + return false +} + +func (s *platformInterfaceStub) ClearDNSCache() { +} + +func (s *platformInterfaceStub) RequestPermissionForWIFIState() error { + return nil +} + +func (s *platformInterfaceStub) UsePlatformWIFIMonitor() bool { + return false +} + +func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState { + return adapter.WIFIState{} +} + +func (s *platformInterfaceStub) SystemCertificates() []string { + return nil +} + +func (s *platformInterfaceStub) UsePlatformConnectionOwnerFinder() bool { + return false +} + +func (s *platformInterfaceStub) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) { + return nil, os.ErrInvalid +} + +func (s *platformInterfaceStub) UsePlatformNotification() bool { + return false +} + +func (s *platformInterfaceStub) SendNotification(notification *adapter.Notification) error { + return nil +} + +func (s *platformInterfaceStub) UsePlatformNeighborResolver() bool { + return false +} + +func (s *platformInterfaceStub) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return os.ErrInvalid +} + +func (s *platformInterfaceStub) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return nil +} + +func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool { + return false +} + +func (s *platformInterfaceStub) LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions] { + return nil +} + +type interfaceMonitorStub struct{} + +func (s *interfaceMonitorStub) Start() error { + return os.ErrInvalid +} + +func (s *interfaceMonitorStub) Close() error { + return os.ErrInvalid +} + +func (s *interfaceMonitorStub) DefaultInterface() *control.Interface { + return nil +} + +func (s *interfaceMonitorStub) OverrideAndroidVPN() bool { + return false +} + +func (s *interfaceMonitorStub) AndroidVPNEnabled() bool { + return false +} + +func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] { + return nil +} + +func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { +} + +func (s *interfaceMonitorStub) RegisterMyInterface(interfaceName string) { +} + +func (s *interfaceMonitorStub) MyInterface() string { + return "" +} + +func FormatConfig(configContent string) (*StringBox, error) { + options, err := parseConfig(baseContext(nil), configContent) + if err != nil { + return nil, err + } + var buffer bytes.Buffer + encoder := json.NewEncoder(&buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(options) + if err != nil { + return nil, err + } + return wrapString(buffer.String()), nil +} diff --git a/experimental/libbox/connection_owner_darwin.go b/experimental/libbox/connection_owner_darwin.go new file mode 100644 index 00000000..20220106 --- /dev/null +++ b/experimental/libbox/connection_owner_darwin.go @@ -0,0 +1,57 @@ +package libbox + +import ( + "net/netip" + "os/user" + "syscall" + + "github.com/sagernet/sing-box/common/process" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) { + source, err := parseConnectionOwnerAddrPort(sourceAddress, sourcePort) + if err != nil { + return nil, E.Cause(err, "parse source") + } + destination, err := parseConnectionOwnerAddrPort(destinationAddress, destinationPort) + if err != nil { + return nil, E.Cause(err, "parse destination") + } + var network string + switch ipProtocol { + case syscall.IPPROTO_TCP: + network = "tcp" + case syscall.IPPROTO_UDP: + network = "udp" + default: + return nil, E.New("unknown protocol: ", ipProtocol) + } + owner, err := process.FindDarwinConnectionOwner(network, source, destination) + if err != nil { + return nil, err + } + result := &ConnectionOwner{ + UserId: owner.UserId, + ProcessPath: owner.ProcessPath, + } + if owner.UserId != -1 && owner.UserName == "" { + osUser, _ := user.LookupId(F.ToString(owner.UserId)) + if osUser != nil { + result.UserName = osUser.Username + } + } + return result, nil +} + +func parseConnectionOwnerAddrPort(address string, port int32) (netip.AddrPort, error) { + if port < 0 || port > 65535 { + return netip.AddrPort{}, E.New("invalid port: ", port) + } + addr, err := netip.ParseAddr(address) + if err != nil { + return netip.AddrPort{}, err + } + return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil +} diff --git a/experimental/libbox/debug.go b/experimental/libbox/debug.go new file mode 100644 index 00000000..75942976 --- /dev/null +++ b/experimental/libbox/debug.go @@ -0,0 +1,12 @@ +package libbox + +import ( + "time" + "unsafe" +) + +func TriggerGoPanic() { + time.AfterFunc(200*time.Millisecond, func() { + *(*int)(unsafe.Pointer(uintptr(0))) = 0 + }) +} diff --git a/experimental/libbox/deprecated.go b/experimental/libbox/deprecated.go new file mode 100644 index 00000000..0c2f8d8a --- /dev/null +++ b/experimental/libbox/deprecated.go @@ -0,0 +1,33 @@ +package libbox + +import ( + "github.com/sagernet/sing-box/experimental/deprecated" +) + +var _ = deprecated.Note(DeprecatedNote{}) + +type DeprecatedNote struct { + Name string + Description string + DeprecatedVersion string + ScheduledVersion string + EnvName string + MigrationLink string +} + +func (n DeprecatedNote) Impending() bool { + return deprecated.Note(n).Impending() +} + +func (n DeprecatedNote) Message() string { + return deprecated.Note(n).Message() +} + +func (n DeprecatedNote) MessageWithLink() string { + return deprecated.Note(n).MessageWithLink() +} + +type DeprecatedNoteIterator interface { + HasNext() bool + Next() *DeprecatedNote +} diff --git a/experimental/libbox/dns.go b/experimental/libbox/dns.go new file mode 100644 index 00000000..b7b3b0f6 --- /dev/null +++ b/experimental/libbox/dns.go @@ -0,0 +1,150 @@ +package libbox + +import ( + "context" + "net/netip" + "strings" + "syscall" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/task" + + mDNS "github.com/miekg/dns" +) + +type LocalDNSTransport interface { + Raw() bool + Lookup(ctx *ExchangeContext, network string, domain string) error + Exchange(ctx *ExchangeContext, message []byte) error +} + +var _ adapter.DNSTransport = (*platformTransport)(nil) + +type platformTransport struct { + dns.TransportAdapter + iif LocalDNSTransport +} + +func newPlatformTransport(iif LocalDNSTransport, tag string, options option.LocalDNSServerOptions) *platformTransport { + return &platformTransport{ + TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), + iif: iif, + } +} + +func (p *platformTransport) Start(stage adapter.StartStage) error { + return nil +} + +func (p *platformTransport) Close() error { + return nil +} + +func (p *platformTransport) Reset() { +} + +func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + response := &ExchangeContext{ + context: ctx, + } + if p.iif.Raw() { + messageBytes, err := message.Pack() + if err != nil { + return nil, err + } + var responseMessage *mDNS.Msg + var group task.Group + group.Append0(func(ctx context.Context) error { + err = p.iif.Exchange(response, messageBytes) + if err != nil { + return err + } + if response.error != nil { + return response.error + } + responseMessage = &response.message + return nil + }) + err = group.Run(ctx) + if err != nil { + return nil, err + } + return responseMessage, nil + } else { + question := message.Question[0] + var network string + switch question.Qtype { + case mDNS.TypeA: + network = "ip4" + case mDNS.TypeAAAA: + network = "ip6" + default: + return nil, E.New("only IP queries are supported by current version of Android") + } + var responseAddrs []netip.Addr + var group task.Group + group.Append0(func(ctx context.Context) error { + err := p.iif.Lookup(response, network, question.Name) + if err != nil { + return err + } + if response.error != nil { + return response.error + } + responseAddrs = response.addresses + return nil + }) + err := group.Run(ctx) + if err != nil { + return nil, err + } + return dns.FixedResponse(message.Id, question, responseAddrs, C.DefaultDNSTTL), nil + } +} + +type Func interface { + Invoke() error +} + +type ExchangeContext struct { + context context.Context + message mDNS.Msg + addresses []netip.Addr + error error +} + +func (c *ExchangeContext) OnCancel(callback Func) { + go func() { + <-c.context.Done() + callback.Invoke() + }() +} + +func (c *ExchangeContext) Success(result string) { + c.addresses = common.Map(common.Filter(strings.Split(result, "\n"), func(it string) bool { + return !common.IsEmpty(it) + }), func(it string) netip.Addr { + return M.ParseSocksaddrHostPort(it, 0).Unwrap().Addr + }) +} + +func (c *ExchangeContext) RawSuccess(result []byte) { + err := c.message.Unpack(result) + if err != nil { + c.error = E.Cause(err, "parse response") + } +} + +func (c *ExchangeContext) ErrorCode(code int32) { + c.error = dns.RcodeError(code) +} + +func (c *ExchangeContext) ErrnoCode(code int32) { + c.error = syscall.Errno(code) +} diff --git a/experimental/libbox/fdroid.go b/experimental/libbox/fdroid.go new file mode 100644 index 00000000..d574ffd8 --- /dev/null +++ b/experimental/libbox/fdroid.go @@ -0,0 +1,493 @@ +package libbox + +import ( + "archive/zip" + "bytes" + "crypto/tls" + "encoding/json" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + E "github.com/sagernet/sing/common/exceptions" +) + +const fdroidUserAgent = "F-Droid 1.21.1" + +type FDroidUpdateInfo struct { + VersionCode int32 + VersionName string + DownloadURL string + FileSize int64 + FileSHA256 string +} + +type FDroidPingResult struct { + URL string + LatencyMs int32 + Error string +} + +type FDroidPingResultIterator interface { + Len() int32 + HasNext() bool + Next() *FDroidPingResult +} + +type fdroidAPIResponse struct { + PackageName string `json:"packageName"` + SuggestedVersionCode int32 `json:"suggestedVersionCode"` + Packages []fdroidAPIPackage `json:"packages"` +} + +type fdroidAPIPackage struct { + VersionName string `json:"versionName"` + VersionCode int32 `json:"versionCode"` +} + +type fdroidEntry struct { + Timestamp int64 `json:"timestamp"` + Version int `json:"version"` + Index fdroidEntryFile `json:"index"` + Diffs map[string]fdroidEntryFile `json:"diffs"` +} + +type fdroidEntryFile struct { + Name string `json:"name"` + SHA256 string `json:"sha256"` + Size int64 `json:"size"` + NumPackages int `json:"numPackages"` +} + +type fdroidIndexV2 struct { + Packages map[string]fdroidV2Package `json:"packages"` +} + +type fdroidV2Package struct { + Versions map[string]fdroidV2Version `json:"versions"` +} + +type fdroidV2Version struct { + Manifest fdroidV2Manifest `json:"manifest"` + File fdroidV2File `json:"file"` +} + +type fdroidV2Manifest struct { + VersionCode int32 `json:"versionCode"` + VersionName string `json:"versionName"` +} + +type fdroidV2File struct { + Name string `json:"name"` + SHA256 string `json:"sha256"` + Size int64 `json:"size"` +} + +type fdroidIndexV1 struct { + Packages map[string][]fdroidV1Package `json:"packages"` +} + +type fdroidV1Package struct { + VersionCode int32 `json:"versionCode"` + VersionName string `json:"versionName"` + ApkName string `json:"apkName"` + Size int64 `json:"size"` + Hash string `json:"hash"` + HashType string `json:"hashType"` +} + +type fdroidCache struct { + MirrorURL string `json:"mirrorURL"` + Timestamp int64 `json:"timestamp"` + ETag string `json:"etag"` + IsV1 bool `json:"isV1,omitempty"` +} + +func CheckFDroidUpdate(mirrorURL, packageName string, currentVersionCode int32, cachePath string) (*FDroidUpdateInfo, error) { + mirrorURL = strings.TrimRight(mirrorURL, "/") + if strings.Contains(mirrorURL, "f-droid.org") { + return checkFDroidAPI(mirrorURL, packageName, currentVersionCode) + } + client := newFDroidHTTPClient() + defer client.CloseIdleConnections() + cache := loadFDroidCache(cachePath, mirrorURL) + if cache != nil && cache.IsV1 { + return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, cache) + } + return checkFDroidV2(client, mirrorURL, packageName, currentVersionCode, cachePath, cache) +} + +func PingFDroidMirrors(mirrorURLs string) (FDroidPingResultIterator, error) { + urls := strings.Split(mirrorURLs, ",") + results := make([]*FDroidPingResult, len(urls)) + var waitGroup sync.WaitGroup + for i, rawURL := range urls { + waitGroup.Add(1) + go func(index int, target string) { + defer waitGroup.Done() + target = strings.TrimSpace(target) + result := &FDroidPingResult{URL: target} + latency, err := pingTLS(target) + if err != nil { + result.LatencyMs = -1 + result.Error = err.Error() + } else { + result.LatencyMs = int32(latency.Milliseconds()) + } + results[index] = result + }(i, rawURL) + } + waitGroup.Wait() + sort.Slice(results, func(i, j int) bool { + if results[i].LatencyMs < 0 { + return false + } + if results[j].LatencyMs < 0 { + return true + } + return results[i].LatencyMs < results[j].LatencyMs + }) + return newIterator(results), nil +} + +func PingFDroidMirror(mirrorURL string) *FDroidPingResult { + mirrorURL = strings.TrimSpace(mirrorURL) + result := &FDroidPingResult{URL: mirrorURL} + latency, err := pingTLS(mirrorURL) + if err != nil { + result.LatencyMs = -1 + result.Error = err.Error() + } else { + result.LatencyMs = int32(latency.Milliseconds()) + } + return result +} + +func newFDroidHTTPClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + } +} + +func newFDroidRequest(requestURL string) (*http.Request, error) { + request, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + request.Header.Set("User-Agent", fdroidUserAgent) + return request, nil +} + +func checkFDroidAPI(mirrorURL, packageName string, currentVersionCode int32) (*FDroidUpdateInfo, error) { + client := newFDroidHTTPClient() + defer client.CloseIdleConnections() + + apiURL := "https://f-droid.org/api/v1/packages/" + packageName + request, err := newFDroidRequest(apiURL) + if err != nil { + return nil, err + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", response.Status) + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var apiResponse fdroidAPIResponse + err = json.Unmarshal(body, &apiResponse) + if err != nil { + return nil, err + } + + var bestCode int32 + var bestName string + for _, pkg := range apiResponse.Packages { + if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode { + bestCode = pkg.VersionCode + bestName = pkg.VersionName + } + } + + if bestCode == 0 { + return nil, nil + } + + return &FDroidUpdateInfo{ + VersionCode: bestCode, + VersionName: bestName, + DownloadURL: "https://f-droid.org/repo/" + packageName + "_" + strconv.FormatInt(int64(bestCode), 10) + ".apk", + }, nil +} + +func checkFDroidV2(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) { + entryURL := mirrorURL + "/entry.jar" + request, err := newFDroidRequest(entryURL) + if err != nil { + return nil, err + } + if cache != nil && cache.ETag != "" { + request.Header.Set("If-None-Match", cache.ETag) + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode == http.StatusNotModified { + return nil, nil + } + if response.StatusCode == http.StatusNotFound { + writeFDroidCache(cachePath, mirrorURL, 0, "", true) + return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, nil) + } + if response.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", response.Status, ": ", entryURL) + } + + jarData, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + etag := response.Header.Get("ETag") + + var entry fdroidEntry + err = readJSONFromJar(jarData, "entry.json", &entry) + if err != nil { + return nil, E.Cause(err, "read entry.jar") + } + + if entry.Timestamp == 0 { + return nil, E.New("entry.json not found in entry.jar") + } + + if cache != nil && cache.Timestamp == entry.Timestamp { + writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false) + return nil, nil + } + + var indexURL string + if cache != nil { + cachedTimestamp := strconv.FormatInt(cache.Timestamp, 10) + if diff, ok := entry.Diffs[cachedTimestamp]; ok { + indexURL = mirrorURL + "/" + diff.Name + } + } + if indexURL == "" { + indexURL = mirrorURL + "/" + entry.Index.Name + } + + indexRequest, err := newFDroidRequest(indexURL) + if err != nil { + return nil, err + } + + indexResponse, err := client.Do(indexRequest) + if err != nil { + return nil, err + } + defer indexResponse.Body.Close() + + if indexResponse.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", indexResponse.Status, ": ", indexURL) + } + + indexData, err := io.ReadAll(indexResponse.Body) + if err != nil { + return nil, err + } + + var index fdroidIndexV2 + err = json.Unmarshal(indexData, &index) + if err != nil { + return nil, err + } + + writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false) + + pkg, ok := index.Packages[packageName] + if !ok { + return nil, nil + } + + var bestCode int32 + var bestVersion fdroidV2Version + for _, version := range pkg.Versions { + if version.Manifest.VersionCode > currentVersionCode && version.Manifest.VersionCode > bestCode { + bestCode = version.Manifest.VersionCode + bestVersion = version + } + } + + if bestCode == 0 { + return nil, nil + } + + return &FDroidUpdateInfo{ + VersionCode: bestCode, + VersionName: bestVersion.Manifest.VersionName, + DownloadURL: mirrorURL + "/" + bestVersion.File.Name, + FileSize: bestVersion.File.Size, + FileSHA256: bestVersion.File.SHA256, + }, nil +} + +func checkFDroidV1(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) { + indexURL := mirrorURL + "/index-v1.jar" + + request, err := newFDroidRequest(indexURL) + if err != nil { + return nil, err + } + if cache != nil && cache.ETag != "" { + request.Header.Set("If-None-Match", cache.ETag) + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode == http.StatusNotModified { + return nil, nil + } + if response.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", response.Status, ": ", indexURL) + } + + jarData, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + etag := response.Header.Get("ETag") + + var index fdroidIndexV1 + err = readJSONFromJar(jarData, "index-v1.json", &index) + if err != nil { + return nil, E.Cause(err, "read index-v1.jar") + } + + writeFDroidCache(cachePath, mirrorURL, 0, etag, true) + + packages, ok := index.Packages[packageName] + if !ok { + return nil, nil + } + + var bestCode int32 + var bestPackage fdroidV1Package + for _, pkg := range packages { + if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode { + bestCode = pkg.VersionCode + bestPackage = pkg + } + } + + if bestCode == 0 { + return nil, nil + } + + return &FDroidUpdateInfo{ + VersionCode: bestCode, + VersionName: bestPackage.VersionName, + DownloadURL: mirrorURL + "/" + bestPackage.ApkName, + FileSize: bestPackage.Size, + FileSHA256: bestPackage.Hash, + }, nil +} + +func readJSONFromJar(jarData []byte, fileName string, destination any) error { + zipReader, err := zip.NewReader(bytes.NewReader(jarData), int64(len(jarData))) + if err != nil { + return err + } + for _, file := range zipReader.File { + if file.Name != fileName { + continue + } + reader, err := file.Open() + if err != nil { + return err + } + data, err := io.ReadAll(reader) + reader.Close() + if err != nil { + return err + } + return json.Unmarshal(data, destination) + } + return nil +} + +func pingTLS(mirrorURL string) (time.Duration, error) { + parsed, err := url.Parse(mirrorURL) + if err != nil { + return 0, err + } + host := parsed.Host + if !strings.Contains(host, ":") { + host = host + ":443" + } + + dialer := &net.Dialer{Timeout: 5 * time.Second} + start := time.Now() + conn, err := tls.DialWithDialer(dialer, "tcp", host, &tls.Config{}) + if err != nil { + return 0, err + } + latency := time.Since(start) + conn.Close() + return latency, nil +} + +func loadFDroidCache(cachePath, mirrorURL string) *fdroidCache { + cacheFile := filepath.Join(cachePath, "fdroid_cache.json") + data, err := os.ReadFile(cacheFile) + if err != nil { + return nil + } + var cache fdroidCache + err = json.Unmarshal(data, &cache) + if err != nil { + return nil + } + if cache.MirrorURL != mirrorURL { + return nil + } + return &cache +} + +func writeFDroidCache(cachePath, mirrorURL string, timestamp int64, etag string, isV1 bool) { + cache := fdroidCache{ + MirrorURL: mirrorURL, + Timestamp: timestamp, + ETag: etag, + IsV1: isV1, + } + data, err := json.Marshal(cache) + if err != nil { + return + } + os.MkdirAll(cachePath, 0o755) + os.WriteFile(filepath.Join(cachePath, "fdroid_cache.json"), data, 0o644) +} diff --git a/experimental/libbox/fdroid_mirrors.go b/experimental/libbox/fdroid_mirrors.go new file mode 100644 index 00000000..4ca82555 --- /dev/null +++ b/experimental/libbox/fdroid_mirrors.go @@ -0,0 +1,92 @@ +package libbox + +type FDroidMirror struct { + URL string + Country string + Name string +} + +type FDroidMirrorIterator interface { + Len() int32 + HasNext() bool + Next() *FDroidMirror +} + +var builtinFDroidMirrors = []FDroidMirror{ + // Official + {URL: "https://f-droid.org/repo", Country: "Official", Name: "f-droid.org"}, + {URL: "https://cloudflare.f-droid.org/repo", Country: "Official", Name: "Cloudflare CDN"}, + + // China + {URL: "https://mirrors.tuna.tsinghua.edu.cn/fdroid/repo", Country: "China", Name: "Tsinghua TUNA"}, + {URL: "https://mirrors.nju.edu.cn/fdroid/repo", Country: "China", Name: "Nanjing University"}, + {URL: "https://mirror.iscas.ac.cn/fdroid/repo", Country: "China", Name: "ISCAS"}, + {URL: "https://mirror.nyist.edu.cn/fdroid/repo", Country: "China", Name: "NYIST"}, + {URL: "https://mirrors.cqupt.edu.cn/fdroid/repo", Country: "China", Name: "CQUPT"}, + {URL: "https://mirrors.shanghaitech.edu.cn/fdroid/repo", Country: "China", Name: "ShanghaiTech"}, + + // India + {URL: "https://mirror.hyd.albony.in/fdroid/repo", Country: "India", Name: "Albony Hyderabad"}, + {URL: "https://mirror.del2.albony.in/fdroid/repo", Country: "India", Name: "Albony Delhi"}, + + // Taiwan + {URL: "https://mirror.ossplanet.net/fdroid/repo", Country: "Taiwan", Name: "OSSPlanet"}, + + // France + {URL: "https://fdroid.tetaneutral.net/fdroid/repo", Country: "France", Name: "tetaneutral.net"}, + {URL: "https://mirror.freedif.org/fdroid/repo", Country: "France", Name: "FreeDif"}, + + // Germany + {URL: "https://ftp.fau.de/fdroid/repo", Country: "Germany", Name: "FAU Erlangen"}, + {URL: "https://ftp.agdsn.de/fdroid/repo", Country: "Germany", Name: "AGDSN Dresden"}, + {URL: "https://ftp.gwdg.de/pub/android/fdroid/repo", Country: "Germany", Name: "GWDG"}, + {URL: "https://mirror.level66.network/fdroid/repo", Country: "Germany", Name: "Level66"}, + {URL: "https://mirror.mci-1.serverforge.org/fdroid/repo", Country: "Germany", Name: "ServerForge"}, + + // Netherlands + {URL: "https://ftp.snt.utwente.nl/pub/software/fdroid/repo", Country: "Netherlands", Name: "University of Twente"}, + + // Sweden + {URL: "https://ftp.lysator.liu.se/pub/fdroid/repo", Country: "Sweden", Name: "Lysator"}, + + // Denmark + {URL: "https://mirrors.dotsrc.org/fdroid/repo", Country: "Denmark", Name: "dotsrc.org"}, + + // Austria + {URL: "https://mirror.kumi.systems/fdroid/repo", Country: "Austria", Name: "Kumi Systems"}, + + // Switzerland + {URL: "https://mirror.init7.net/fdroid/repo", Country: "Switzerland", Name: "Init7"}, + + // Romania + {URL: "https://mirrors.hostico.ro/fdroid/repo", Country: "Romania", Name: "Hostico"}, + {URL: "https://mirrors.chroot.ro/fdroid/repo", Country: "Romania", Name: "Chroot"}, + {URL: "https://ftp.lug.ro/fdroid/repo", Country: "Romania", Name: "LUG Romania"}, + + // US + {URL: "https://plug-mirror.rcac.purdue.edu/fdroid/repo", Country: "US", Name: "Purdue"}, + {URL: "https://mirror.fcix.net/fdroid/repo", Country: "US", Name: "FCIX"}, + {URL: "https://opencolo.mm.fcix.net/fdroid/repo", Country: "US", Name: "OpenColo"}, + {URL: "https://forksystems.mm.fcix.net/fdroid/repo", Country: "US", Name: "Fork Systems"}, + {URL: "https://southfront.mm.fcix.net/fdroid/repo", Country: "US", Name: "South Front"}, + {URL: "https://ziply.mm.fcix.net/fdroid/repo", Country: "US", Name: "Ziply"}, + + // Canada + {URL: "https://mirror.quantum5.ca/fdroid/repo", Country: "Canada", Name: "Quantum5"}, + + // Australia + {URL: "https://mirror.aarnet.edu.au/fdroid/repo", Country: "Australia", Name: "AARNet"}, + + // Other + {URL: "https://mirror.cyberbits.eu/fdroid/repo", Country: "Europe", Name: "Cyberbits EU"}, + {URL: "https://mirror.eu.ossplanet.net/fdroid/repo", Country: "Europe", Name: "OSSPlanet EU"}, + {URL: "https://mirror.cyberbits.asia/fdroid/repo", Country: "Asia", Name: "Cyberbits Asia"}, + {URL: "https://mirrors.jevincanders.net/fdroid/repo", Country: "US", Name: "Jevincanders"}, + {URL: "https://mirrors.komogoto.com/fdroid/repo", Country: "US", Name: "Komogoto"}, + {URL: "https://fdroid.rasp.sh/fdroid/repo", Country: "Europe", Name: "rasp.sh"}, + {URL: "https://mirror.gofoss.xyz/fdroid/repo", Country: "Europe", Name: "GoFOSS"}, +} + +func GetFDroidMirrors() FDroidMirrorIterator { + return newPtrIterator(builtinFDroidMirrors) +} diff --git a/experimental/libbox/ffi.json b/experimental/libbox/ffi.json new file mode 100644 index 00000000..81fae27d --- /dev/null +++ b/experimental/libbox/ffi.json @@ -0,0 +1,257 @@ +{ + "version": 1, + "variables": { + "VERSION": "$(go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)", + "WORKSPACE_ROOT": "../../..", + "DEPLOY_ANDROID": "${WORKSPACE_ROOT}/sing-box-for-android/app/libs", + "DEPLOY_APPLE": "${WORKSPACE_ROOT}/sing-box-for-apple", + "DEPLOY_WINDOWS": "${WORKSPACE_ROOT}/sing-box-for-windows/local-packages" + }, + "packages": [ + { + "id": "libbox", + "path": ".", + "java_package": "io.nekohasekai.libbox", + "csharp_namespace": "SagerNet", + "csharp_entrypoint": "Libbox", + "apple_prefix": "Libbox" + } + ], + "builds": [ + { + "id": "android-main", + "packages": ["libbox"], + "default": { + "tags": [ + "with_gvisor", + "with_quic", + "with_wireguard", + "with_utls", + "with_naive_outbound", + "with_clash_api", + "badlinkname", + "tfogo_checklinkname0", + "with_tailscale", + "ts_omit_logtail", + "ts_omit_ssh", + "ts_omit_drive", + "ts_omit_taildrop", + "ts_omit_webclient", + "ts_omit_doctor", + "ts_omit_capture", + "ts_omit_kube", + "ts_omit_aws", + "ts_omit_synology", + "ts_omit_bird" + ], + "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", + "trimpath": true + } + }, + { + "id": "android-legacy", + "packages": ["libbox"], + "default": { + "tags": [ + "with_gvisor", + "with_quic", + "with_wireguard", + "with_utls", + "with_clash_api", + "badlinkname", + "tfogo_checklinkname0", + "with_tailscale", + "ts_omit_logtail", + "ts_omit_ssh", + "ts_omit_drive", + "ts_omit_taildrop", + "ts_omit_webclient", + "ts_omit_doctor", + "ts_omit_capture", + "ts_omit_kube", + "ts_omit_aws", + "ts_omit_synology", + "ts_omit_bird" + ], + "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", + "trimpath": true + } + }, + { + "id": "apple", + "packages": ["libbox"], + "default": { + "tags": [ + "with_gvisor", + "with_quic", + "with_wireguard", + "with_utls", + "with_naive_outbound", + "with_clash_api", + "badlinkname", + "tfogo_checklinkname0", + "with_dhcp", + "grpcnotrace", + "with_tailscale", + "ts_omit_logtail", + "ts_omit_ssh", + "ts_omit_drive", + "ts_omit_taildrop", + "ts_omit_webclient", + "ts_omit_doctor", + "ts_omit_capture", + "ts_omit_kube", + "ts_omit_aws", + "ts_omit_synology", + "ts_omit_bird" + ], + "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", + "trimpath": true + }, + "overrides": [ + { + "match": { "os": "ios" }, + "tags_append": ["with_low_memory"] + }, + { + "match": { "os": "tvos" }, + "tags_append": ["with_low_memory"] + } + ] + }, + { + "id": "windows", + "packages": ["libbox"], + "default": { + "tags": [ + "with_gvisor", + "with_quic", + "with_wireguard", + "with_utls", + "with_naive_outbound", + "with_purego", + "with_clash_api", + "badlinkname", + "tfogo_checklinkname0", + "with_tailscale", + "ts_omit_logtail", + "ts_omit_ssh", + "ts_omit_drive", + "ts_omit_taildrop", + "ts_omit_webclient", + "ts_omit_doctor", + "ts_omit_capture", + "ts_omit_kube", + "ts_omit_aws", + "ts_omit_synology", + "ts_omit_bird" + ], + "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", + "trimpath": true + } + } + ], + "platforms": [ + { + "type": "android", + "build": "android-main", + "min_sdk": 23, + "ndk_version": "28.0.13004108", + "lib_name": "box", + "languages": [{ "type": "java" }], + "artifacts": [ + { + "type": "aar", + "output_path": "libbox.aar", + "execute_after": [ + "if [ -d \"${DEPLOY_ANDROID}\" ]; then", + " rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"", + " mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"", + "fi" + ] + } + ] + }, + { + "type": "android", + "build": "android-legacy", + "min_sdk": 21, + "ndk_version": "28.0.13004108", + "lib_name": "box", + "languages": [{ "type": "java" }], + "artifacts": [ + { + "type": "aar", + "output_path": "libbox-legacy.aar", + "execute_after": [ + "if [ -d \"${DEPLOY_ANDROID}\" ]; then", + " rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"", + " mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"", + "fi" + ] + } + ] + }, + { + "type": "apple", + "build": "apple", + "targets": [ + "ios/arm64", + "ios/simulator/arm64", + "ios/simulator/amd64", + "tvos/arm64", + "tvos/simulator/arm64", + "tvos/simulator/amd64", + "macos/arm64", + "macos/amd64" + ], + "languages": [{ "type": "objc" }], + "artifacts": [ + { + "type": "xcframework", + "module_name": "Libbox", + "execute_after": [ + "if [ -d \"${DEPLOY_APPLE}\" ]; then", + " rm -rf \"${DEPLOY_APPLE}/${MODULE_NAME}.xcframework\"", + " mv \"${OUTPUT_PATH}\" \"${DEPLOY_APPLE}/\"", + "fi" + ] + } + ] + }, + { + "type": "csharp", + "build": "windows", + "targets": [ + "windows/amd64" + ], + "languages": [{ "type": "csharp" }], + "artifacts": [ + { + "type": "nuget", + "package_id": "SagerNet.Libbox", + "package_version": "0.0.0-local", + "execute_after": { + "windows": [ + "$$deployPath = '${DEPLOY_WINDOWS}'", + "if (Test-Path $$deployPath) {", + " Remove-Item \"$$deployPath\\${PACKAGE_ID}.*.nupkg\" -ErrorAction SilentlyContinue", + " Move-Item -Force '${OUTPUT_PATH}' \"$$deployPath\\\"", + " $$cachePath = if ($$env:NUGET_PACKAGES) { $$env:NUGET_PACKAGES } else { \"$$env:USERPROFILE\\.nuget\\packages\" }", + " Remove-Item -Recurse -Force \"$$cachePath\\sagernet.libbox\\${PACKAGE_VERSION}\" -ErrorAction SilentlyContinue", + "}" + ], + "default": [ + "if [ -d \"${DEPLOY_WINDOWS}\" ]; then", + " rm -f \"${DEPLOY_WINDOWS}/${PACKAGE_ID}.*.nupkg\"", + " mv \"${OUTPUT_PATH}\" \"${DEPLOY_WINDOWS}/\"", + " cache_path=\"$${NUGET_PACKAGES:-$${HOME}/.nuget/packages}\"", + " rm -rf \"$${cache_path}/sagernet.libbox/${PACKAGE_VERSION}\"", + "fi" + ] + } + } + ] + } + ] +} diff --git a/experimental/libbox/http.go b/experimental/libbox/http.go new file mode 100644 index 00000000..69a23d26 --- /dev/null +++ b/experimental/libbox/http.go @@ -0,0 +1,274 @@ +package libbox + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io" + "math/rand" + "net" + "net/http" + "net/url" + "os" + "strconv" + "sync" + + C "github.com/sagernet/sing-box/constant" + "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" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/protocol/socks/socks5" +) + +type HTTPClient interface { + RestrictedTLS() + ModernTLS() + PinnedTLS12() + PinnedSHA256(sumHex string) + TrySocks5(port int32) + KeepAlive() + NewRequest() HTTPRequest + Close() +} + +type HTTPRequest interface { + SetURL(link string) error + SetMethod(method string) + SetHeader(key string, value string) + SetContent(content []byte) + SetContentString(content string) + RandomUserAgent() + SetUserAgent(userAgent string) + Execute() (HTTPResponse, error) +} + +type HTTPResponse interface { + GetContent() (*StringBox, error) + WriteTo(path string) error + WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error +} + +type HTTPResponseWriteToProgressHandler interface { + Update(progress int64, total int64) +} + +var ( + _ HTTPClient = (*httpClient)(nil) + _ HTTPRequest = (*httpRequest)(nil) + _ HTTPResponse = (*httpResponse)(nil) +) + +type httpClient struct { + tls tls.Config + client http.Client + transport http.Transport +} + +func NewHTTPClient() HTTPClient { + client := new(httpClient) + client.client.Transport = &client.transport + client.transport.ForceAttemptHTTP2 = true + client.transport.TLSHandshakeTimeout = C.TCPTimeout + client.transport.TLSClientConfig = &client.tls + client.transport.DisableKeepAlives = true + return client +} + +func (c *httpClient) ModernTLS() { + c.setTLSVersion(tls.VersionTLS12, 0, func(suite *tls.CipherSuite) bool { return true }) +} + +func (c *httpClient) RestrictedTLS() { + c.setTLSVersion(tls.VersionTLS13, 0, func(suite *tls.CipherSuite) bool { + return common.Contains(suite.SupportedVersions, uint16(tls.VersionTLS13)) + }) +} + +func (c *httpClient) setTLSVersion(minVersion, maxVersion uint16, filter func(*tls.CipherSuite) bool) { + c.tls.MinVersion = minVersion + if maxVersion != 0 { + c.tls.MaxVersion = maxVersion + } + c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), filter), func(it *tls.CipherSuite) uint16 { + return it.ID + }) +} + +func (c *httpClient) PinnedTLS12() { + c.setTLSVersion(tls.VersionTLS12, tls.VersionTLS12, func(suite *tls.CipherSuite) bool { return true }) +} + +func (c *httpClient) PinnedSHA256(sumHex string) { + c.tls.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + for _, rawCert := range rawCerts { + certSum := sha256.Sum256(rawCert) + if sumHex == hex.EncodeToString(certSum[:]) { + return nil + } + } + return E.New("pinned sha256 sum mismatch") + } +} + +func (c *httpClient) TrySocks5(port int32) { + dialer := new(net.Dialer) + c.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + for { + socksConn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(port))) + if err != nil { + break + } + _, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, M.ParseSocksaddr(addr), "", "") + if err != nil { + break + } + //nolint:staticcheck + return socksConn, err + } + return dialer.DialContext(ctx, network, addr) + } +} + +func (c *httpClient) KeepAlive() { + c.transport.DisableKeepAlives = false +} + +func (c *httpClient) NewRequest() HTTPRequest { + req := &httpRequest{httpClient: c} + req.request = http.Request{ + Method: "GET", + Header: http.Header{}, + } + return req +} + +func (c *httpClient) Close() { + c.transport.CloseIdleConnections() +} + +type httpRequest struct { + *httpClient + request http.Request +} + +func (r *httpRequest) SetURL(link string) (err error) { + r.request.URL, err = url.Parse(link) + if err != nil { + return + } + if r.request.URL.User != nil { + user := r.request.URL.User.Username() + password, _ := r.request.URL.User.Password() + r.request.SetBasicAuth(user, password) + } + return +} + +func (r *httpRequest) SetMethod(method string) { + r.request.Method = method +} + +func (r *httpRequest) SetHeader(key string, value string) { + r.request.Header.Set(key, value) +} + +func (r *httpRequest) RandomUserAgent() { + r.request.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) +} + +func (r *httpRequest) SetUserAgent(userAgent string) { + r.request.Header.Set("User-Agent", userAgent) +} + +func (r *httpRequest) SetContent(content []byte) { + r.request.Body = io.NopCloser(bytes.NewReader(content)) + r.request.ContentLength = int64(len(content)) +} + +func (r *httpRequest) SetContentString(content string) { + r.SetContent([]byte(content)) +} + +func (r *httpRequest) Execute() (HTTPResponse, error) { + response, err := r.client.Do(&r.request) + if err != nil { + return nil, err + } + httpResp := &httpResponse{Response: response} + if response.StatusCode != http.StatusOK { + return nil, errors.New(httpResp.errorString()) + } + return httpResp, nil +} + +type httpResponse struct { + *http.Response + + getContentOnce sync.Once + content []byte + contentError error +} + +func (h *httpResponse) errorString() string { + content, err := h.GetContent() + if err != nil { + return fmt.Sprint("HTTP ", h.Status) + } + return fmt.Sprint("HTTP ", h.Status, ": ", content) +} + +func (h *httpResponse) GetContent() (*StringBox, error) { + h.getContentOnce.Do(func() { + defer h.Body.Close() + h.content, h.contentError = io.ReadAll(h.Body) + }) + if h.contentError != nil { + return nil, h.contentError + } + return wrapString(string(h.content)), nil +} + +func (h *httpResponse) WriteTo(path string) error { + defer h.Body.Close() + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + return common.Error(bufio.Copy(file, h.Body)) +} + +func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error { + defer h.Body.Close() + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + return common.Error(bufio.Copy(&progressWriter{ + writer: file, + handler: handler, + total: h.ContentLength, + }, h.Body)) +} + +type progressWriter struct { + writer io.Writer + handler HTTPResponseWriteToProgressHandler + total int64 + written int64 +} + +func (w *progressWriter) Write(p []byte) (int, error) { + n, err := w.writer.Write(p) + w.written += int64(n) + w.handler.Update(w.written, w.total) + return n, err +} diff --git a/experimental/libbox/internal/oomprofile/builder.go b/experimental/libbox/internal/oomprofile/builder.go new file mode 100644 index 00000000..1f59078a --- /dev/null +++ b/experimental/libbox/internal/oomprofile/builder.go @@ -0,0 +1,390 @@ +//go:build darwin || linux || windows + +package oomprofile + +import ( + "fmt" + "io" + "runtime" + "time" +) + +const ( + tagProfile_SampleType = 1 + tagProfile_Sample = 2 + tagProfile_Mapping = 3 + tagProfile_Location = 4 + tagProfile_Function = 5 + tagProfile_StringTable = 6 + tagProfile_TimeNanos = 9 + tagProfile_PeriodType = 11 + tagProfile_Period = 12 + tagProfile_DefaultSampleType = 14 + + tagValueType_Type = 1 + tagValueType_Unit = 2 + + tagSample_Location = 1 + tagSample_Value = 2 + tagSample_Label = 3 + + tagLabel_Key = 1 + tagLabel_Str = 2 + tagLabel_Num = 3 + + tagMapping_ID = 1 + tagMapping_Start = 2 + tagMapping_Limit = 3 + tagMapping_Offset = 4 + tagMapping_Filename = 5 + tagMapping_BuildID = 6 + tagMapping_HasFunctions = 7 + tagMapping_HasFilenames = 8 + tagMapping_HasLineNumbers = 9 + tagMapping_HasInlineFrames = 10 + + tagLocation_ID = 1 + tagLocation_MappingID = 2 + tagLocation_Address = 3 + tagLocation_Line = 4 + + tagLine_FunctionID = 1 + tagLine_Line = 2 + + tagFunction_ID = 1 + tagFunction_Name = 2 + tagFunction_SystemName = 3 + tagFunction_Filename = 4 + tagFunction_StartLine = 5 +) + +type memMap struct { + start uintptr + end uintptr + offset uint64 + file string + buildID string + funcs symbolizeFlag + fake bool +} + +type symbolizeFlag uint8 + +const ( + lookupTried symbolizeFlag = 1 << iota + lookupFailed +) + +func newProfileBuilder(w io.Writer) *profileBuilder { + builder := &profileBuilder{ + start: time.Now(), + w: w, + strings: []string{""}, + stringMap: map[string]int{"": 0}, + locs: map[uintptr]locInfo{}, + funcs: map[string]int{}, + } + builder.readMapping() + return builder +} + +func (b *profileBuilder) stringIndex(s string) int64 { + id, ok := b.stringMap[s] + if !ok { + id = len(b.strings) + b.strings = append(b.strings, s) + b.stringMap[s] = id + } + return int64(id) +} + +func (b *profileBuilder) flush() { + const dataFlush = 4096 + if b.err != nil || b.pb.nest != 0 || len(b.pb.data) <= dataFlush { + return + } + + _, b.err = b.w.Write(b.pb.data) + b.pb.data = b.pb.data[:0] +} + +func (b *profileBuilder) pbValueType(tag int, typ string, unit string) { + start := b.pb.startMessage() + b.pb.int64(tagValueType_Type, b.stringIndex(typ)) + b.pb.int64(tagValueType_Unit, b.stringIndex(unit)) + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) { + start := b.pb.startMessage() + b.pb.int64s(tagSample_Value, values) + b.pb.uint64s(tagSample_Location, locs) + if labels != nil { + labels() + } + b.pb.endMessage(tagProfile_Sample, start) + b.flush() +} + +func (b *profileBuilder) pbLabel(tag int, key string, str string, num int64) { + start := b.pb.startMessage() + b.pb.int64Opt(tagLabel_Key, b.stringIndex(key)) + b.pb.int64Opt(tagLabel_Str, b.stringIndex(str)) + b.pb.int64Opt(tagLabel_Num, num) + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) { + start := b.pb.startMessage() + b.pb.uint64Opt(tagLine_FunctionID, funcID) + b.pb.int64Opt(tagLine_Line, line) + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) pbMapping(tag int, id uint64, base uint64, limit uint64, offset uint64, file string, buildID string, hasFuncs bool) { + start := b.pb.startMessage() + b.pb.uint64Opt(tagMapping_ID, id) + b.pb.uint64Opt(tagMapping_Start, base) + b.pb.uint64Opt(tagMapping_Limit, limit) + b.pb.uint64Opt(tagMapping_Offset, offset) + b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file)) + b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID)) + if hasFuncs { + b.pb.bool(tagMapping_HasFunctions, true) + } + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) build() error { + if b.err != nil { + return b.err + } + + b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano()) + for i, mapping := range b.mem { + hasFunctions := mapping.funcs == lookupTried + b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(mapping.start), uint64(mapping.end), mapping.offset, mapping.file, mapping.buildID, hasFunctions) + } + b.pb.strings(tagProfile_StringTable, b.strings) + if b.err != nil { + return b.err + } + _, err := b.w.Write(b.pb.data) + return err +} + +func allFrames(addr uintptr) ([]runtime.Frame, symbolizeFlag) { + frames := runtime.CallersFrames([]uintptr{addr}) + frame, more := frames.Next() + if frame.Function == "runtime.goexit" { + return nil, 0 + } + + result := lookupTried + if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 { + result |= lookupFailed + } + if frame.PC == 0 { + frame.PC = addr - 1 + } + + ret := []runtime.Frame{frame} + for frame.Function != "runtime.goexit" && more { + frame, more = frames.Next() + ret = append(ret, frame) + } + return ret, result +} + +type locInfo struct { + id uint64 + + pcs []uintptr + + firstPCFrames []runtime.Frame + firstPCSymbolizeResult symbolizeFlag +} + +func (b *profileBuilder) appendLocsForStack(locs []uint64, stk []uintptr) []uint64 { + b.deck.reset() + origStk := stk + stk = runtimeExpandFinalInlineFrame(stk) + + for len(stk) > 0 { + addr := stk[0] + if loc, ok := b.locs[addr]; ok { + if len(b.deck.pcs) > 0 { + if b.deck.tryAdd(addr, loc.firstPCFrames, loc.firstPCSymbolizeResult) { + stk = stk[1:] + continue + } + } + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + locs = append(locs, loc.id) + if len(loc.pcs) > len(stk) { + panic(fmt.Sprintf("stack too short to match cached location; stk = %#x, loc.pcs = %#x, original stk = %#x", stk, loc.pcs, origStk)) + } + stk = stk[len(loc.pcs):] + continue + } + + frames, symbolizeResult := allFrames(addr) + if len(frames) == 0 { + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + stk = stk[1:] + continue + } + + if b.deck.tryAdd(addr, frames, symbolizeResult) { + stk = stk[1:] + continue + } + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + + if loc, ok := b.locs[addr]; ok { + locs = append(locs, loc.id) + stk = stk[len(loc.pcs):] + } else { + b.deck.tryAdd(addr, frames, symbolizeResult) + stk = stk[1:] + } + } + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + return locs +} + +type pcDeck struct { + pcs []uintptr + frames []runtime.Frame + symbolizeResult symbolizeFlag + + firstPCFrames int + firstPCSymbolizeResult symbolizeFlag +} + +func (d *pcDeck) reset() { + d.pcs = d.pcs[:0] + d.frames = d.frames[:0] + d.symbolizeResult = 0 + d.firstPCFrames = 0 + d.firstPCSymbolizeResult = 0 +} + +func (d *pcDeck) tryAdd(pc uintptr, frames []runtime.Frame, symbolizeResult symbolizeFlag) bool { + if existing := len(d.frames); existing > 0 { + newFrame := frames[0] + last := d.frames[existing-1] + if last.Func != nil { + return false + } + if last.Entry == 0 || newFrame.Entry == 0 { + return false + } + if last.Entry != newFrame.Entry { + return false + } + if runtimeFrameSymbolName(&last) == runtimeFrameSymbolName(&newFrame) { + return false + } + } + + d.pcs = append(d.pcs, pc) + d.frames = append(d.frames, frames...) + d.symbolizeResult |= symbolizeResult + if len(d.pcs) == 1 { + d.firstPCFrames = len(d.frames) + d.firstPCSymbolizeResult = symbolizeResult + } + return true +} + +func (b *profileBuilder) emitLocation() uint64 { + if len(b.deck.pcs) == 0 { + return 0 + } + defer b.deck.reset() + + addr := b.deck.pcs[0] + firstFrame := b.deck.frames[0] + + type newFunc struct { + id uint64 + name string + file string + startLine int64 + } + + newFuncs := make([]newFunc, 0, 8) + id := uint64(len(b.locs)) + 1 + b.locs[addr] = locInfo{ + id: id, + pcs: append([]uintptr{}, b.deck.pcs...), + firstPCFrames: append([]runtime.Frame{}, b.deck.frames[:b.deck.firstPCFrames]...), + firstPCSymbolizeResult: b.deck.firstPCSymbolizeResult, + } + + start := b.pb.startMessage() + b.pb.uint64Opt(tagLocation_ID, id) + b.pb.uint64Opt(tagLocation_Address, uint64(firstFrame.PC)) + for _, frame := range b.deck.frames { + funcName := runtimeFrameSymbolName(&frame) + funcID := uint64(b.funcs[funcName]) + if funcID == 0 { + funcID = uint64(len(b.funcs)) + 1 + b.funcs[funcName] = int(funcID) + newFuncs = append(newFuncs, newFunc{ + id: funcID, + name: funcName, + file: frame.File, + startLine: int64(runtimeFrameStartLine(&frame)), + }) + } + b.pbLine(tagLocation_Line, funcID, int64(frame.Line)) + } + for i := range b.mem { + if (b.mem[i].start <= addr && addr < b.mem[i].end) || b.mem[i].fake { + b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1)) + mapping := b.mem[i] + mapping.funcs |= b.deck.symbolizeResult + b.mem[i] = mapping + break + } + } + b.pb.endMessage(tagProfile_Location, start) + + for _, fn := range newFuncs { + start := b.pb.startMessage() + b.pb.uint64Opt(tagFunction_ID, fn.id) + b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name)) + b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name)) + b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file)) + b.pb.int64Opt(tagFunction_StartLine, fn.startLine) + b.pb.endMessage(tagProfile_Function, start) + } + + b.flush() + return id +} + +func (b *profileBuilder) addMapping(lo uint64, hi uint64, offset uint64, file string, buildID string) { + b.addMappingEntry(lo, hi, offset, file, buildID, false) +} + +func (b *profileBuilder) addMappingEntry(lo uint64, hi uint64, offset uint64, file string, buildID string, fake bool) { + b.mem = append(b.mem, memMap{ + start: uintptr(lo), + end: uintptr(hi), + offset: offset, + file: file, + buildID: buildID, + fake: fake, + }) +} diff --git a/experimental/libbox/internal/oomprofile/defs_darwin_amd64.go b/experimental/libbox/internal/oomprofile/defs_darwin_amd64.go new file mode 100644 index 00000000..8a30074c --- /dev/null +++ b/experimental/libbox/internal/oomprofile/defs_darwin_amd64.go @@ -0,0 +1,24 @@ +//go:build darwin && amd64 + +package oomprofile + +type machVMRegionBasicInfoData struct { + Protection int32 + MaxProtection int32 + Inheritance uint32 + Shared uint32 + Reserved uint32 + Offset [8]byte + Behavior int32 + UserWiredCount uint16 + PadCgo1 [2]byte +} + +const ( + _VM_PROT_READ = 0x1 + _VM_PROT_EXECUTE = 0x4 + + _MACH_SEND_INVALID_DEST = 0x10000003 + + _MAXPATHLEN = 0x400 +) diff --git a/experimental/libbox/internal/oomprofile/defs_darwin_arm64.go b/experimental/libbox/internal/oomprofile/defs_darwin_arm64.go new file mode 100644 index 00000000..2fd46590 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/defs_darwin_arm64.go @@ -0,0 +1,24 @@ +//go:build darwin && arm64 + +package oomprofile + +type machVMRegionBasicInfoData struct { + Protection int32 + MaxProtection int32 + Inheritance uint32 + Shared int32 + Reserved int32 + Offset [8]byte + Behavior int32 + UserWiredCount uint16 + PadCgo1 [2]byte +} + +const ( + _VM_PROT_READ = 0x1 + _VM_PROT_EXECUTE = 0x4 + + _MACH_SEND_INVALID_DEST = 0x10000003 + + _MAXPATHLEN = 0x400 +) diff --git a/experimental/libbox/internal/oomprofile/linkname.go b/experimental/libbox/internal/oomprofile/linkname.go new file mode 100644 index 00000000..f7ab2717 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/linkname.go @@ -0,0 +1,46 @@ +//go:build darwin || linux || windows + +package oomprofile + +import ( + "runtime" + _ "runtime/pprof" + "unsafe" + _ "unsafe" +) + +//go:linkname runtimeMemProfileInternal runtime.pprof_memProfileInternal +func runtimeMemProfileInternal(p []memProfileRecord, inuseZero bool) (n int, ok bool) + +//go:linkname runtimeBlockProfileInternal runtime.pprof_blockProfileInternal +func runtimeBlockProfileInternal(p []blockProfileRecord) (n int, ok bool) + +//go:linkname runtimeMutexProfileInternal runtime.pprof_mutexProfileInternal +func runtimeMutexProfileInternal(p []blockProfileRecord) (n int, ok bool) + +//go:linkname runtimeThreadCreateInternal runtime.pprof_threadCreateInternal +func runtimeThreadCreateInternal(p []stackRecord) (n int, ok bool) + +//go:linkname runtimeGoroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels +func runtimeGoroutineProfileWithLabels(p []stackRecord, labels []unsafe.Pointer) (n int, ok bool) + +//go:linkname runtimeCyclesPerSecond runtime/pprof.runtime_cyclesPerSecond +func runtimeCyclesPerSecond() int64 + +//go:linkname runtimeMakeProfStack runtime.pprof_makeProfStack +func runtimeMakeProfStack() []uintptr + +//go:linkname runtimeFrameStartLine runtime/pprof.runtime_FrameStartLine +func runtimeFrameStartLine(f *runtime.Frame) int + +//go:linkname runtimeFrameSymbolName runtime/pprof.runtime_FrameSymbolName +func runtimeFrameSymbolName(f *runtime.Frame) string + +//go:linkname runtimeExpandFinalInlineFrame runtime/pprof.runtime_expandFinalInlineFrame +func runtimeExpandFinalInlineFrame(stk []uintptr) []uintptr + +//go:linkname stdParseProcSelfMaps runtime/pprof.parseProcSelfMaps +func stdParseProcSelfMaps(data []byte, addMapping func(lo uint64, hi uint64, offset uint64, file string, buildID string)) + +//go:linkname stdELFBuildID runtime/pprof.elfBuildID +func stdELFBuildID(file string) (string, error) diff --git a/experimental/libbox/internal/oomprofile/mapping_darwin.go b/experimental/libbox/internal/oomprofile/mapping_darwin.go new file mode 100644 index 00000000..e2730005 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/mapping_darwin.go @@ -0,0 +1,56 @@ +//go:build darwin + +package oomprofile + +import ( + "encoding/binary" + "os" + "unsafe" + _ "unsafe" +) + +func isExecutable(protection int32) bool { + return (protection&_VM_PROT_EXECUTE) != 0 && (protection&_VM_PROT_READ) != 0 +} + +func (b *profileBuilder) readMapping() { + if !machVMInfo(b.addMapping) { + b.addMappingEntry(0, 0, 0, "", "", true) + } +} + +func machVMInfo(addMapping func(lo uint64, hi uint64, off uint64, file string, buildID string)) bool { + added := false + addr := uint64(0x1) + for { + var regionSize uint64 + var info machVMRegionBasicInfoData + kr := machVMRegion(&addr, ®ionSize, unsafe.Pointer(&info)) + if kr != 0 { + if kr == _MACH_SEND_INVALID_DEST { + return true + } + return added + } + if isExecutable(info.Protection) { + addMapping(addr, addr+regionSize, binary.LittleEndian.Uint64(info.Offset[:]), regionFilename(addr), "") + added = true + } + addr += regionSize + } +} + +func regionFilename(address uint64) string { + buf := make([]byte, _MAXPATHLEN) + n := procRegionFilename(os.Getpid(), address, unsafe.SliceData(buf), int64(cap(buf))) + if n == 0 { + return "" + } + return string(buf[:n]) +} + +//go:linkname machVMRegion runtime/pprof.mach_vm_region +func machVMRegion(address *uint64, regionSize *uint64, info unsafe.Pointer) int32 + +//go:linkname procRegionFilename runtime/pprof.proc_regionfilename +func procRegionFilename(pid int, address uint64, buf *byte, buflen int64) int32 diff --git a/experimental/libbox/internal/oomprofile/mapping_linux.go b/experimental/libbox/internal/oomprofile/mapping_linux.go new file mode 100644 index 00000000..cc9b03a6 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/mapping_linux.go @@ -0,0 +1,13 @@ +//go:build linux + +package oomprofile + +import "os" + +func (b *profileBuilder) readMapping() { + data, _ := os.ReadFile("/proc/self/maps") + stdParseProcSelfMaps(data, b.addMapping) + if len(b.mem) == 0 { + b.addMappingEntry(0, 0, 0, "", "", true) + } +} diff --git a/experimental/libbox/internal/oomprofile/mapping_windows.go b/experimental/libbox/internal/oomprofile/mapping_windows.go new file mode 100644 index 00000000..68303d89 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/mapping_windows.go @@ -0,0 +1,58 @@ +//go:build windows + +package oomprofile + +import ( + "errors" + "os" + + "golang.org/x/sys/windows" +) + +func (b *profileBuilder) readMapping() { + snapshot, err := createModuleSnapshot() + if err != nil { + b.addMappingEntry(0, 0, 0, "", "", true) + return + } + defer windows.CloseHandle(snapshot) + + var module windows.ModuleEntry32 + module.Size = uint32(windows.SizeofModuleEntry32) + err = windows.Module32First(snapshot, &module) + if err != nil { + b.addMappingEntry(0, 0, 0, "", "", true) + return + } + for err == nil { + exe := windows.UTF16ToString(module.ExePath[:]) + b.addMappingEntry( + uint64(module.ModBaseAddr), + uint64(module.ModBaseAddr)+uint64(module.ModBaseSize), + 0, + exe, + peBuildID(exe), + false, + ) + err = windows.Module32Next(snapshot, &module) + } +} + +func createModuleSnapshot() (windows.Handle, error) { + for { + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, uint32(windows.GetCurrentProcessId())) + var errno windows.Errno + if err != nil && errors.As(err, &errno) && errno == windows.ERROR_BAD_LENGTH { + continue + } + return snapshot, err + } +} + +func peBuildID(file string) string { + info, err := os.Stat(file) + if err != nil { + return file + } + return file + info.ModTime().String() +} diff --git a/experimental/libbox/internal/oomprofile/oomprofile.go b/experimental/libbox/internal/oomprofile/oomprofile.go new file mode 100644 index 00000000..cd0b9bec --- /dev/null +++ b/experimental/libbox/internal/oomprofile/oomprofile.go @@ -0,0 +1,383 @@ +//go:build darwin || linux || windows + +package oomprofile + +import ( + "fmt" + "io" + "math" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "time" + "unsafe" +) + +type stackRecord struct { + Stack []uintptr +} + +type memProfileRecord struct { + AllocBytes, FreeBytes int64 + AllocObjects, FreeObjects int64 + Stack []uintptr +} + +func (r *memProfileRecord) InUseBytes() int64 { + return r.AllocBytes - r.FreeBytes +} + +func (r *memProfileRecord) InUseObjects() int64 { + return r.AllocObjects - r.FreeObjects +} + +type blockProfileRecord struct { + Count int64 + Cycles int64 + Stack []uintptr +} + +type label struct { + key string + value string +} + +type labelSet struct { + list []label +} + +type labelMap struct { + labelSet +} + +func WriteFile(destPath string, name string) (string, error) { + writer, ok := profileWriters[name] + if !ok { + return "", fmt.Errorf("unsupported profile %q", name) + } + + filePath := filepath.Join(destPath, name+".pb") + file, err := os.Create(filePath) + if err != nil { + return "", err + } + defer file.Close() + + if err := writer(file); err != nil { + _ = os.Remove(filePath) + return "", err + } + if err := file.Close(); err != nil { + _ = os.Remove(filePath) + return "", err + } + return filePath, nil +} + +var profileWriters = map[string]func(io.Writer) error{ + "allocs": writeAlloc, + "block": writeBlock, + "goroutine": writeGoroutine, + "heap": writeHeap, + "mutex": writeMutex, + "threadcreate": writeThreadCreate, +} + +func writeHeap(w io.Writer) error { + return writeHeapInternal(w, "") +} + +func writeAlloc(w io.Writer) error { + return writeHeapInternal(w, "alloc_space") +} + +func writeHeapInternal(w io.Writer, defaultSampleType string) error { + var profile []memProfileRecord + n, _ := runtimeMemProfileInternal(nil, true) + var ok bool + for { + profile = make([]memProfileRecord, n+50) + n, ok = runtimeMemProfileInternal(profile, true) + if ok { + profile = profile[:n] + break + } + } + return writeHeapProto(w, profile, int64(runtime.MemProfileRate), defaultSampleType) +} + +func writeGoroutine(w io.Writer) error { + return writeRuntimeProfile(w, "goroutine", runtimeGoroutineProfileWithLabels) +} + +func writeThreadCreate(w io.Writer) error { + return writeRuntimeProfile(w, "threadcreate", func(p []stackRecord, _ []unsafe.Pointer) (int, bool) { + return runtimeThreadCreateInternal(p) + }) +} + +func writeRuntimeProfile(w io.Writer, name string, fetch func([]stackRecord, []unsafe.Pointer) (int, bool)) error { + var profile []stackRecord + var labels []unsafe.Pointer + + n, _ := fetch(nil, nil) + var ok bool + for { + profile = make([]stackRecord, n+10) + labels = make([]unsafe.Pointer, n+10) + n, ok = fetch(profile, labels) + if ok { + profile = profile[:n] + labels = labels[:n] + break + } + } + + return writeCountProfile(w, name, &runtimeProfile{profile, labels}) +} + +func writeBlock(w io.Writer) error { + return writeCycleProfile(w, "contentions", "delay", runtimeBlockProfileInternal) +} + +func writeMutex(w io.Writer) error { + return writeCycleProfile(w, "contentions", "delay", runtimeMutexProfileInternal) +} + +func writeCycleProfile(w io.Writer, countName string, cycleName string, fetch func([]blockProfileRecord) (int, bool)) error { + var profile []blockProfileRecord + n, _ := fetch(nil) + var ok bool + for { + profile = make([]blockProfileRecord, n+50) + n, ok = fetch(profile) + if ok { + profile = profile[:n] + break + } + } + + sort.Slice(profile, func(i, j int) bool { + return profile[i].Cycles > profile[j].Cycles + }) + + builder := newProfileBuilder(w) + builder.pbValueType(tagProfile_PeriodType, countName, "count") + builder.pb.int64Opt(tagProfile_Period, 1) + builder.pbValueType(tagProfile_SampleType, countName, "count") + builder.pbValueType(tagProfile_SampleType, cycleName, "nanoseconds") + + cpuGHz := float64(runtimeCyclesPerSecond()) / 1e9 + values := []int64{0, 0} + var locs []uint64 + expandedStack := runtimeMakeProfStack() + for _, record := range profile { + values[0] = record.Count + if cpuGHz > 0 { + values[1] = int64(float64(record.Cycles) / cpuGHz) + } else { + values[1] = 0 + } + n := expandInlinedFrames(expandedStack, record.Stack) + locs = builder.appendLocsForStack(locs[:0], expandedStack[:n]) + builder.pbSample(values, locs, nil) + } + + return builder.build() +} + +type countProfile interface { + Len() int + Stack(i int) []uintptr + Label(i int) *labelMap +} + +type runtimeProfile struct { + stk []stackRecord + labels []unsafe.Pointer +} + +func (p *runtimeProfile) Len() int { + return len(p.stk) +} + +func (p *runtimeProfile) Stack(i int) []uintptr { + return p.stk[i].Stack +} + +func (p *runtimeProfile) Label(i int) *labelMap { + return (*labelMap)(p.labels[i]) +} + +func writeCountProfile(w io.Writer, name string, profile countProfile) error { + var buf strings.Builder + key := func(stk []uintptr, labels *labelMap) string { + buf.Reset() + buf.WriteByte('@') + for _, pc := range stk { + fmt.Fprintf(&buf, " %#x", pc) + } + if labels != nil { + buf.WriteString("\n# labels:") + for _, label := range labels.list { + fmt.Fprintf(&buf, " %q:%q", label.key, label.value) + } + } + return buf.String() + } + + counts := make(map[string]int) + index := make(map[string]int) + var keys []string + for i := 0; i < profile.Len(); i++ { + k := key(profile.Stack(i), profile.Label(i)) + if counts[k] == 0 { + index[k] = i + keys = append(keys, k) + } + counts[k]++ + } + + sort.Sort(&keysByCount{keys: keys, count: counts}) + + builder := newProfileBuilder(w) + builder.pbValueType(tagProfile_PeriodType, name, "count") + builder.pb.int64Opt(tagProfile_Period, 1) + builder.pbValueType(tagProfile_SampleType, name, "count") + + values := []int64{0} + var locs []uint64 + for _, k := range keys { + values[0] = int64(counts[k]) + idx := index[k] + locs = builder.appendLocsForStack(locs[:0], profile.Stack(idx)) + + var labels func() + if profile.Label(idx) != nil { + labels = func() { + for _, label := range profile.Label(idx).list { + builder.pbLabel(tagSample_Label, label.key, label.value, 0) + } + } + } + builder.pbSample(values, locs, labels) + } + + return builder.build() +} + +type keysByCount struct { + keys []string + count map[string]int +} + +func (x *keysByCount) Len() int { + return len(x.keys) +} + +func (x *keysByCount) Swap(i int, j int) { + x.keys[i], x.keys[j] = x.keys[j], x.keys[i] +} + +func (x *keysByCount) Less(i int, j int) bool { + ki, kj := x.keys[i], x.keys[j] + ci, cj := x.count[ki], x.count[kj] + if ci != cj { + return ci > cj + } + return ki < kj +} + +func expandInlinedFrames(dst []uintptr, pcs []uintptr) int { + frames := runtime.CallersFrames(pcs) + var n int + for n < len(dst) { + frame, more := frames.Next() + dst[n] = frame.PC + 1 + n++ + if !more { + break + } + } + return n +} + +func writeHeapProto(w io.Writer, profile []memProfileRecord, rate int64, defaultSampleType string) error { + builder := newProfileBuilder(w) + builder.pbValueType(tagProfile_PeriodType, "space", "bytes") + builder.pb.int64Opt(tagProfile_Period, rate) + builder.pbValueType(tagProfile_SampleType, "alloc_objects", "count") + builder.pbValueType(tagProfile_SampleType, "alloc_space", "bytes") + builder.pbValueType(tagProfile_SampleType, "inuse_objects", "count") + builder.pbValueType(tagProfile_SampleType, "inuse_space", "bytes") + if defaultSampleType != "" { + builder.pb.int64Opt(tagProfile_DefaultSampleType, builder.stringIndex(defaultSampleType)) + } + + values := []int64{0, 0, 0, 0} + var locs []uint64 + for _, record := range profile { + hideRuntime := true + for tries := 0; tries < 2; tries++ { + stk := record.Stack + if hideRuntime { + for i, addr := range stk { + if f := runtime.FuncForPC(addr); f != nil && (strings.HasPrefix(f.Name(), "runtime.") || strings.HasPrefix(f.Name(), "internal/runtime/")) { + continue + } + stk = stk[i:] + break + } + } + locs = builder.appendLocsForStack(locs[:0], stk) + if len(locs) > 0 { + break + } + hideRuntime = false + } + + values[0], values[1] = scaleHeapSample(record.AllocObjects, record.AllocBytes, rate) + values[2], values[3] = scaleHeapSample(record.InUseObjects(), record.InUseBytes(), rate) + + var blockSize int64 + if record.AllocObjects > 0 { + blockSize = record.AllocBytes / record.AllocObjects + } + builder.pbSample(values, locs, func() { + if blockSize != 0 { + builder.pbLabel(tagSample_Label, "bytes", "", blockSize) + } + }) + } + + return builder.build() +} + +func scaleHeapSample(count int64, size int64, rate int64) (int64, int64) { + if count == 0 || size == 0 { + return 0, 0 + } + if rate <= 1 { + return count, size + } + + avgSize := float64(size) / float64(count) + scale := 1 / (1 - math.Exp(-avgSize/float64(rate))) + return int64(float64(count) * scale), int64(float64(size) * scale) +} + +type profileBuilder struct { + start time.Time + w io.Writer + err error + + pb protobuf + strings []string + stringMap map[string]int + locs map[uintptr]locInfo + funcs map[string]int + mem []memMap + deck pcDeck +} diff --git a/experimental/libbox/internal/oomprofile/protobuf.go b/experimental/libbox/internal/oomprofile/protobuf.go new file mode 100644 index 00000000..ed60df21 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/protobuf.go @@ -0,0 +1,120 @@ +//go:build darwin || linux || windows + +package oomprofile + +type protobuf struct { + data []byte + tmp [16]byte + nest int +} + +func (b *protobuf) varint(x uint64) { + for x >= 128 { + b.data = append(b.data, byte(x)|0x80) + x >>= 7 + } + b.data = append(b.data, byte(x)) +} + +func (b *protobuf) length(tag int, length int) { + b.varint(uint64(tag)<<3 | 2) + b.varint(uint64(length)) +} + +func (b *protobuf) uint64(tag int, x uint64) { + b.varint(uint64(tag) << 3) + b.varint(x) +} + +func (b *protobuf) uint64s(tag int, x []uint64) { + if len(x) > 2 { + n1 := len(b.data) + for _, u := range x { + b.varint(u) + } + n2 := len(b.data) + b.length(tag, n2-n1) + n3 := len(b.data) + copy(b.tmp[:], b.data[n2:n3]) + copy(b.data[n1+(n3-n2):], b.data[n1:n2]) + copy(b.data[n1:], b.tmp[:n3-n2]) + return + } + for _, u := range x { + b.uint64(tag, u) + } +} + +func (b *protobuf) uint64Opt(tag int, x uint64) { + if x == 0 { + return + } + b.uint64(tag, x) +} + +func (b *protobuf) int64(tag int, x int64) { + b.uint64(tag, uint64(x)) +} + +func (b *protobuf) int64Opt(tag int, x int64) { + if x == 0 { + return + } + b.int64(tag, x) +} + +func (b *protobuf) int64s(tag int, x []int64) { + if len(x) > 2 { + n1 := len(b.data) + for _, u := range x { + b.varint(uint64(u)) + } + n2 := len(b.data) + b.length(tag, n2-n1) + n3 := len(b.data) + copy(b.tmp[:], b.data[n2:n3]) + copy(b.data[n1+(n3-n2):], b.data[n1:n2]) + copy(b.data[n1:], b.tmp[:n3-n2]) + return + } + for _, u := range x { + b.int64(tag, u) + } +} + +func (b *protobuf) bool(tag int, x bool) { + if x { + b.uint64(tag, 1) + } else { + b.uint64(tag, 0) + } +} + +func (b *protobuf) string(tag int, x string) { + b.length(tag, len(x)) + b.data = append(b.data, x...) +} + +func (b *protobuf) strings(tag int, x []string) { + for _, s := range x { + b.string(tag, s) + } +} + +type msgOffset int + +func (b *protobuf) startMessage() msgOffset { + b.nest++ + return msgOffset(len(b.data)) +} + +func (b *protobuf) endMessage(tag int, start msgOffset) { + n1 := int(start) + n2 := len(b.data) + b.length(tag, n2-n1) + n3 := len(b.data) + copy(b.tmp[:], b.data[n2:n3]) + copy(b.data[n1+(n3-n2):], b.data[n1:n2]) + copy(b.data[n1:], b.tmp[:n3-n2]) + b.nest-- +} diff --git a/experimental/libbox/internal/procfs/procfs.go b/experimental/libbox/internal/procfs/procfs.go new file mode 100644 index 00000000..8c918a79 --- /dev/null +++ b/experimental/libbox/internal/procfs/procfs.go @@ -0,0 +1,148 @@ +package procfs + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "fmt" + "net" + "net/netip" + "os" + "strconv" + "strings" + "unsafe" + + N "github.com/sagernet/sing/common/network" +) + +var ( + netIndexOfLocal = -1 + netIndexOfUid = -1 + nativeEndian binary.ByteOrder +) + +func init() { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + nativeEndian = binary.BigEndian + } else { + nativeEndian = binary.LittleEndian + } +} + +func ResolveSocketByProcSearch(network string, source, _ netip.AddrPort) int32 { + if netIndexOfLocal < 0 || netIndexOfUid < 0 { + return -1 + } + + path := "/proc/net/" + + if network == N.NetworkTCP { + path += "tcp" + } else { + path += "udp" + } + + if source.Addr().Is6() { + path += "6" + } + + sIP := source.Addr().AsSlice() + if len(sIP) == 0 { + return -1 + } + + var bytes [2]byte + binary.BigEndian.PutUint16(bytes[:], source.Port()) + local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:])) + + file, err := os.Open(path) + if err != nil { + return -1 + } + + defer file.Close() + + reader := bufio.NewReader(file) + + for { + row, _, err := reader.ReadLine() + if err != nil { + return -1 + } + + fields := strings.Fields(string(row)) + + if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid { + continue + } + + if strings.EqualFold(local, fields[netIndexOfLocal]) { + uid, err := strconv.Atoi(fields[netIndexOfUid]) + if err != nil { + return -1 + } + + return int32(uid) + } + } +} + +func nativeEndianIP(ip net.IP) []byte { + result := make([]byte, len(ip)) + + for i := 0; i < len(ip); i += 4 { + value := binary.BigEndian.Uint32(ip[i:]) + + nativeEndian.PutUint32(result[i:], value) + } + + return result +} + +func init() { + file, err := os.Open("/proc/net/tcp") + if err != nil { + return + } + + defer file.Close() + + reader := bufio.NewReader(file) + + header, _, err := reader.ReadLine() + if err != nil { + return + } + + columns := strings.Fields(string(header)) + + var txQueue, rxQueue, tr, tmWhen bool + + for idx, col := range columns { + offset := 0 + + if txQueue && rxQueue { + offset-- + } + + if tr && tmWhen { + offset-- + } + + switch col { + case "tx_queue": + txQueue = true + case "rx_queue": + rxQueue = true + case "tr": + tr = true + case "tm->when": + tmWhen = true + case "local_address": + netIndexOfLocal = idx + offset + case "uid": + netIndexOfUid = idx + offset + } + } +} diff --git a/experimental/libbox/iterator.go b/experimental/libbox/iterator.go new file mode 100644 index 00000000..32cbbddb --- /dev/null +++ b/experimental/libbox/iterator.go @@ -0,0 +1,63 @@ +package libbox + +import "github.com/sagernet/sing/common" + +type StringIterator interface { + Len() int32 + HasNext() bool + Next() string +} + +type Int32Iterator interface { + Len() int32 + HasNext() bool + Next() int32 +} + +var _ StringIterator = (*iterator[string])(nil) + +type iterator[T any] struct { + values []T +} + +func newIterator[T any](values []T) *iterator[T] { + return &iterator[T]{values} +} + +//go:noinline +func newPtrIterator[T any](values []T) *iterator[*T] { + return &iterator[*T]{common.Map(values, func(value T) *T { return &value })} +} + +func (i *iterator[T]) Len() int32 { + return int32(len(i.values)) +} + +func (i *iterator[T]) HasNext() bool { + return len(i.values) > 0 +} + +func (i *iterator[T]) Next() T { + if len(i.values) == 0 { + return common.DefaultValue[T]() + } + nextValue := i.values[0] + i.values = i.values[1:] + return nextValue +} + +type abstractIterator[T any] interface { + Next() T + HasNext() bool +} + +func iteratorToArray[T any](iterator abstractIterator[T]) []T { + if iterator == nil { + return nil + } + var values []T + for iterator.HasNext() { + values = append(values, iterator.Next()) + } + return values +} diff --git a/experimental/libbox/link_flags_stub.go b/experimental/libbox/link_flags_stub.go new file mode 100644 index 00000000..ce3d1eb8 --- /dev/null +++ b/experimental/libbox/link_flags_stub.go @@ -0,0 +1,11 @@ +//go:build !unix + +package libbox + +import ( + "net" +) + +func linkFlags(rawFlags uint32) net.Flags { + panic("stub!") +} diff --git a/experimental/libbox/link_flags_unix.go b/experimental/libbox/link_flags_unix.go new file mode 100644 index 00000000..04f41d64 --- /dev/null +++ b/experimental/libbox/link_flags_unix.go @@ -0,0 +1,32 @@ +//go:build unix + +package libbox + +import ( + "net" + "syscall" +) + +// copied from net.linkFlags +func linkFlags(rawFlags uint32) net.Flags { + var f net.Flags + if rawFlags&syscall.IFF_UP != 0 { + f |= net.FlagUp + } + if rawFlags&syscall.IFF_RUNNING != 0 { + f |= net.FlagRunning + } + if rawFlags&syscall.IFF_BROADCAST != 0 { + f |= net.FlagBroadcast + } + if rawFlags&syscall.IFF_LOOPBACK != 0 { + f |= net.FlagLoopback + } + if rawFlags&syscall.IFF_POINTOPOINT != 0 { + f |= net.FlagPointToPoint + } + if rawFlags&syscall.IFF_MULTICAST != 0 { + f |= net.FlagMulticast + } + return f +} diff --git a/experimental/libbox/log.go b/experimental/libbox/log.go new file mode 100644 index 00000000..e275d7e6 --- /dev/null +++ b/experimental/libbox/log.go @@ -0,0 +1,165 @@ +//go:build darwin || linux || windows + +package libbox + +import ( + "archive/zip" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "runtime/debug" + "time" +) + +type crashReportMetadata struct { + reportMetadata + CrashedAt string `json:"crashedAt,omitempty"` + SignalName string `json:"signalName,omitempty"` + SignalCode string `json:"signalCode,omitempty"` + ExceptionName string `json:"exceptionName,omitempty"` + ExceptionReason string `json:"exceptionReason,omitempty"` +} + +func archiveCrashReport(path string, crashReportsDir string) { + content, err := os.ReadFile(path) + if err != nil || len(content) == 0 { + return + } + + info, _ := os.Stat(path) + crashTime := time.Now().UTC() + if info != nil { + crashTime = info.ModTime().UTC() + } + + initReportDir(crashReportsDir) + destPath, err := nextAvailableReportPath(crashReportsDir, crashTime) + if err != nil { + return + } + initReportDir(destPath) + + writeReportFile(destPath, "go.log", content) + metadata := crashReportMetadata{ + reportMetadata: baseReportMetadata(), + CrashedAt: crashTime.Format(time.RFC3339), + } + writeReportMetadata(destPath, metadata) + os.Remove(path) + copyConfigSnapshot(destPath) +} + +func configSnapshotPath() string { + return filepath.Join(sBasePath, "configuration.json") +} + +func saveConfigSnapshot(configContent string) { + snapshotPath := configSnapshotPath() + os.WriteFile(snapshotPath, []byte(configContent), 0o666) + chownReport(snapshotPath) +} + +func redirectStderr(path string) error { + crashReportsDir := filepath.Join(sWorkingPath, "crash_reports") + archiveCrashReport(path, crashReportsDir) + archiveCrashReport(path+".old", crashReportsDir) + + outputFile, err := os.Create(path) + if err != nil { + return err + } + if runtime.GOOS != "android" && runtime.GOOS != "windows" { + err = outputFile.Chown(sUserID, sGroupID) + if err != nil { + outputFile.Close() + os.Remove(outputFile.Name()) + return err + } + } + + err = debug.SetCrashOutput(outputFile, debug.CrashOptions{}) + if err != nil { + outputFile.Close() + os.Remove(outputFile.Name()) + return err + } + _ = outputFile.Close() + return nil +} + +func CreateZipArchive(sourcePath string, destinationPath string) error { + sourceInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + if !sourceInfo.IsDir() { + return os.ErrInvalid + } + + destinationFile, err := os.Create(destinationPath) + if err != nil { + return err + } + defer func() { + _ = destinationFile.Close() + }() + + zipWriter := zip.NewWriter(destinationFile) + + rootName := filepath.Base(sourcePath) + err = filepath.WalkDir(sourcePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + relativePath, err := filepath.Rel(sourcePath, path) + if err != nil { + return err + } + if relativePath == "." { + return nil + } + + archivePath := filepath.ToSlash(filepath.Join(rootName, relativePath)) + if d.IsDir() { + _, err = zipWriter.Create(archivePath + "/") + return err + } + + fileInfo, err := d.Info() + if err != nil { + return err + } + header, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return err + } + header.Name = archivePath + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + sourceFile, err := os.Open(path) + if err != nil { + return err + } + + _, err = io.Copy(writer, sourceFile) + closeErr := sourceFile.Close() + if err != nil { + return err + } + return closeErr + }) + if err != nil { + _ = zipWriter.Close() + return err + } + + return zipWriter.Close() +} diff --git a/experimental/libbox/monitor.go b/experimental/libbox/monitor.go new file mode 100644 index 00000000..2deedb2e --- /dev/null +++ b/experimental/libbox/monitor.go @@ -0,0 +1,117 @@ +package libbox + +import ( + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/x/list" +) + +var ( + _ tun.DefaultInterfaceMonitor = (*platformDefaultInterfaceMonitor)(nil) + _ InterfaceUpdateListener = (*platformDefaultInterfaceMonitor)(nil) +) + +type platformDefaultInterfaceMonitor struct { + *platformInterfaceWrapper + logger logger.Logger + element *list.Element[tun.NetworkUpdateCallback] + callbacks list.List[tun.DefaultInterfaceUpdateCallback] + myInterface string +} + +func (m *platformDefaultInterfaceMonitor) Start() error { + return m.iif.StartDefaultInterfaceMonitor(m) +} + +func (m *platformDefaultInterfaceMonitor) Close() error { + return m.iif.CloseDefaultInterfaceMonitor(m) +} + +func (m *platformDefaultInterfaceMonitor) DefaultInterface() *control.Interface { + m.defaultInterfaceAccess.Lock() + defer m.defaultInterfaceAccess.Unlock() + return m.defaultInterface +} + +func (m *platformDefaultInterfaceMonitor) OverrideAndroidVPN() bool { + return false +} + +func (m *platformDefaultInterfaceMonitor) AndroidVPNEnabled() bool { + return false +} + +func (m *platformDefaultInterfaceMonitor) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] { + m.defaultInterfaceAccess.Lock() + defer m.defaultInterfaceAccess.Unlock() + return m.callbacks.PushBack(callback) +} + +func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { + m.defaultInterfaceAccess.Lock() + defer m.defaultInterfaceAccess.Unlock() + m.callbacks.Remove(element) +} + +func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { + if sFixAndroidStack { + done := make(chan struct{}) + go func() { + m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained) + close(done) + }() + <-done + } else { + m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained) + } +} + +func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { + m.isExpensive = isExpensive + m.isConstrained = isConstrained + err := m.networkManager.UpdateInterfaces() + if err != nil { + m.logger.Error(E.Cause(err, "update interfaces")) + } + m.defaultInterfaceAccess.Lock() + if interfaceIndex32 == -1 { + m.defaultInterface = nil + callbacks := m.callbacks.Array() + m.defaultInterfaceAccess.Unlock() + for _, callback := range callbacks { + callback(nil, 0) + } + return + } + oldInterface := m.defaultInterface + newInterface, err := m.networkManager.InterfaceFinder().ByIndex(int(interfaceIndex32)) + if err != nil { + m.defaultInterfaceAccess.Unlock() + m.logger.Error(E.Cause(err, "find updated interface: ", interfaceName)) + return + } + m.defaultInterface = newInterface + if oldInterface != nil && oldInterface.Name == m.defaultInterface.Name && oldInterface.Index == m.defaultInterface.Index { + m.defaultInterfaceAccess.Unlock() + return + } + callbacks := m.callbacks.Array() + m.defaultInterfaceAccess.Unlock() + for _, callback := range callbacks { + 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 +} diff --git a/experimental/libbox/neighbor.go b/experimental/libbox/neighbor.go new file mode 100644 index 00000000..e38aa802 --- /dev/null +++ b/experimental/libbox/neighbor.go @@ -0,0 +1,53 @@ +package libbox + +import ( + "net" + "net/netip" +) + +type NeighborEntry struct { + Address string + MacAddress string + Hostname string +} + +type NeighborEntryIterator interface { + Next() *NeighborEntry + HasNext() bool +} + +type NeighborSubscription struct { + done chan struct{} +} + +func (s *NeighborSubscription) Close() { + close(s.done) +} + +func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator { + entries := make([]*NeighborEntry, 0, len(table)) + for address, mac := range table { + entries = append(entries, &NeighborEntry{ + Address: address.String(), + MacAddress: mac.String(), + }) + } + return &neighborEntryIterator{entries} +} + +type neighborEntryIterator struct { + entries []*NeighborEntry +} + +func (i *neighborEntryIterator) HasNext() bool { + return len(i.entries) > 0 +} + +func (i *neighborEntryIterator) Next() *NeighborEntry { + if len(i.entries) == 0 { + return nil + } + entry := i.entries[0] + i.entries = i.entries[1:] + return entry +} diff --git a/experimental/libbox/neighbor_darwin.go b/experimental/libbox/neighbor_darwin.go new file mode 100644 index 00000000..d7484a69 --- /dev/null +++ b/experimental/libbox/neighbor_darwin.go @@ -0,0 +1,123 @@ +//go:build darwin + +package libbox + +import ( + "net" + "net/netip" + "os" + "slices" + "time" + + "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + + xroute "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { + entries, err := route.ReadNeighborEntries() + if err != nil { + return nil, E.Cause(err, "initial neighbor dump") + } + table := make(map[netip.Addr]net.HardwareAddr) + for _, entry := range entries { + table[entry.Address] = entry.MACAddress + } + listener.UpdateNeighborTable(tableToIterator(table)) + routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) + if err != nil { + return nil, E.Cause(err, "open route socket") + } + err = unix.SetNonblock(routeSocket, true) + if err != nil { + unix.Close(routeSocket) + return nil, E.Cause(err, "set route socket nonblock") + } + subscription := &NeighborSubscription{ + done: make(chan struct{}), + } + go subscription.loop(listener, routeSocket, table) + return subscription, nil +} + +func (s *NeighborSubscription) loop(listener NeighborUpdateListener, routeSocket int, table map[netip.Addr]net.HardwareAddr) { + routeSocketFile := os.NewFile(uintptr(routeSocket), "route") + defer routeSocketFile.Close() + buffer := buf.NewPacket() + defer buffer.Release() + for { + select { + case <-s.done: + return + default: + } + tv := unix.NsecToTimeval(int64(3 * time.Second)) + _ = unix.SetsockoptTimeval(routeSocket, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv) + n, err := routeSocketFile.Read(buffer.FreeBytes()) + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-s.done: + return + default: + } + continue + } + messages, err := xroute.ParseRIB(xroute.RIBTypeRoute, buffer.FreeBytes()[:n]) + if err != nil { + continue + } + changed := false + for _, message := range messages { + routeMessage, isRouteMessage := message.(*xroute.RouteMessage) + if !isRouteMessage { + continue + } + if routeMessage.Flags&unix.RTF_LLINFO == 0 { + continue + } + address, mac, isDelete, ok := route.ParseRouteNeighborMessage(routeMessage) + if !ok { + continue + } + if isDelete { + if _, exists := table[address]; exists { + delete(table, address) + changed = true + } + } else { + existing, exists := table[address] + if !exists || !slices.Equal(existing, mac) { + table[address] = mac + changed = true + } + } + } + if changed { + listener.UpdateNeighborTable(tableToIterator(table)) + } + } +} + +func ReadBootpdLeases() NeighborEntryIterator { + leaseIPToMAC, ipToHostname, macToHostname := route.ReloadLeaseFiles([]string{"/var/db/dhcpd_leases"}) + entries := make([]*NeighborEntry, 0, len(leaseIPToMAC)) + for address, mac := range leaseIPToMAC { + entry := &NeighborEntry{ + Address: address.String(), + MacAddress: mac.String(), + } + hostname, found := ipToHostname[address] + if !found { + hostname = macToHostname[mac.String()] + } + entry.Hostname = hostname + entries = append(entries, entry) + } + return &neighborEntryIterator{entries} +} diff --git a/experimental/libbox/neighbor_linux.go b/experimental/libbox/neighbor_linux.go new file mode 100644 index 00000000..ae10bdd2 --- /dev/null +++ b/experimental/libbox/neighbor_linux.go @@ -0,0 +1,88 @@ +//go:build linux + +package libbox + +import ( + "net" + "net/netip" + "slices" + "time" + + "github.com/sagernet/sing-box/route" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { + entries, err := route.ReadNeighborEntries() + if err != nil { + return nil, E.Cause(err, "initial neighbor dump") + } + table := make(map[netip.Addr]net.HardwareAddr) + for _, entry := range entries { + table[entry.Address] = entry.MACAddress + } + listener.UpdateNeighborTable(tableToIterator(table)) + connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ + Groups: 1 << (unix.RTNLGRP_NEIGH - 1), + }) + if err != nil { + return nil, E.Cause(err, "subscribe neighbor updates") + } + subscription := &NeighborSubscription{ + done: make(chan struct{}), + } + go subscription.loop(listener, connection, table) + return subscription, nil +} + +func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) { + defer connection.Close() + for { + select { + case <-s.done: + return + default: + } + err := connection.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return + } + messages, err := connection.Receive() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-s.done: + return + default: + } + continue + } + changed := false + for _, message := range messages { + address, mac, isDelete, ok := route.ParseNeighborMessage(message) + if !ok { + continue + } + if isDelete { + if _, exists := table[address]; exists { + delete(table, address) + changed = true + } + } else { + existing, exists := table[address] + if !exists || !slices.Equal(existing, mac) { + table[address] = mac + changed = true + } + } + } + if changed { + listener.UpdateNeighborTable(tableToIterator(table)) + } + } +} diff --git a/experimental/libbox/neighbor_stub.go b/experimental/libbox/neighbor_stub.go new file mode 100644 index 00000000..d465bc7b --- /dev/null +++ b/experimental/libbox/neighbor_stub.go @@ -0,0 +1,9 @@ +//go:build !linux && !darwin + +package libbox + +import "os" + +func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) { + return nil, os.ErrInvalid +} diff --git a/experimental/libbox/networkquality.go b/experimental/libbox/networkquality.go new file mode 100644 index 00000000..fcbe6f3a --- /dev/null +++ b/experimental/libbox/networkquality.go @@ -0,0 +1,74 @@ +package libbox + +import ( + "context" + "time" + + "github.com/sagernet/sing-box/common/networkquality" +) + +type NetworkQualityTest struct { + ctx context.Context + cancel context.CancelFunc +} + +func NewNetworkQualityTest() *NetworkQualityTest { + ctx, cancel := context.WithCancel(context.Background()) + return &NetworkQualityTest{ctx: ctx, cancel: cancel} +} + +func (t *NetworkQualityTest) Start(configURL string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) { + go func() { + httpClient := networkquality.NewHTTPClient(nil) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(nil, http3) + if err != nil { + handler.OnError(err.Error()) + return + } + + result, err := networkquality.Run(networkquality.Options{ + ConfigURL: configURL, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: serial, + MaxRuntime: time.Duration(maxRuntimeSeconds) * time.Second, + Context: t.ctx, + OnProgress: func(p networkquality.Progress) { + handler.OnProgress(&NetworkQualityProgress{ + Phase: int32(p.Phase), + DownloadCapacity: p.DownloadCapacity, + UploadCapacity: p.UploadCapacity, + DownloadRPM: p.DownloadRPM, + UploadRPM: p.UploadRPM, + IdleLatencyMs: p.IdleLatencyMs, + ElapsedMs: p.ElapsedMs, + DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(p.UploadRPMAccuracy), + }) + }, + }) + if err != nil { + handler.OnError(err.Error()) + return + } + handler.OnResult(&NetworkQualityResult{ + DownloadCapacity: result.DownloadCapacity, + UploadCapacity: result.UploadCapacity, + DownloadRPM: result.DownloadRPM, + UploadRPM: result.UploadRPM, + IdleLatencyMs: result.IdleLatencyMs, + DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(result.UploadRPMAccuracy), + }) + }() +} + +func (t *NetworkQualityTest) Cancel() { + t.cancel() +} diff --git a/experimental/libbox/oom_report.go b/experimental/libbox/oom_report.go new file mode 100644 index 00000000..e96c3e87 --- /dev/null +++ b/experimental/libbox/oom_report.go @@ -0,0 +1,141 @@ +//go:build darwin || linux || windows + +package libbox + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/sagernet/sing-box/experimental/libbox/internal/oomprofile" + "github.com/sagernet/sing-box/service/oomkiller" + "github.com/sagernet/sing/common/byteformats" + "github.com/sagernet/sing/common/memory" +) + +func init() { + sOOMReporter = &oomReporter{} +} + +var oomReportProfiles = []string{ + "allocs", + "block", + "goroutine", + "heap", + "mutex", + "threadcreate", +} + +type oomReportMetadata struct { + reportMetadata + RecordedAt string `json:"recordedAt"` + MemoryUsage string `json:"memoryUsage"` + AvailableMemory string `json:"availableMemory,omitempty"` + // Heap + HeapAlloc string `json:"heapAlloc,omitempty"` + HeapObjects uint64 `json:"heapObjects,omitempty,string"` + HeapInuse string `json:"heapInuse,omitempty"` + HeapIdle string `json:"heapIdle,omitempty"` + HeapReleased string `json:"heapReleased,omitempty"` + HeapSys string `json:"heapSys,omitempty"` + // Stack + StackInuse string `json:"stackInuse,omitempty"` + StackSys string `json:"stackSys,omitempty"` + // Runtime metadata + MSpanInuse string `json:"mSpanInuse,omitempty"` + MSpanSys string `json:"mSpanSys,omitempty"` + MCacheSys string `json:"mCacheSys,omitempty"` + BuckHashSys string `json:"buckHashSys,omitempty"` + GCSys string `json:"gcSys,omitempty"` + OtherSys string `json:"otherSys,omitempty"` + Sys string `json:"sys,omitempty"` + // GC & runtime + TotalAlloc string `json:"totalAlloc,omitempty"` + NumGC uint32 `json:"numGC,omitempty,string"` + NumGoroutine int `json:"numGoroutine,omitempty,string"` + NextGC string `json:"nextGC,omitempty"` + LastGC string `json:"lastGC,omitempty"` +} + +type oomReporter struct{} + +var _ oomkiller.OOMReporter = (*oomReporter)(nil) + +func (r *oomReporter) WriteReport(memoryUsage uint64) error { + now := time.Now().UTC() + reportsDir := filepath.Join(sWorkingPath, "oom_reports") + err := os.MkdirAll(reportsDir, 0o777) + if err != nil { + return err + } + chownReport(reportsDir) + + destPath, err := nextAvailableReportPath(reportsDir, now) + if err != nil { + return err + } + err = os.MkdirAll(destPath, 0o777) + if err != nil { + return err + } + chownReport(destPath) + + for _, name := range oomReportProfiles { + writeOOMProfile(destPath, name) + } + + writeReportFile(destPath, "cmdline", []byte(strings.Join(os.Args, "\000"))) + + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + + metadata := oomReportMetadata{ + reportMetadata: baseReportMetadata(), + RecordedAt: now.Format(time.RFC3339), + MemoryUsage: byteformats.FormatMemoryBytes(memoryUsage), + // Heap + HeapAlloc: byteformats.FormatMemoryBytes(memStats.HeapAlloc), + HeapObjects: memStats.HeapObjects, + HeapInuse: byteformats.FormatMemoryBytes(memStats.HeapInuse), + HeapIdle: byteformats.FormatMemoryBytes(memStats.HeapIdle), + HeapReleased: byteformats.FormatMemoryBytes(memStats.HeapReleased), + HeapSys: byteformats.FormatMemoryBytes(memStats.HeapSys), + // Stack + StackInuse: byteformats.FormatMemoryBytes(memStats.StackInuse), + StackSys: byteformats.FormatMemoryBytes(memStats.StackSys), + // Runtime metadata + MSpanInuse: byteformats.FormatMemoryBytes(memStats.MSpanInuse), + MSpanSys: byteformats.FormatMemoryBytes(memStats.MSpanSys), + MCacheSys: byteformats.FormatMemoryBytes(memStats.MCacheSys), + BuckHashSys: byteformats.FormatMemoryBytes(memStats.BuckHashSys), + GCSys: byteformats.FormatMemoryBytes(memStats.GCSys), + OtherSys: byteformats.FormatMemoryBytes(memStats.OtherSys), + Sys: byteformats.FormatMemoryBytes(memStats.Sys), + // GC & runtime + TotalAlloc: byteformats.FormatMemoryBytes(memStats.TotalAlloc), + NumGC: memStats.NumGC, + NumGoroutine: runtime.NumGoroutine(), + NextGC: byteformats.FormatMemoryBytes(memStats.NextGC), + } + if memStats.LastGC > 0 { + metadata.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339) + } + availableMemory := memory.Available() + if availableMemory > 0 { + metadata.AvailableMemory = byteformats.FormatMemoryBytes(availableMemory) + } + writeReportMetadata(destPath, metadata) + copyConfigSnapshot(destPath) + + return nil +} + +func writeOOMProfile(destPath string, name string) { + filePath, err := oomprofile.WriteFile(destPath, name) + if err != nil { + return + } + chownReport(filePath) +} diff --git a/experimental/libbox/panic.go b/experimental/libbox/panic.go new file mode 100644 index 00000000..62fe1326 --- /dev/null +++ b/experimental/libbox/panic.go @@ -0,0 +1,12 @@ +package libbox + +// https://github.com/golang/go/issues/46893 +// TODO: remove after `bulkBarrierPreWrite: unaligned arguments` fixed + +type StringBox struct { + Value string +} + +func wrapString(value string) *StringBox { + return &StringBox{Value: value} +} diff --git a/experimental/libbox/pidfd_android.go b/experimental/libbox/pidfd_android.go new file mode 100644 index 00000000..cc7cdd7a --- /dev/null +++ b/experimental/libbox/pidfd_android.go @@ -0,0 +1,19 @@ +package libbox + +import ( + "os" + _ "unsafe" +) + +// https://github.com/SagerNet/sing-box/issues/3233 +// https://github.com/golang/go/issues/70508 +// https://github.com/tailscale/tailscale/issues/13452 + +//go:linkname checkPidfdOnce os.checkPidfdOnce +var checkPidfdOnce func() error + +func init() { + checkPidfdOnce = func() error { + return os.ErrInvalid + } +} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go new file mode 100644 index 00000000..e65d0818 --- /dev/null +++ b/experimental/libbox/platform.go @@ -0,0 +1,141 @@ +package libbox + +import ( + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +type PlatformInterface interface { + LocalDNSTransport() LocalDNSTransport + UsePlatformAutoDetectInterfaceControl() bool + AutoDetectInterfaceControl(fd int32) error + OpenTun(options TunOptions) (int32, error) + UseProcFS() bool + FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) + StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error + CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error + GetInterfaces() (NetworkInterfaceIterator, error) + UnderNetworkExtension() bool + IncludeAllNetworks() bool + ReadWIFIState() *WIFIState + SystemCertificates() StringIterator + ClearDNSCache() + SendNotification(notification *Notification) error + StartNeighborMonitor(listener NeighborUpdateListener) error + CloseNeighborMonitor(listener NeighborUpdateListener) error + RegisterMyInterface(name string) +} + +type NeighborUpdateListener interface { + UpdateNeighborTable(entries NeighborEntryIterator) +} + +type ConnectionOwner struct { + UserId int32 + UserName string + ProcessPath string + androidPackageNames []string +} + +func (c *ConnectionOwner) SetAndroidPackageNames(names StringIterator) { + c.androidPackageNames = iteratorToArray[string](names) +} + +func (c *ConnectionOwner) AndroidPackageNames() StringIterator { + return newIterator(c.androidPackageNames) +} + +type InterfaceUpdateListener interface { + UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool) +} + +const ( + InterfaceTypeWIFI = int32(C.InterfaceTypeWIFI) + InterfaceTypeCellular = int32(C.InterfaceTypeCellular) + InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet) + InterfaceTypeOther = int32(C.InterfaceTypeOther) +) + +type NetworkInterface struct { + Index int32 + MTU int32 + Name string + Addresses StringIterator + Flags int32 + + Type int32 + DNSServer StringIterator + Metered bool +} + +type WIFIState struct { + SSID string + BSSID string +} + +func NewWIFIState(wifiSSID string, wifiBSSID string) *WIFIState { + return &WIFIState{wifiSSID, wifiBSSID} +} + +type NetworkInterfaceIterator interface { + Next() *NetworkInterface + HasNext() bool +} + +type Notification struct { + Identifier string + TypeName string + TypeID int32 + Title string + Subtitle string + Body string + OpenURL string +} + +type OnDemandRule interface { + Target() int32 + DNSSearchDomainMatch() StringIterator + DNSServerAddressMatch() StringIterator + InterfaceTypeMatch() int32 + SSIDMatch() StringIterator + ProbeURL() string +} + +type OnDemandRuleIterator interface { + Next() OnDemandRule + HasNext() bool +} + +type onDemandRule struct { + option.OnDemandRule +} + +func (r *onDemandRule) Target() int32 { + if r.OnDemandRule.Action == nil { + return -1 + } + return int32(*r.OnDemandRule.Action) +} + +func (r *onDemandRule) DNSSearchDomainMatch() StringIterator { + return newIterator(r.OnDemandRule.DNSSearchDomainMatch) +} + +func (r *onDemandRule) DNSServerAddressMatch() StringIterator { + return newIterator(r.OnDemandRule.DNSServerAddressMatch) +} + +func (r *onDemandRule) InterfaceTypeMatch() int32 { + if r.OnDemandRule.InterfaceTypeMatch == nil { + return -1 + } + return int32(*r.OnDemandRule.InterfaceTypeMatch) +} + +func (r *onDemandRule) SSIDMatch() StringIterator { + return newIterator(r.OnDemandRule.SSIDMatch) +} + +func (r *onDemandRule) ProbeURL() string { + return r.OnDemandRule.ProbeURL +} diff --git a/experimental/libbox/pprof.go b/experimental/libbox/pprof.go new file mode 100644 index 00000000..d6d07800 --- /dev/null +++ b/experimental/libbox/pprof.go @@ -0,0 +1,33 @@ +package libbox + +import ( + "net" + "net/http" + _ "net/http/pprof" + "strconv" +) + +type PProfServer struct { + server *http.Server +} + +func NewPProfServer(port int) *PProfServer { + return &PProfServer{ + &http.Server{ + Addr: ":" + strconv.Itoa(port), + }, + } +} + +func (s *PProfServer) Start() error { + ln, err := net.Listen("tcp", s.server.Addr) + if err != nil { + return err + } + go s.server.Serve(ln) + return nil +} + +func (s *PProfServer) Close() error { + return s.server.Close() +} diff --git a/experimental/libbox/profile_import.go b/experimental/libbox/profile_import.go new file mode 100644 index 00000000..c337d015 --- /dev/null +++ b/experimental/libbox/profile_import.go @@ -0,0 +1,278 @@ +package libbox + +import ( + "bufio" + "bytes" + "compress/gzip" + "encoding/binary" + "io" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/varbin" +) + +func EncodeChunkedMessage(data []byte) []byte { + var buffer bytes.Buffer + binary.Write(&buffer, binary.BigEndian, uint16(len(data))) + buffer.Write(data) + return buffer.Bytes() +} + +func DecodeLengthChunk(data []byte) int32 { + return int32(binary.BigEndian.Uint16(data)) +} + +const ( + MessageTypeError = iota + MessageTypeProfileList + MessageTypeProfileContentRequest + MessageTypeProfileContent +) + +type ErrorMessage struct { + Message string +} + +func (e *ErrorMessage) Encode() []byte { + var buffer bytes.Buffer + buffer.WriteByte(MessageTypeError) + writeString(&buffer, e.Message) + return buffer.Bytes() +} + +func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { + reader := bytes.NewReader(data) + messageType, err := reader.ReadByte() + if err != nil { + return nil, err + } + if messageType != MessageTypeError { + return nil, E.New("invalid message") + } + var message ErrorMessage + message.Message, err = readString(reader) + if err != nil { + return nil, err + } + return &message, nil +} + +const ( + ProfileTypeLocal int32 = iota + ProfileTypeiCloud + ProfileTypeRemote +) + +type ProfilePreview struct { + ProfileID int64 + Name string + Type int32 +} + +type ProfilePreviewIterator interface { + Next() *ProfilePreview + HasNext() bool +} + +type ProfileEncoder struct { + profiles []ProfilePreview +} + +func (e *ProfileEncoder) Append(profile *ProfilePreview) { + e.profiles = append(e.profiles, *profile) +} + +func (e *ProfileEncoder) Encode() []byte { + var buffer bytes.Buffer + buffer.WriteByte(MessageTypeProfileList) + binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles))) + for _, preview := range e.profiles { + binary.Write(&buffer, binary.BigEndian, preview.ProfileID) + writeString(&buffer, preview.Name) + binary.Write(&buffer, binary.BigEndian, preview.Type) + } + return buffer.Bytes() +} + +type ProfileDecoder struct { + profiles []*ProfilePreview +} + +func (d *ProfileDecoder) Decode(data []byte) error { + reader := bytes.NewReader(data) + messageType, err := reader.ReadByte() + if err != nil { + return err + } + if messageType != MessageTypeProfileList { + return E.New("invalid message") + } + var profileCount uint16 + err = binary.Read(reader, binary.BigEndian, &profileCount) + if err != nil { + return err + } + for i := 0; i < int(profileCount); i++ { + var profile ProfilePreview + err = binary.Read(reader, binary.BigEndian, &profile.ProfileID) + if err != nil { + return err + } + profile.Name, err = readString(reader) + if err != nil { + return err + } + err = binary.Read(reader, binary.BigEndian, &profile.Type) + if err != nil { + return err + } + d.profiles = append(d.profiles, &profile) + } + return nil +} + +func (d *ProfileDecoder) Iterator() ProfilePreviewIterator { + return newIterator(d.profiles) +} + +type ProfileContentRequest struct { + ProfileID int64 +} + +func (r *ProfileContentRequest) Encode() []byte { + var buffer bytes.Buffer + buffer.WriteByte(MessageTypeProfileContentRequest) + binary.Write(&buffer, binary.BigEndian, r.ProfileID) + return buffer.Bytes() +} + +func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) { + reader := bytes.NewReader(data) + messageType, err := reader.ReadByte() + if err != nil { + return nil, err + } + if messageType != MessageTypeProfileContentRequest { + return nil, E.New("invalid message") + } + var request ProfileContentRequest + err = binary.Read(reader, binary.BigEndian, &request.ProfileID) + if err != nil { + return nil, err + } + return &request, nil +} + +type ProfileContent struct { + Name string + Type int32 + Config string + RemotePath string + AutoUpdate bool + AutoUpdateInterval int32 + LastUpdated int64 +} + +func (c *ProfileContent) Encode() []byte { + buffer := new(bytes.Buffer) + buffer.WriteByte(MessageTypeProfileContent) + buffer.WriteByte(1) + gWriter := gzip.NewWriter(buffer) + writer := bufio.NewWriter(gWriter) + writeStringBuffered(writer, c.Name) + binary.Write(writer, binary.BigEndian, c.Type) + writeStringBuffered(writer, c.Config) + if c.Type != ProfileTypeLocal { + writeStringBuffered(writer, c.RemotePath) + } + if c.Type == ProfileTypeRemote { + binary.Write(writer, binary.BigEndian, c.AutoUpdate) + binary.Write(writer, binary.BigEndian, c.AutoUpdateInterval) + binary.Write(writer, binary.BigEndian, c.LastUpdated) + } + writer.Flush() + gWriter.Flush() + gWriter.Close() + return buffer.Bytes() +} + +func DecodeProfileContent(data []byte) (*ProfileContent, error) { + reader := bytes.NewReader(data) + messageType, err := reader.ReadByte() + if err != nil { + return nil, err + } + if messageType != MessageTypeProfileContent { + return nil, E.New("invalid message") + } + version, err := reader.ReadByte() + if err != nil { + return nil, err + } + gReader, err := gzip.NewReader(reader) + if err != nil { + return nil, E.Cause(err, "unsupported profile") + } + bReader := varbin.StubReader(gReader) + var content ProfileContent + content.Name, err = readString(bReader) + if err != nil { + return nil, err + } + err = binary.Read(bReader, binary.BigEndian, &content.Type) + if err != nil { + return nil, err + } + content.Config, err = readString(bReader) + if err != nil { + return nil, err + } + if content.Type != ProfileTypeLocal { + content.RemotePath, err = readString(bReader) + if err != nil { + return nil, err + } + } + if content.Type == ProfileTypeRemote || (version == 0 && content.Type != ProfileTypeLocal) { + err = binary.Read(bReader, binary.BigEndian, &content.AutoUpdate) + if err != nil { + return nil, err + } + if version >= 1 { + err = binary.Read(bReader, binary.BigEndian, &content.AutoUpdateInterval) + if err != nil { + return nil, err + } + } + err = binary.Read(bReader, binary.BigEndian, &content.LastUpdated) + if err != nil { + return nil, err + } + } + return &content, nil +} + +func readString(reader io.ByteReader) (string, error) { + length, err := binary.ReadUvarint(reader) + if err != nil { + return "", err + } + buf := make([]byte, length) + for i := range buf { + buf[i], err = reader.ReadByte() + if err != nil { + return "", err + } + } + return string(buf), nil +} + +func writeString(buffer *bytes.Buffer, value string) { + varbin.WriteUvarint(buffer, uint64(len(value))) + buffer.WriteString(value) +} + +func writeStringBuffered(writer *bufio.Writer, value string) { + varbin.WriteUvarint(writer, uint64(len(value))) + writer.WriteString(value) +} diff --git a/experimental/libbox/remote_profile.go b/experimental/libbox/remote_profile.go new file mode 100644 index 00000000..19c1256a --- /dev/null +++ b/experimental/libbox/remote_profile.go @@ -0,0 +1,41 @@ +package libbox + +import ( + "net/url" +) + +func GenerateRemoteProfileImportLink(name string, remoteURL string) string { + importLink := &url.URL{ + Scheme: "sing-box", + Host: "import-remote-profile", + RawQuery: url.Values{"url": []string{remoteURL}}.Encode(), + Fragment: name, + } + return importLink.String() +} + +type ImportRemoteProfile struct { + Name string + URL string + Host string +} + +func ParseRemoteProfileImportLink(importLink string) (*ImportRemoteProfile, error) { + importURL, err := url.Parse(importLink) + if err != nil { + return nil, err + } + remoteURL, err := url.Parse(importURL.Query().Get("url")) + if err != nil { + return nil, err + } + name := importURL.Fragment + if name == "" { + name = remoteURL.Host + } + return &ImportRemoteProfile{ + Name: name, + URL: remoteURL.String(), + Host: remoteURL.Host, + }, nil +} diff --git a/experimental/libbox/report.go b/experimental/libbox/report.go new file mode 100644 index 00000000..816dcac4 --- /dev/null +++ b/experimental/libbox/report.go @@ -0,0 +1,97 @@ +//go:build darwin || linux || windows + +package libbox + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "runtime" + "strconv" + "time" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" +) + +type reportMetadata struct { + Source string `json:"source,omitempty"` + BundleIdentifier string `json:"bundleIdentifier,omitempty"` + ProcessName string `json:"processName,omitempty"` + ProcessPath string `json:"processPath,omitempty"` + StartedAt string `json:"startedAt,omitempty"` + AppVersion string `json:"appVersion,omitempty"` + AppMarketingVersion string `json:"appMarketingVersion,omitempty"` + CoreVersion string `json:"coreVersion,omitempty"` + GoVersion string `json:"goVersion,omitempty"` +} + +func baseReportMetadata() reportMetadata { + processPath, _ := os.Executable() + processName := filepath.Base(processPath) + if processName == "." { + processName = "" + } + return reportMetadata{ + Source: sCrashReportSource, + ProcessName: processName, + ProcessPath: processPath, + CoreVersion: C.Version, + GoVersion: GoVersion(), + } +} + +func writeReportFile(destPath string, name string, content []byte) { + filePath := filepath.Join(destPath, name) + os.WriteFile(filePath, content, 0o666) + chownReport(filePath) +} + +func writeReportMetadata(destPath string, metadata any) { + data, err := json.Marshal(metadata) + if err != nil { + return + } + writeReportFile(destPath, "metadata.json", data) +} + +func copyConfigSnapshot(destPath string) { + snapshotPath := configSnapshotPath() + content, err := os.ReadFile(snapshotPath) + if err != nil { + return + } + if len(bytes.TrimSpace(content)) == 0 { + return + } + writeReportFile(destPath, "configuration.json", content) +} + +func initReportDir(path string) { + os.MkdirAll(path, 0o777) + chownReport(path) +} + +func chownReport(path string) { + if runtime.GOOS != "android" && runtime.GOOS != "windows" { + os.Chown(path, sUserID, sGroupID) + } +} + +func nextAvailableReportPath(reportsDir string, timestamp time.Time) (string, error) { + destName := timestamp.Format("2006-01-02T15-04-05") + destPath := filepath.Join(reportsDir, destName) + _, err := os.Stat(destPath) + if os.IsNotExist(err) { + return destPath, nil + } + for i := 1; i <= 1000; i++ { + suffixedPath := filepath.Join(reportsDir, destName+"-"+strconv.Itoa(i)) + _, err = os.Stat(suffixedPath) + if os.IsNotExist(err) { + return suffixedPath, nil + } + } + return "", E.New("no available report path for ", destName) +} diff --git a/experimental/libbox/semver.go b/experimental/libbox/semver.go new file mode 100644 index 00000000..b0919222 --- /dev/null +++ b/experimental/libbox/semver.go @@ -0,0 +1,27 @@ +package libbox + +import ( + "strings" + + "golang.org/x/mod/semver" +) + +func CompareSemver(left string, right string) bool { + normalizedLeft := normalizeSemver(left) + if !semver.IsValid(normalizedLeft) { + return false + } + normalizedRight := normalizeSemver(right) + if !semver.IsValid(normalizedRight) { + return false + } + return semver.Compare(normalizedLeft, normalizedRight) > 0 +} + +func normalizeSemver(version string) string { + trimmedVersion := strings.TrimSpace(version) + if strings.HasPrefix(trimmedVersion, "v") { + return trimmedVersion + } + return "v" + trimmedVersion +} diff --git a/experimental/libbox/semver_test.go b/experimental/libbox/semver_test.go new file mode 100644 index 00000000..f76093b4 --- /dev/null +++ b/experimental/libbox/semver_test.go @@ -0,0 +1,16 @@ +package libbox + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCompareSemver(t *testing.T) { + t.Parallel() + + require.False(t, CompareSemver("1.13.0-rc.4", "1.13.0")) + require.True(t, CompareSemver("1.13.1", "1.13.0")) + require.False(t, CompareSemver("v1.13.0", "1.13.0")) + require.False(t, CompareSemver("1.13.0-", "1.13.0")) +} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go new file mode 100644 index 00000000..7becf9fa --- /dev/null +++ b/experimental/libbox/service.go @@ -0,0 +1,288 @@ +package libbox + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "net" + "net/netip" + "runtime" + "strconv" + "sync" + "syscall" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/libbox/internal/procfs" + "github.com/sagernet/sing-box/option" + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil) + +type platformInterfaceWrapper struct { + iif PlatformInterface + useProcFS bool + networkManager adapter.NetworkManager + myTunName string + defaultInterfaceAccess sync.Mutex + defaultInterface *control.Interface + isExpensive bool + isConstrained bool +} + +func (w *platformInterfaceWrapper) Initialize(networkManager adapter.NetworkManager) error { + w.networkManager = networkManager + return nil +} + +func (w *platformInterfaceWrapper) UsePlatformAutoDetectInterfaceControl() bool { + return w.iif.UsePlatformAutoDetectInterfaceControl() +} + +func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error { + return w.iif.AutoDetectInterfaceControl(int32(fd)) +} + +func (w *platformInterfaceWrapper) UsePlatformInterface() bool { + return true +} + +func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { + if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 { + return nil, E.New("platform: unsupported uid options") + } + if len(options.IncludeAndroidUser) > 0 { + return nil, E.New("platform: unsupported android_user option") + } + routeRanges, err := options.BuildAutoRouteRanges(true) + if err != nil { + return nil, err + } + tunFd, err := w.iif.OpenTun(&tunOptions{options, routeRanges, platformOptions}) + if err != nil { + return nil, err + } + options.Name, err = getTunnelName(tunFd) + 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") + } + options.FileDescriptor = dupFd + w.myTunName = options.Name + w.iif.RegisterMyInterface(options.Name) + return tun.New(*options) +} + +func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { + return true +} + +func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { + return &platformDefaultInterfaceMonitor{ + platformInterfaceWrapper: w, + logger: logger, + } +} + +func (w *platformInterfaceWrapper) UsePlatformNetworkInterfaces() bool { + return true +} + +func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) { + interfaceIterator, err := w.iif.GetInterfaces() + if err != nil { + return nil, err + } + var interfaces []adapter.NetworkInterface + for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) { + if netInterface.Name == w.myTunName { + continue + } + w.defaultInterfaceAccess.Lock() + // (GOOS=windows) SA4006: this value of `isDefault` is never used + // Why not used? + //nolint:staticcheck + isDefault := w.defaultInterface != nil && int(netInterface.Index) == w.defaultInterface.Index + w.defaultInterfaceAccess.Unlock() + interfaces = append(interfaces, adapter.NetworkInterface{ + Interface: control.Interface{ + Index: int(netInterface.Index), + MTU: int(netInterface.MTU), + Name: netInterface.Name, + Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), + Flags: linkFlags(uint32(netInterface.Flags)), + }, + Type: C.InterfaceType(netInterface.Type), + DNSServers: iteratorToArray[string](netInterface.DNSServer), + Expensive: netInterface.Metered || isDefault && w.isExpensive, + Constrained: isDefault && w.isConstrained, + }) + } + interfaces = common.UniqBy(interfaces, func(it adapter.NetworkInterface) string { + return it.Name + }) + return interfaces, nil +} + +func (w *platformInterfaceWrapper) UnderNetworkExtension() bool { + return w.iif.UnderNetworkExtension() +} + +func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool { + return w.iif.IncludeAllNetworks() +} + +func (w *platformInterfaceWrapper) ClearDNSCache() { + w.iif.ClearDNSCache() +} + +func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error { + return nil +} + +func (w *platformInterfaceWrapper) UsePlatformWIFIMonitor() bool { + return true +} + +func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { + wifiState := w.iif.ReadWIFIState() + if wifiState == nil { + return adapter.WIFIState{} + } + return (adapter.WIFIState)(*wifiState) +} + +func (w *platformInterfaceWrapper) SystemCertificates() []string { + return iteratorToArray[string](w.iif.SystemCertificates()) +} + +func (w *platformInterfaceWrapper) UsePlatformConnectionOwnerFinder() bool { + return true +} + +func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) { + if w.useProcFS { + var source netip.AddrPort + var destination netip.AddrPort + sourceAddr, _ := netip.ParseAddr(request.SourceAddress) + source = netip.AddrPortFrom(sourceAddr, uint16(request.SourcePort)) + destAddr, _ := netip.ParseAddr(request.DestinationAddress) + destination = netip.AddrPortFrom(destAddr, uint16(request.DestinationPort)) + + var network string + switch request.IpProtocol { + case int32(syscall.IPPROTO_TCP): + network = "tcp" + case int32(syscall.IPPROTO_UDP): + network = "udp" + default: + return nil, E.New("unknown protocol: ", request.IpProtocol) + } + + uid := procfs.ResolveSocketByProcSearch(network, source, destination) + if uid == -1 { + return nil, E.New("procfs: not found") + } + return &adapter.ConnectionOwner{ + UserId: uid, + }, nil + } + + result, err := w.iif.FindConnectionOwner(request.IpProtocol, request.SourceAddress, request.SourcePort, request.DestinationAddress, request.DestinationPort) + if err != nil { + return nil, err + } + return &adapter.ConnectionOwner{ + UserId: result.UserId, + UserName: result.UserName, + ProcessPath: result.ProcessPath, + AndroidPackageNames: result.androidPackageNames, + }, nil +} + +func (w *platformInterfaceWrapper) DisableColors() bool { + return runtime.GOOS != "android" +} + +func (w *platformInterfaceWrapper) UsePlatformNotification() bool { + return true +} + +func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notification) error { + return w.iif.SendNotification((*Notification)(notification)) +} + +func (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool { + return true +} + +func (w *platformInterfaceWrapper) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return w.iif.StartNeighborMonitor(&neighborUpdateListenerWrapper{listener: listener}) +} + +func (w *platformInterfaceWrapper) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return w.iif.CloseNeighborMonitor(nil) +} + +type neighborUpdateListenerWrapper struct { + listener adapter.NeighborUpdateListener +} + +func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntryIterator) { + var result []adapter.NeighborEntry + for entries.HasNext() { + entry := entries.Next() + if entry == nil { + continue + } + address, err := netip.ParseAddr(entry.Address) + if err != nil { + continue + } + macAddress, err := net.ParseMAC(entry.MacAddress) + if err != nil { + continue + } + result = append(result, adapter.NeighborEntry{ + Address: address, + MACAddress: macAddress, + Hostname: entry.Hostname, + }) + } + w.listener.UpdateNeighborTable(result) +} + +func AvailablePort(startPort int32) (int32, error) { + for port := int(startPort); ; port++ { + if port > 65535 { + return 0, E.New("no available port found") + } + listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))) + if err != nil { + if errors.Is(err, syscall.EADDRINUSE) { + continue + } + return 0, E.Cause(err, "find available port") + } + err = listener.Close() + if err != nil { + return 0, E.Cause(err, "close listener") + } + return int32(port), nil + } +} + +func RandomHex(length int32) *StringBox { + bytes := make([]byte, length) + common.Must1(rand.Read(bytes)) + return wrapString(hex.EncodeToString(bytes)) +} diff --git a/experimental/libbox/service_other.go b/experimental/libbox/service_other.go new file mode 100644 index 00000000..9ea68335 --- /dev/null +++ b/experimental/libbox/service_other.go @@ -0,0 +1,9 @@ +//go:build !windows + +package libbox + +import "syscall" + +func dup(fd int) (nfd int, err error) { + return syscall.Dup(fd) +} diff --git a/experimental/libbox/service_windows.go b/experimental/libbox/service_windows.go new file mode 100644 index 00000000..2dc3b645 --- /dev/null +++ b/experimental/libbox/service_windows.go @@ -0,0 +1,7 @@ +package libbox + +import "os" + +func dup(fd int) (nfd int, err error) { + return 0, os.ErrInvalid +} diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go new file mode 100644 index 00000000..9f8aa03c --- /dev/null +++ b/experimental/libbox/setup.go @@ -0,0 +1,191 @@ +package libbox + +import ( + "math" + "os" + "path/filepath" + "runtime" + "runtime/debug" + "strings" + "time" + + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/common/stun" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/experimental/locale" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/service/oomkiller" + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" +) + +var ( + sBasePath string + sWorkingPath string + sTempPath string + sUserID int + sGroupID int + sFixAndroidStack bool + sCommandServerListenPort uint16 + sCommandServerSecret string + sLogMaxLines int + sDebug bool + sCrashReportSource string + sOOMKillerEnabled bool + sOOMKillerDisabled bool + sOOMMemoryLimit int64 +) + +func init() { + debug.SetPanicOnFault(true) + debug.SetTraceback("all") +} + +type SetupOptions struct { + BasePath string + WorkingPath string + TempPath string + FixAndroidStack bool + CommandServerListenPort int32 + CommandServerSecret string + LogMaxLines int + Debug bool + CrashReportSource string + OomKillerEnabled bool + OomKillerDisabled bool + OomMemoryLimit int64 +} + +func applySetupOptions(options *SetupOptions) { + sBasePath = options.BasePath + sWorkingPath = options.WorkingPath + sTempPath = options.TempPath + + sUserID = os.Getuid() + sGroupID = os.Getgid() + + // TODO: remove after fixed + // https://github.com/golang/go/issues/68760 + sFixAndroidStack = options.FixAndroidStack + + sCommandServerListenPort = uint16(options.CommandServerListenPort) + sCommandServerSecret = options.CommandServerSecret + sLogMaxLines = options.LogMaxLines + sDebug = options.Debug + sCrashReportSource = options.CrashReportSource + ReloadSetupOptions(options) +} + +func ReloadSetupOptions(options *SetupOptions) { + sOOMKillerEnabled = options.OomKillerEnabled + sOOMKillerDisabled = options.OomKillerDisabled + sOOMMemoryLimit = options.OomMemoryLimit + if sOOMKillerEnabled { + if sOOMMemoryLimit == 0 && C.IsIos { + sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit + } + if sOOMMemoryLimit > 0 { + debug.SetMemoryLimit(sOOMMemoryLimit * 3 / 4) + } else { + debug.SetMemoryLimit(math.MaxInt64) + } + } else { + debug.SetMemoryLimit(math.MaxInt64) + } +} + +func Setup(options *SetupOptions) error { + applySetupOptions(options) + os.MkdirAll(sWorkingPath, 0o777) + os.MkdirAll(sTempPath, 0o777) + return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log")) +} + +func SetLocale(localeId string) error { + if strings.Contains(localeId, "@") { + localeId = strings.Split(localeId, "@")[0] + } + if !locale.Set(localeId) { + return E.New("unsupported locale: ", localeId) + } + return nil +} + +func Version() string { + return C.Version +} + +func GoVersion() string { + return runtime.Version() + ", " + runtime.GOOS + "/" + runtime.GOARCH +} + +func FormatBytes(length int64) string { + return byteformats.FormatKBytes(uint64(length)) +} + +func FormatMemoryBytes(length int64) string { + return byteformats.FormatMemoryKBytes(uint64(length)) +} + +func FormatDuration(duration int64) string { + return log.FormatDuration(time.Duration(duration) * time.Millisecond) +} + +func FormatBitrate(bps int64) string { + return networkquality.FormatBitrate(bps) +} + +const NetworkQualityDefaultConfigURL = networkquality.DefaultConfigURL + +const NetworkQualityDefaultMaxRuntimeSeconds = int32(networkquality.DefaultMaxRuntime / time.Second) + +const ( + NetworkQualityAccuracyLow = int32(networkquality.AccuracyLow) + NetworkQualityAccuracyMedium = int32(networkquality.AccuracyMedium) + NetworkQualityAccuracyHigh = int32(networkquality.AccuracyHigh) +) + +const ( + NetworkQualityPhaseIdle = int32(networkquality.PhaseIdle) + NetworkQualityPhaseDownload = int32(networkquality.PhaseDownload) + NetworkQualityPhaseUpload = int32(networkquality.PhaseUpload) + NetworkQualityPhaseDone = int32(networkquality.PhaseDone) +) + +const STUNDefaultServer = stun.DefaultServer + +const ( + STUNPhaseBinding = int32(stun.PhaseBinding) + STUNPhaseNATMapping = int32(stun.PhaseNATMapping) + STUNPhaseNATFiltering = int32(stun.PhaseNATFiltering) + STUNPhaseDone = int32(stun.PhaseDone) +) + +const ( + NATMappingEndpointIndependent = int32(stun.NATMappingEndpointIndependent) + NATMappingAddressDependent = int32(stun.NATMappingAddressDependent) + NATMappingAddressAndPortDependent = int32(stun.NATMappingAddressAndPortDependent) +) + +const ( + NATFilteringEndpointIndependent = int32(stun.NATFilteringEndpointIndependent) + NATFilteringAddressDependent = int32(stun.NATFilteringAddressDependent) + NATFilteringAddressAndPortDependent = int32(stun.NATFilteringAddressAndPortDependent) +) + +func FormatNATMapping(value int32) string { + return stun.NATMapping(value).String() +} + +func FormatNATFiltering(value int32) string { + return stun.NATFiltering(value).String() +} + +func FormatFQDN(fqdn string) string { + return dns.FqdnToDomain(fqdn) +} + +func ProxyDisplayType(proxyType string) string { + return C.ProxyDisplayName(proxyType) +} diff --git a/experimental/libbox/signal_handler_darwin.go b/experimental/libbox/signal_handler_darwin.go new file mode 100644 index 00000000..a60ddd90 --- /dev/null +++ b/experimental/libbox/signal_handler_darwin.go @@ -0,0 +1,146 @@ +//go:build darwin && badlinkname + +package libbox + +/* +#include +#include +#include + +static struct sigaction _go_sa[32]; +static struct sigaction _plcrash_sa[32]; +static int _saved = 0; + +static int _signals[] = {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP}; +static const int _signal_count = sizeof(_signals) / sizeof(_signals[0]); + +static void _save_go_handlers(void) { + if (_saved) return; + for (int i = 0; i < _signal_count; i++) + sigaction(_signals[i], NULL, &_go_sa[_signals[i]]); + _saved = 1; +} + +static void _combined_handler(int sig, siginfo_t *info, void *uap) { + // Step 1: PLCrashReporter writes .plcrash, resets all handlers to SIG_DFL, + // and calls raise(sig) which pends (signal is blocked, no SA_NODEFER). + if ((_plcrash_sa[sig].sa_flags & SA_SIGINFO) && + (uintptr_t)_plcrash_sa[sig].sa_sigaction > 1) + _plcrash_sa[sig].sa_sigaction(sig, info, uap); + + // SIGTRAP does not rely on sigreturn -> sigpanic. Once Go's trap trampoline + // is force-installed, we can chain into it directly after PLCrashReporter. + if (sig == SIGTRAP && + (_go_sa[sig].sa_flags & SA_SIGINFO) && + (uintptr_t)_go_sa[sig].sa_sigaction > 1) { + _go_sa[sig].sa_sigaction(sig, info, uap); + return; + } + + // Step 2: Restore Go's handler via sigaction (overwrites PLCrashReporter's SIG_DFL). + // Do NOT call Go's handler directly — Go's preparePanic only modifies the + // ucontext and returns. The actual crash output is written by sigpanic, which + // only runs when the KERNEL restores the modified ucontext via sigreturn. + // A direct C function call has no sigreturn, so sigpanic would never execute. + sigaction(sig, &_go_sa[sig], NULL); + + // Step 3: Return. The kernel restores the original ucontext and re-executes + // the faulting instruction. Two signals are now pending/imminent: + // a) PLCrashReporter's raise() (SI_USER) — Go's handler ignores it + // (sighandler: sigFromUser() → return). + // b) The re-executed fault (SEGV_MAPERR) — Go's handler processes it: + // preparePanic → kernel sigreturn → sigpanic → crash output written + // via debug.SetCrashOutput. +} + +static void _reinstall_handlers(void) { + if (!_saved) return; + for (int i = 0; i < _signal_count; i++) { + int sig = _signals[i]; + struct sigaction current; + sigaction(sig, NULL, ¤t); + // Only save the handler if it's not one of ours + if (current.sa_sigaction != _combined_handler) { + // If current handler is still Go's, PLCrashReporter wasn't installed + if ((current.sa_flags & SA_SIGINFO) && + (uintptr_t)current.sa_sigaction > 1 && + current.sa_sigaction == _go_sa[sig].sa_sigaction) + memset(&_plcrash_sa[sig], 0, sizeof(_plcrash_sa[sig])); + else + _plcrash_sa[sig] = current; + } + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = _combined_handler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + sigaction(sig, &sa, NULL); + } +} +*/ +import "C" + +import ( + "reflect" + _ "unsafe" +) + +const ( + _sigtrap = 5 + _nsig = 32 +) + +//go:linkname runtimeGetsig runtime.getsig +func runtimeGetsig(i uint32) uintptr + +//go:linkname runtimeSetsig runtime.setsig +func runtimeSetsig(i uint32, fn uintptr) + +//go:linkname runtimeCgoSigtramp runtime.cgoSigtramp +func runtimeCgoSigtramp() + +//go:linkname runtimeFwdSig runtime.fwdSig +var runtimeFwdSig [_nsig]uintptr + +//go:linkname runtimeHandlingSig runtime.handlingSig +var runtimeHandlingSig [_nsig]uint32 + +func forceGoSIGTRAPHandler() { + runtimeFwdSig[_sigtrap] = runtimeGetsig(_sigtrap) + runtimeHandlingSig[_sigtrap] = 1 + runtimeSetsig(_sigtrap, reflect.ValueOf(runtimeCgoSigtramp).Pointer()) +} + +// PrepareCrashSignalHandlers captures Go's original synchronous signal handlers. +// +// In gomobile/c-archive embeddings, package init runs on the first Go entry. +// That means a native crash reporter installed before the first Go call would +// otherwise be captured as the "Go" handler and break handler restoration on +// SIGSEGV. Go skips SIGTRAP in c-archive mode, so install its trap trampoline +// before saving handlers. Call this before installing PLCrashReporter. +func PrepareCrashSignalHandlers() { + forceGoSIGTRAPHandler() + C._save_go_handlers() +} + +// ReinstallCrashSignalHandlers installs a combined signal handler that chains +// PLCrashReporter (native crash report) and Go's runtime handler (Go crash log). +// +// Call PrepareCrashSignalHandlers before installing PLCrashReporter, then call +// this after PLCrashReporter has been installed. +// +// Flow on SIGSEGV: +// 1. Combined handler calls PLCrashReporter's saved handler → .plcrash written +// 2. Combined handler restores Go's handler via sigaction +// 3. Combined handler returns — kernel re-executes faulting instruction +// 4. PLCrashReporter's pending raise() (SI_USER) is ignored by Go's handler +// 5. Hardware fault → Go's handler → preparePanic → kernel sigreturn → +// sigpanic → crash output via debug.SetCrashOutput +// +// Flow on SIGTRAP: +// 1. PrepareCrashSignalHandlers force-installs Go's cgo trap trampoline +// 2. Combined handler calls PLCrashReporter's saved handler → .plcrash written +// 3. Combined handler directly calls the saved Go trap trampoline +func ReinstallCrashSignalHandlers() { + C._reinstall_handlers() +} diff --git a/experimental/libbox/signal_handler_stub.go b/experimental/libbox/signal_handler_stub.go new file mode 100644 index 00000000..2ac68b86 --- /dev/null +++ b/experimental/libbox/signal_handler_stub.go @@ -0,0 +1,7 @@ +//go:build !darwin || !badlinkname + +package libbox + +func PrepareCrashSignalHandlers() {} + +func ReinstallCrashSignalHandlers() {} diff --git a/experimental/libbox/stun.go b/experimental/libbox/stun.go new file mode 100644 index 00000000..3f38815d --- /dev/null +++ b/experimental/libbox/stun.go @@ -0,0 +1,50 @@ +package libbox + +import ( + "context" + + "github.com/sagernet/sing-box/common/stun" +) + +type STUNTest struct { + ctx context.Context + cancel context.CancelFunc +} + +func NewSTUNTest() *STUNTest { + ctx, cancel := context.WithCancel(context.Background()) + return &STUNTest{ctx: ctx, cancel: cancel} +} + +func (t *STUNTest) Start(server string, handler STUNTestHandler) { + go func() { + result, err := stun.Run(stun.Options{ + Server: server, + Context: t.ctx, + OnProgress: func(p stun.Progress) { + handler.OnProgress(&STUNTestProgress{ + Phase: int32(p.Phase), + ExternalAddr: p.ExternalAddr, + LatencyMs: p.LatencyMs, + NATMapping: int32(p.NATMapping), + NATFiltering: int32(p.NATFiltering), + }) + }, + }) + if err != nil { + handler.OnError(err.Error()) + return + } + handler.OnResult(&STUNTestResult{ + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: int32(result.NATMapping), + NATFiltering: int32(result.NATFiltering), + NATTypeSupported: result.NATTypeSupported, + }) + }() +} + +func (t *STUNTest) Cancel() { + t.cancel() +} diff --git a/experimental/libbox/tun.go b/experimental/libbox/tun.go new file mode 100644 index 00000000..84c6372a --- /dev/null +++ b/experimental/libbox/tun.go @@ -0,0 +1,168 @@ +package libbox + +import ( + "net" + "net/netip" + + "github.com/sagernet/sing-box/option" + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +type TunOptions interface { + GetInet4Address() RoutePrefixIterator + GetInet6Address() RoutePrefixIterator + GetDNSServerAddress() (*StringBox, error) + GetMTU() int32 + GetAutoRoute() bool + GetStrictRoute() bool + GetInet4RouteAddress() RoutePrefixIterator + GetInet6RouteAddress() RoutePrefixIterator + GetInet4RouteExcludeAddress() RoutePrefixIterator + GetInet6RouteExcludeAddress() RoutePrefixIterator + GetInet4RouteRange() RoutePrefixIterator + GetInet6RouteRange() RoutePrefixIterator + GetIncludePackage() StringIterator + GetExcludePackage() StringIterator + IsHTTPProxyEnabled() bool + GetHTTPProxyServer() string + GetHTTPProxyServerPort() int32 + GetHTTPProxyBypassDomain() StringIterator + GetHTTPProxyMatchDomain() StringIterator +} + +type RoutePrefix struct { + address netip.Addr + prefix int +} + +func (p *RoutePrefix) Address() string { + return p.address.String() +} + +func (p *RoutePrefix) Prefix() int32 { + return int32(p.prefix) +} + +func (p *RoutePrefix) Mask() string { + var bits int + if p.address.Is6() { + bits = 128 + } else { + bits = 32 + } + return net.IP(net.CIDRMask(p.prefix, bits)).String() +} + +func (p *RoutePrefix) String() string { + return netip.PrefixFrom(p.address, p.prefix).String() +} + +type RoutePrefixIterator interface { + Next() *RoutePrefix + HasNext() bool +} + +func mapRoutePrefix(prefixes []netip.Prefix) RoutePrefixIterator { + return newIterator(common.Map(prefixes, func(prefix netip.Prefix) *RoutePrefix { + return &RoutePrefix{ + address: prefix.Addr(), + prefix: prefix.Bits(), + } + })) +} + +var _ TunOptions = (*tunOptions)(nil) + +type tunOptions struct { + *tun.Options + routeRanges []netip.Prefix + option.TunPlatformOptions +} + +func (o *tunOptions) GetInet4Address() RoutePrefixIterator { + return mapRoutePrefix(o.Inet4Address) +} + +func (o *tunOptions) GetInet6Address() RoutePrefixIterator { + return mapRoutePrefix(o.Inet6Address) +} + +func (o *tunOptions) GetDNSServerAddress() (*StringBox, error) { + if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 { + return nil, E.New("need one more IPv4 address for DNS hijacking") + } + return wrapString(o.Inet4Address[0].Addr().Next().String()), nil +} + +func (o *tunOptions) GetMTU() int32 { + return int32(o.MTU) +} + +func (o *tunOptions) GetAutoRoute() bool { + return o.AutoRoute +} + +func (o *tunOptions) GetStrictRoute() bool { + return o.StrictRoute +} + +func (o *tunOptions) GetInet4RouteAddress() RoutePrefixIterator { + return mapRoutePrefix(o.Inet4RouteAddress) +} + +func (o *tunOptions) GetInet6RouteAddress() RoutePrefixIterator { + return mapRoutePrefix(o.Inet6RouteAddress) +} + +func (o *tunOptions) GetInet4RouteExcludeAddress() RoutePrefixIterator { + return mapRoutePrefix(o.Inet4RouteExcludeAddress) +} + +func (o *tunOptions) GetInet6RouteExcludeAddress() RoutePrefixIterator { + return mapRoutePrefix(o.Inet6RouteExcludeAddress) +} + +func (o *tunOptions) GetInet4RouteRange() RoutePrefixIterator { + return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool { + return it.Addr().Is4() + })) +} + +func (o *tunOptions) GetInet6RouteRange() RoutePrefixIterator { + return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool { + return it.Addr().Is6() + })) +} + +func (o *tunOptions) GetIncludePackage() StringIterator { + return newIterator(o.IncludePackage) +} + +func (o *tunOptions) GetExcludePackage() StringIterator { + return newIterator(o.ExcludePackage) +} + +func (o *tunOptions) IsHTTPProxyEnabled() bool { + if o.TunPlatformOptions.HTTPProxy == nil { + return false + } + return o.TunPlatformOptions.HTTPProxy.Enabled +} + +func (o *tunOptions) GetHTTPProxyServer() string { + return o.TunPlatformOptions.HTTPProxy.Server +} + +func (o *tunOptions) GetHTTPProxyServerPort() int32 { + return int32(o.TunPlatformOptions.HTTPProxy.ServerPort) +} + +func (o *tunOptions) GetHTTPProxyBypassDomain() StringIterator { + return newIterator(o.TunPlatformOptions.HTTPProxy.BypassDomain) +} + +func (o *tunOptions) GetHTTPProxyMatchDomain() StringIterator { + return newIterator(o.TunPlatformOptions.HTTPProxy.MatchDomain) +} diff --git a/experimental/libbox/tun_darwin.go b/experimental/libbox/tun_darwin.go new file mode 100644 index 00000000..e312cb91 --- /dev/null +++ b/experimental/libbox/tun_darwin.go @@ -0,0 +1,34 @@ +package libbox + +import ( + "golang.org/x/sys/unix" +) + +// kanged from wireauard-apple + +const utunControlName = "com.apple.net.utun_control" + +func GetTunnelFileDescriptor() int32 { + ctlInfo := &unix.CtlInfo{} + copy(ctlInfo.Name[:], utunControlName) + for fd := 0; fd < 1024; fd++ { + addr, err := unix.Getpeername(fd) + if err != nil { + continue + } + addrCTL, loaded := addr.(*unix.SockaddrCtl) + if !loaded { + continue + } + if ctlInfo.Id == 0 { + err = unix.IoctlCtlInfo(fd, ctlInfo) + if err != nil { + continue + } + } + if addrCTL.ID == ctlInfo.Id { + return int32(fd) + } + } + return -1 +} diff --git a/experimental/libbox/tun_name_darwin.go b/experimental/libbox/tun_name_darwin.go new file mode 100644 index 00000000..ff8c9dcc --- /dev/null +++ b/experimental/libbox/tun_name_darwin.go @@ -0,0 +1,11 @@ +package libbox + +import "golang.org/x/sys/unix" + +func getTunnelName(fd int32) (string, error) { + return unix.GetsockoptString( + int(fd), + 2, /* #define SYSPROTO_CONTROL 2 */ + 2, /* #define UTUN_OPT_IFNAME 2 */ + ) +} diff --git a/experimental/libbox/tun_name_linux.go b/experimental/libbox/tun_name_linux.go new file mode 100644 index 00000000..bf2f4f07 --- /dev/null +++ b/experimental/libbox/tun_name_linux.go @@ -0,0 +1,26 @@ +package libbox + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ifReqSize = unix.IFNAMSIZ + 64 + +func getTunnelName(fd int32) (string, error) { + var ifr [ifReqSize]byte + var errno syscall.Errno + _, _, errno = unix.Syscall( + unix.SYS_IOCTL, + uintptr(fd), + uintptr(unix.TUNGETIFF), + uintptr(unsafe.Pointer(&ifr[0])), + ) + if errno != 0 { + return "", fmt.Errorf("failed to get name of TUN device: %w", errno) + } + return unix.ByteSliceToString(ifr[:]), nil +} diff --git a/experimental/libbox/tun_name_other.go b/experimental/libbox/tun_name_other.go new file mode 100644 index 00000000..94a08afa --- /dev/null +++ b/experimental/libbox/tun_name_other.go @@ -0,0 +1,9 @@ +//go:build !(darwin || linux) + +package libbox + +import "os" + +func getTunnelName(fd int32) (string, error) { + return "", os.ErrInvalid +} diff --git a/experimental/locale/locale.go b/experimental/locale/locale.go new file mode 100644 index 00000000..e5575af4 --- /dev/null +++ b/experimental/locale/locale.go @@ -0,0 +1,32 @@ +package locale + +var ( + localeRegistry = make(map[string]*Locale) + current = defaultLocal +) + +type Locale struct { + // deprecated messages for graphical clients + Locale string + DeprecatedMessage string + DeprecatedMessageNoLink string +} + +var defaultLocal = &Locale{ + Locale: "en_US", + DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s please checkout documentation for migration.", + DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.", +} + +func Current() *Locale { + return current +} + +func Set(localeId string) bool { + locale, loaded := localeRegistry[localeId] + if !loaded { + return false + } + current = locale + return true +} diff --git a/experimental/locale/locale_zh_CN.go b/experimental/locale/locale_zh_CN.go new file mode 100644 index 00000000..f5605d9d --- /dev/null +++ b/experimental/locale/locale_zh_CN.go @@ -0,0 +1,11 @@ +package locale + +var warningMessageForEndUsers = "\n\n如果您不明白此消息意味着什么:您的配置文件已过时,且将很快不可用。请联系您的配置提供者以更新配置。" + +func init() { + localeRegistry["zh_CN"] = &Locale{ + Locale: "zh_CN", + DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers, + DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers, + } +} diff --git a/experimental/v2rayapi.go b/experimental/v2rayapi.go new file mode 100644 index 00000000..cf479631 --- /dev/null +++ b/experimental/v2rayapi.go @@ -0,0 +1,24 @@ +package experimental + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type V2RayServerConstructor = func(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) + +var v2rayServerConstructor V2RayServerConstructor + +func RegisterV2RayServerConstructor(constructor V2RayServerConstructor) { + v2rayServerConstructor = constructor +} + +func NewV2RayServer(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) { + if v2rayServerConstructor == nil { + return nil, os.ErrInvalid + } + return v2rayServerConstructor(logger, options) +} diff --git a/experimental/v2rayapi/server.go b/experimental/v2rayapi/server.go new file mode 100644 index 00000000..8ebae1c4 --- /dev/null +++ b/experimental/v2rayapi/server.go @@ -0,0 +1,82 @@ +package v2rayapi + +import ( + "errors" + "net" + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func init() { + experimental.RegisterV2RayServerConstructor(NewServer) +} + +var _ adapter.V2RayServer = (*Server)(nil) + +type Server struct { + logger log.Logger + listen string + tcpListener net.Listener + grpcServer *grpc.Server + statsService *StatsService +} + +func NewServer(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) { + grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) + statsService := NewStatsService(common.PtrValueOrDefault(options.Stats)) + if statsService != nil { + RegisterStatsServiceServer(grpcServer, statsService) + } + server := &Server{ + logger: logger, + listen: options.Listen, + grpcServer: grpcServer, + statsService: statsService, + } + return server, nil +} + +func (s *Server) Name() string { + return "v2ray server" +} + +func (s *Server) Start(stage adapter.StartStage) error { + if stage != adapter.StartStatePostStart { + return nil + } + listener, err := net.Listen("tcp", s.listen) + if err != nil { + return err + } + s.logger.Info("grpc server started at ", listener.Addr()) + s.tcpListener = listener + go func() { + err = s.grpcServer.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error(err) + } + }() + return nil +} + +func (s *Server) Close() error { + if s.grpcServer != nil { + s.grpcServer.Stop() + } + return common.Close( + common.PtrOrNil(s.grpcServer), + s.tcpListener, + ) +} + +func (s *Server) StatsService() adapter.ConnectionTracker { + return s.statsService +} diff --git a/experimental/v2rayapi/stats.go b/experimental/v2rayapi/stats.go new file mode 100644 index 00000000..c7b2b49f --- /dev/null +++ b/experimental/v2rayapi/stats.go @@ -0,0 +1,222 @@ +package v2rayapi + +import ( + "context" + "net" + "regexp" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +func init() { + StatsService_ServiceDesc.ServiceName = "v2ray.core.app.stats.command.StatsService" +} + +var ( + _ adapter.ConnectionTracker = (*StatsService)(nil) + _ StatsServiceServer = (*StatsService)(nil) +) + +type StatsService struct { + createdAt time.Time + inbounds map[string]bool + outbounds map[string]bool + users map[string]bool + access sync.Mutex + counters map[string]*atomic.Int64 +} + +func NewStatsService(options option.V2RayStatsServiceOptions) *StatsService { + if !options.Enabled { + return nil + } + inbounds := make(map[string]bool) + outbounds := make(map[string]bool) + users := make(map[string]bool) + for _, inbound := range options.Inbounds { + inbounds[inbound] = true + } + for _, outbound := range options.Outbounds { + outbounds[outbound] = true + } + for _, user := range options.Users { + users[user] = true + } + return &StatsService{ + createdAt: time.Now(), + inbounds: inbounds, + outbounds: outbounds, + users: users, + counters: make(map[string]*atomic.Int64), + } +} + +func (s *StatsService) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + inbound := metadata.Inbound + user := metadata.User + outbound := matchOutbound.Tag() + var readCounter []*atomic.Int64 + var writeCounter []*atomic.Int64 + countInbound := inbound != "" && s.inbounds[inbound] + countOutbound := outbound != "" && s.outbounds[outbound] + countUser := user != "" && s.users[user] + if !countInbound && !countOutbound && !countUser { + return conn + } + s.access.Lock() + if countInbound { + readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink")) + writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink")) + } + if countOutbound { + readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink")) + writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink")) + } + if countUser { + readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink")) + writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) + } + s.access.Unlock() + return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) +} + +func (s *StatsService) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { + inbound := metadata.Inbound + user := metadata.User + outbound := matchOutbound.Tag() + var readCounter []*atomic.Int64 + var writeCounter []*atomic.Int64 + countInbound := inbound != "" && s.inbounds[inbound] + countOutbound := outbound != "" && s.outbounds[outbound] + countUser := user != "" && s.users[user] + if !countInbound && !countOutbound && !countUser { + return conn + } + s.access.Lock() + if countInbound { + readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink")) + writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink")) + } + if countOutbound { + readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink")) + writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink")) + } + if countUser { + readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink")) + writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) + } + s.access.Unlock() + return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil) +} + +func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { + s.access.Lock() + counter, loaded := s.counters[request.Name] + s.access.Unlock() + if !loaded { + return nil, E.New(request.Name, " not found.") + } + var value int64 + if request.Reset_ { + value = counter.Swap(0) + } else { + value = counter.Load() + } + return &GetStatsResponse{Stat: &Stat{Name: request.Name, Value: value}}, nil +} + +func (s *StatsService) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { + var response QueryStatsResponse + s.access.Lock() + defer s.access.Unlock() + if len(request.Patterns) == 0 { + for name, counter := range s.counters { + var value int64 + if request.Reset_ { + value = counter.Swap(0) + } else { + value = counter.Load() + } + response.Stat = append(response.Stat, &Stat{Name: name, Value: value}) + } + } else if request.Regexp { + matchers := make([]*regexp.Regexp, 0, len(request.Patterns)) + for _, pattern := range request.Patterns { + matcher, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + matchers = append(matchers, matcher) + } + for name, counter := range s.counters { + for _, matcher := range matchers { + if matcher.MatchString(name) { + var value int64 + if request.Reset_ { + value = counter.Swap(0) + } else { + value = counter.Load() + } + response.Stat = append(response.Stat, &Stat{Name: name, Value: value}) + } + } + } + } else { + for name, counter := range s.counters { + for _, matcher := range request.Patterns { + if strings.Contains(name, matcher) { + var value int64 + if request.Reset_ { + value = counter.Swap(0) + } else { + value = counter.Load() + } + response.Stat = append(response.Stat, &Stat{Name: name, Value: value}) + } + } + } + } + return &response, nil +} + +func (s *StatsService) GetSysStats(ctx context.Context, request *SysStatsRequest) (*SysStatsResponse, error) { + var rtm runtime.MemStats + runtime.ReadMemStats(&rtm) + response := &SysStatsResponse{ + Uptime: uint32(time.Since(s.createdAt).Seconds()), + NumGoroutine: uint32(runtime.NumGoroutine()), + Alloc: rtm.Alloc, + TotalAlloc: rtm.TotalAlloc, + Sys: rtm.Sys, + Mallocs: rtm.Mallocs, + Frees: rtm.Frees, + LiveObjects: rtm.Mallocs - rtm.Frees, + NumGC: rtm.NumGC, + PauseTotalNs: rtm.PauseTotalNs, + } + + return response, nil +} + +func (s *StatsService) mustEmbedUnimplementedStatsServiceServer() { +} + +//nolint:staticcheck +func (s *StatsService) loadOrCreateCounter(name string) *atomic.Int64 { + counter, loaded := s.counters[name] + if loaded { + return counter + } + counter = &atomic.Int64{} + s.counters[name] = counter + return counter +} diff --git a/experimental/v2rayapi/stats.pb.go b/experimental/v2rayapi/stats.pb.go new file mode 100644 index 00000000..586b9a7f --- /dev/null +++ b/experimental/v2rayapi/stats.pb.go @@ -0,0 +1,538 @@ +package v2rayapi + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the stat counter. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Whether or not to reset the counter to fetching its value. + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStatsRequest) Reset() { + *x = GetStatsRequest{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStatsRequest) ProtoMessage() {} + +func (x *GetStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead. +func (*GetStatsRequest) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{0} +} + +func (x *GetStatsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetStatsRequest) GetReset_() bool { + if x != nil { + return x.Reset_ + } + return false +} + +type Stat struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Stat) Reset() { + *x = Stat{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Stat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stat) ProtoMessage() {} + +func (x *Stat) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stat.ProtoReflect.Descriptor instead. +func (*Stat) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{1} +} + +func (x *Stat) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Stat) GetValue() int64 { + if x != nil { + return x.Value + } + return 0 +} + +type GetStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stat *Stat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStatsResponse) Reset() { + *x = GetStatsResponse{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStatsResponse) ProtoMessage() {} + +func (x *GetStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead. +func (*GetStatsResponse) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{2} +} + +func (x *GetStatsResponse) GetStat() *Stat { + if x != nil { + return x.Stat + } + return nil +} + +type QueryStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Deprecated, use Patterns instead + Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + Patterns []string `protobuf:"bytes,3,rep,name=patterns,proto3" json:"patterns,omitempty"` + Regexp bool `protobuf:"varint,4,opt,name=regexp,proto3" json:"regexp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryStatsRequest) Reset() { + *x = QueryStatsRequest{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsRequest) ProtoMessage() {} + +func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsRequest.ProtoReflect.Descriptor instead. +func (*QueryStatsRequest) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{3} +} + +func (x *QueryStatsRequest) GetPattern() string { + if x != nil { + return x.Pattern + } + return "" +} + +func (x *QueryStatsRequest) GetReset_() bool { + if x != nil { + return x.Reset_ + } + return false +} + +func (x *QueryStatsRequest) GetPatterns() []string { + if x != nil { + return x.Patterns + } + return nil +} + +func (x *QueryStatsRequest) GetRegexp() bool { + if x != nil { + return x.Regexp + } + return false +} + +type QueryStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stat []*Stat `protobuf:"bytes,1,rep,name=stat,proto3" json:"stat,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryStatsResponse) Reset() { + *x = QueryStatsResponse{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsResponse) ProtoMessage() {} + +func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsResponse.ProtoReflect.Descriptor instead. +func (*QueryStatsResponse) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{4} +} + +func (x *QueryStatsResponse) GetStat() []*Stat { + if x != nil { + return x.Stat + } + return nil +} + +type SysStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SysStatsRequest) Reset() { + *x = SysStatsRequest{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SysStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SysStatsRequest) ProtoMessage() {} + +func (x *SysStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SysStatsRequest.ProtoReflect.Descriptor instead. +func (*SysStatsRequest) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{5} +} + +type SysStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NumGoroutine uint32 `protobuf:"varint,1,opt,name=NumGoroutine,proto3" json:"NumGoroutine,omitempty"` + NumGC uint32 `protobuf:"varint,2,opt,name=NumGC,proto3" json:"NumGC,omitempty"` + Alloc uint64 `protobuf:"varint,3,opt,name=Alloc,proto3" json:"Alloc,omitempty"` + TotalAlloc uint64 `protobuf:"varint,4,opt,name=TotalAlloc,proto3" json:"TotalAlloc,omitempty"` + Sys uint64 `protobuf:"varint,5,opt,name=Sys,proto3" json:"Sys,omitempty"` + Mallocs uint64 `protobuf:"varint,6,opt,name=Mallocs,proto3" json:"Mallocs,omitempty"` + Frees uint64 `protobuf:"varint,7,opt,name=Frees,proto3" json:"Frees,omitempty"` + LiveObjects uint64 `protobuf:"varint,8,opt,name=LiveObjects,proto3" json:"LiveObjects,omitempty"` + PauseTotalNs uint64 `protobuf:"varint,9,opt,name=PauseTotalNs,proto3" json:"PauseTotalNs,omitempty"` + Uptime uint32 `protobuf:"varint,10,opt,name=Uptime,proto3" json:"Uptime,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SysStatsResponse) Reset() { + *x = SysStatsResponse{} + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SysStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SysStatsResponse) ProtoMessage() {} + +func (x *SysStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_experimental_v2rayapi_stats_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SysStatsResponse.ProtoReflect.Descriptor instead. +func (*SysStatsResponse) Descriptor() ([]byte, []int) { + return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{6} +} + +func (x *SysStatsResponse) GetNumGoroutine() uint32 { + if x != nil { + return x.NumGoroutine + } + return 0 +} + +func (x *SysStatsResponse) GetNumGC() uint32 { + if x != nil { + return x.NumGC + } + return 0 +} + +func (x *SysStatsResponse) GetAlloc() uint64 { + if x != nil { + return x.Alloc + } + return 0 +} + +func (x *SysStatsResponse) GetTotalAlloc() uint64 { + if x != nil { + return x.TotalAlloc + } + return 0 +} + +func (x *SysStatsResponse) GetSys() uint64 { + if x != nil { + return x.Sys + } + return 0 +} + +func (x *SysStatsResponse) GetMallocs() uint64 { + if x != nil { + return x.Mallocs + } + return 0 +} + +func (x *SysStatsResponse) GetFrees() uint64 { + if x != nil { + return x.Frees + } + return 0 +} + +func (x *SysStatsResponse) GetLiveObjects() uint64 { + if x != nil { + return x.LiveObjects + } + return 0 +} + +func (x *SysStatsResponse) GetPauseTotalNs() uint64 { + if x != nil { + return x.PauseTotalNs + } + return 0 +} + +func (x *SysStatsResponse) GetUptime() uint32 { + if x != nil { + return x.Uptime + } + return 0 +} + +var File_experimental_v2rayapi_stats_proto protoreflect.FileDescriptor + +const file_experimental_v2rayapi_stats_proto_rawDesc = "" + + "\n" + + "!experimental/v2rayapi/stats.proto\x12\x15experimental.v2rayapi\";\n" + + "\x0fGetStatsRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\"0\n" + + "\x04Stat\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\x03R\x05value\"C\n" + + "\x10GetStatsResponse\x12/\n" + + "\x04stat\x18\x01 \x01(\v2\x1b.experimental.v2rayapi.StatR\x04stat\"w\n" + + "\x11QueryStatsRequest\x12\x18\n" + + "\apattern\x18\x01 \x01(\tR\apattern\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\x12\x1a\n" + + "\bpatterns\x18\x03 \x03(\tR\bpatterns\x12\x16\n" + + "\x06regexp\x18\x04 \x01(\bR\x06regexp\"E\n" + + "\x12QueryStatsResponse\x12/\n" + + "\x04stat\x18\x01 \x03(\v2\x1b.experimental.v2rayapi.StatR\x04stat\"\x11\n" + + "\x0fSysStatsRequest\"\xa2\x02\n" + + "\x10SysStatsResponse\x12\"\n" + + "\fNumGoroutine\x18\x01 \x01(\rR\fNumGoroutine\x12\x14\n" + + "\x05NumGC\x18\x02 \x01(\rR\x05NumGC\x12\x14\n" + + "\x05Alloc\x18\x03 \x01(\x04R\x05Alloc\x12\x1e\n" + + "\n" + + "TotalAlloc\x18\x04 \x01(\x04R\n" + + "TotalAlloc\x12\x10\n" + + "\x03Sys\x18\x05 \x01(\x04R\x03Sys\x12\x18\n" + + "\aMallocs\x18\x06 \x01(\x04R\aMallocs\x12\x14\n" + + "\x05Frees\x18\a \x01(\x04R\x05Frees\x12 \n" + + "\vLiveObjects\x18\b \x01(\x04R\vLiveObjects\x12\"\n" + + "\fPauseTotalNs\x18\t \x01(\x04R\fPauseTotalNs\x12\x16\n" + + "\x06Uptime\x18\n" + + " \x01(\rR\x06Uptime2\xb4\x02\n" + + "\fStatsService\x12]\n" + + "\bGetStats\x12&.experimental.v2rayapi.GetStatsRequest\x1a'.experimental.v2rayapi.GetStatsResponse\"\x00\x12c\n" + + "\n" + + "QueryStats\x12(.experimental.v2rayapi.QueryStatsRequest\x1a).experimental.v2rayapi.QueryStatsResponse\"\x00\x12`\n" + + "\vGetSysStats\x12&.experimental.v2rayapi.SysStatsRequest\x1a'.experimental.v2rayapi.SysStatsResponse\"\x00B4Z2github.com/sagernet/sing-box/experimental/v2rayapib\x06proto3" + +var ( + file_experimental_v2rayapi_stats_proto_rawDescOnce sync.Once + file_experimental_v2rayapi_stats_proto_rawDescData []byte +) + +func file_experimental_v2rayapi_stats_proto_rawDescGZIP() []byte { + file_experimental_v2rayapi_stats_proto_rawDescOnce.Do(func() { + file_experimental_v2rayapi_stats_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_experimental_v2rayapi_stats_proto_rawDesc), len(file_experimental_v2rayapi_stats_proto_rawDesc))) + }) + return file_experimental_v2rayapi_stats_proto_rawDescData +} + +var ( + file_experimental_v2rayapi_stats_proto_msgTypes = make([]protoimpl.MessageInfo, 7) + file_experimental_v2rayapi_stats_proto_goTypes = []any{ + (*GetStatsRequest)(nil), // 0: experimental.v2rayapi.GetStatsRequest + (*Stat)(nil), // 1: experimental.v2rayapi.Stat + (*GetStatsResponse)(nil), // 2: experimental.v2rayapi.GetStatsResponse + (*QueryStatsRequest)(nil), // 3: experimental.v2rayapi.QueryStatsRequest + (*QueryStatsResponse)(nil), // 4: experimental.v2rayapi.QueryStatsResponse + (*SysStatsRequest)(nil), // 5: experimental.v2rayapi.SysStatsRequest + (*SysStatsResponse)(nil), // 6: experimental.v2rayapi.SysStatsResponse + } +) + +var file_experimental_v2rayapi_stats_proto_depIdxs = []int32{ + 1, // 0: experimental.v2rayapi.GetStatsResponse.stat:type_name -> experimental.v2rayapi.Stat + 1, // 1: experimental.v2rayapi.QueryStatsResponse.stat:type_name -> experimental.v2rayapi.Stat + 0, // 2: experimental.v2rayapi.StatsService.GetStats:input_type -> experimental.v2rayapi.GetStatsRequest + 3, // 3: experimental.v2rayapi.StatsService.QueryStats:input_type -> experimental.v2rayapi.QueryStatsRequest + 5, // 4: experimental.v2rayapi.StatsService.GetSysStats:input_type -> experimental.v2rayapi.SysStatsRequest + 2, // 5: experimental.v2rayapi.StatsService.GetStats:output_type -> experimental.v2rayapi.GetStatsResponse + 4, // 6: experimental.v2rayapi.StatsService.QueryStats:output_type -> experimental.v2rayapi.QueryStatsResponse + 6, // 7: experimental.v2rayapi.StatsService.GetSysStats:output_type -> experimental.v2rayapi.SysStatsResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_experimental_v2rayapi_stats_proto_init() } +func file_experimental_v2rayapi_stats_proto_init() { + if File_experimental_v2rayapi_stats_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_experimental_v2rayapi_stats_proto_rawDesc), len(file_experimental_v2rayapi_stats_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_experimental_v2rayapi_stats_proto_goTypes, + DependencyIndexes: file_experimental_v2rayapi_stats_proto_depIdxs, + MessageInfos: file_experimental_v2rayapi_stats_proto_msgTypes, + }.Build() + File_experimental_v2rayapi_stats_proto = out.File + file_experimental_v2rayapi_stats_proto_goTypes = nil + file_experimental_v2rayapi_stats_proto_depIdxs = nil +} diff --git a/experimental/v2rayapi/stats.proto b/experimental/v2rayapi/stats.proto new file mode 100644 index 00000000..5fc3da49 --- /dev/null +++ b/experimental/v2rayapi/stats.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package experimental.v2rayapi; +option go_package = "github.com/sagernet/sing-box/experimental/v2rayapi"; + +message GetStatsRequest { + // Name of the stat counter. + string name = 1; + // Whether or not to reset the counter to fetching its value. + bool reset = 2; +} + +message Stat { + string name = 1; + int64 value = 2; +} + +message GetStatsResponse { + Stat stat = 1; +} + +message QueryStatsRequest { + // Deprecated, use Patterns instead + string pattern = 1; + bool reset = 2; + repeated string patterns = 3; + bool regexp = 4; +} + +message QueryStatsResponse { + repeated Stat stat = 1; +} + +message SysStatsRequest {} + +message SysStatsResponse { + uint32 NumGoroutine = 1; + uint32 NumGC = 2; + uint64 Alloc = 3; + uint64 TotalAlloc = 4; + uint64 Sys = 5; + uint64 Mallocs = 6; + uint64 Frees = 7; + uint64 LiveObjects = 8; + uint64 PauseTotalNs = 9; + uint32 Uptime = 10; +} + +service StatsService { + rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} + rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {} + rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} +} \ No newline at end of file diff --git a/experimental/v2rayapi/stats_grpc.pb.go b/experimental/v2rayapi/stats_grpc.pb.go new file mode 100644 index 00000000..0745899f --- /dev/null +++ b/experimental/v2rayapi/stats_grpc.pb.go @@ -0,0 +1,194 @@ +package v2rayapi + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + StatsService_GetStats_FullMethodName = "/experimental.v2rayapi.StatsService/GetStats" + StatsService_QueryStats_FullMethodName = "/experimental.v2rayapi.StatsService/QueryStats" + StatsService_GetSysStats_FullMethodName = "/experimental.v2rayapi.StatsService/GetSysStats" +) + +// StatsServiceClient is the client API for StatsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StatsServiceClient interface { + GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) + QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) + GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) +} + +type statsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStatsServiceClient(cc grpc.ClientConnInterface) StatsServiceClient { + return &statsServiceClient{cc} +} + +func (c *statsServiceClient) GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetStatsResponse) + err := c.cc.Invoke(ctx, StatsService_GetStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *statsServiceClient) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QueryStatsResponse) + err := c.cc.Invoke(ctx, StatsService_QueryStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *statsServiceClient) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SysStatsResponse) + err := c.cc.Invoke(ctx, StatsService_GetSysStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StatsServiceServer is the server API for StatsService service. +// All implementations must embed UnimplementedStatsServiceServer +// for forward compatibility. +type StatsServiceServer interface { + GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) + QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) + GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) + mustEmbedUnimplementedStatsServiceServer() +} + +// UnimplementedStatsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedStatsServiceServer struct{} + +func (UnimplementedStatsServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetStats not implemented") +} + +func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method QueryStats not implemented") +} + +func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSysStats not implemented") +} +func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} +func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} + +// UnsafeStatsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StatsServiceServer will +// result in compilation errors. +type UnsafeStatsServiceServer interface { + mustEmbedUnimplementedStatsServiceServer() +} + +func RegisterStatsServiceServer(s grpc.ServiceRegistrar, srv StatsServiceServer) { + // If the following call panics, it indicates UnimplementedStatsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&StatsService_ServiceDesc, srv) +} + +func _StatsService_GetStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).GetStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetStats(ctx, req.(*GetStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StatsService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).QueryStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_QueryStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).QueryStats(ctx, req.(*QueryStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StatsService_GetSysStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SysStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).GetSysStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetSysStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetSysStats(ctx, req.(*SysStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StatsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "experimental.v2rayapi.StatsService", + HandlerType: (*StatsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetStats", + Handler: _StatsService_GetStats_Handler, + }, + { + MethodName: "QueryStats", + Handler: _StatsService_QueryStats_Handler, + }, + { + MethodName: "GetSysStats", + Handler: _StatsService_GetSysStats_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "experimental/v2rayapi/stats.proto", +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..4a9c656a --- /dev/null +++ b/go.mod @@ -0,0 +1,173 @@ +module github.com/sagernet/sing-box + +go 1.24.7 + +require ( + github.com/anthropics/anthropic-sdk-go v1.26.0 + github.com/anytls/sing-anytls v0.0.11 + github.com/caddyserver/certmagic v0.25.2 + github.com/caddyserver/zerossl v0.1.5 + github.com/coder/websocket v1.8.14 + github.com/cretz/bine v0.2.0 + github.com/database64128/tfo-go/v2 v2.3.2 + github.com/go-chi/chi/v5 v5.2.5 + github.com/go-chi/render v1.0.3 + github.com/godbus/dbus/v5 v5.2.2 + github.com/gofrs/uuid/v5 v5.4.0 + github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 + github.com/jsimonetti/rtnetlink v1.4.0 + github.com/keybase/go-keychain v0.0.1 + github.com/libdns/acmedns v0.5.0 + github.com/libdns/alidns v1.0.6 + github.com/libdns/cloudflare v0.2.2 + github.com/libdns/libdns v1.1.1 + github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/mdlayher/netlink v1.9.0 + github.com/metacubex/utls v1.8.4 + github.com/mholt/acmez/v3 v3.1.6 + github.com/miekg/dns v1.1.72 + github.com/openai/openai-go/v3 v3.26.0 + github.com/oschwald/maxminddb-golang v1.13.1 + github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 + github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a + github.com/sagernet/cors v1.2.1 + github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa + github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa + github.com/sagernet/fswatch v0.1.1 + github.com/sagernet/gomobile v0.1.12 + github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 + github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 + github.com/sagernet/sing v0.8.5-0.20260414061014-3597f84f897c + github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa + github.com/sagernet/sing-mux v0.3.4 + github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 + github.com/sagernet/sing-shadowsocks v0.2.8 + github.com/sagernet/sing-shadowsocks2 v0.2.1 + github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 + github.com/sagernet/sing-tun v0.8.8-0.20260410061515-018f5eaae695 + github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 + github.com/sagernet/smux v1.5.50-sing-box-mod.1 + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 + github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c + github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 + github.com/vishvananda/netns v0.0.5 + go.uber.org/zap v1.27.1 + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + golang.org/x/crypto v0.48.0 + golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 + golang.org/x/mod v0.33.0 + golang.org/x/net v0.50.0 + golang.org/x/sys v0.41.0 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 + google.golang.org/grpc v1.79.1 + google.golang.org/protobuf v1.36.11 + howett.net/plist v1.0.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/database64128/netx-go v0.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect + github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gaissmai/bart v0.18.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // 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.7.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect + github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect + github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect + github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.42.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/windows v0.5.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.3.0 // indirect + zombiezen.com/go/capnproto2 v2.18.2+incompatible // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..9f224685 --- /dev/null +++ b/go.sum @@ -0,0 +1,415 @@ +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= +github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= +github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= +github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= +github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= +github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= +github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= +github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= +github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= +github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw= +github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= +github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= +github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= +github.com/gofrs/uuid/v5 v5.4.0/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= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY= +github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= +github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE= +github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ= +github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM= +github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec= +github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= +github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= +github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= +github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= +github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= +github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 h1:qi+ijeREa0yfAaO+NOcZ81gv4uzOfALUIdhkiIFvmG4= +github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1/go.mod h1:JULDuzTMn2gyZFcjpTVZP4/UuwAdbHJ0bum2RdjXojU= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= +github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= +github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa h1:7SehNSF1UHbLZa5dk+1rW1aperffJzl5r6TCJIXtAaY= +github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa h1:ijk5v9N/akiMgqu734yMpv7Pk9F4Qmjh8Vfdcb4uJHE= +github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa/go.mod h1:+FENo4+0AOvH9e3oY6/iO7yy7USNt61dgbnI5W0TDZ0= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b h1:O+PkYT88ayVWESX5tqxeMeS9OnzC3ZTic8gYiPJNXT8= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:o0MsgbsJwYkbqlbfaCvmAwb8/LAXeoSP8NE/aNvR/yY= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b h1:JEQnc7cRMUahWJFtWY6n0hs1LE0KgyRv3pD0RWS8Yo8= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:69+AKzuUW9hzw2nU79c2DWfuzrIZ3PJm1KAwXh+7xr0= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:jp9FHUVTCJQ67Ecw3Inoct6/z1VTFXPtNYpXt47pa4E= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:WN3DZoECd2UbhmYQGpOA4jx4QBXiZuN1DvL/35NT61g= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:H4RKicwrIa4PwTXZOmXOg85hiCrpeFja4daOlX180pE= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:Rwi+Cu+Hgwj28F1lh837gGqSqn7oU8+r5i3UJyLPkKc= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:v2wcnPX3gt0PngFYXjXYAiarFckwx3pVAP6ETSpbSWE= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b h1:Bl0zZ3QZq6pPJMbQlYHDhhaGngVefRlFzxWc0p48eHo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b h1:vf+MbGv6RvvmXUNvganykBOnDIVXxy8XgtKOOqOcxtE= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:2IAc1bVFYF+B6hof34ChQKVhw7LElBxEEx7S0n+7o78= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b h1:NrJaiOS0VLmWTbUHhXDsLTqelmCW4y3xJqptPs4Sx0s= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b h1:A+ubSkca1nl2cT8pYUqCo1O7M41suNrKpWhZKCM/aIQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:WrhGH5FDXlCAoXwN6N44yCMvy6EbIurmTmptkz3mmms= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b h1:kgwB5p5e0gdVX5iYRE7VbZS/On4qnb4UKonkGPwhkDI= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b h1:Z3dOeFlRIOeQhSh+mCYDHui1yR3S/Uw8eupczzBvxqw= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b h1:LPi6jz1k11Q67hm3Pw6aaPJ/Z6e3VtNhzrRjr5/5AQo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b h1:55sqihyfXWN7y7p7gOEgtUz9cm1mV3SDQ90/v6ROFaA= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b h1:OTA1cbv5YIDVsYA8AAXHC4NgEc7b6pDiY+edujLWfJU= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b h1:B/rdD/1A+RgqUYUZcoGhLeMqijnBd1mUt8+5LhOH7j8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b h1:QFRWi6FucrODS4xQ8e9GYIzGSeMFO/DAMtTCVeJiCvM= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b h1:2WJjPKZHLNIB4D17c3o9S+SP9kb3Qh0D26oWlun1+pE= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b h1:cUNTe4gNncRpYL28jzQf6qcJej40zzGQsH0o6CLUGws= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:+sc1LJF0FjU2hVO5xBqqT+8qzoU08J2uHwxSle2m/Hw= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:+D/uhFxllI/KTLpeNEl8dwF3omPGmUFbrqt5tJkAyp0= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:nSUzzTUAZdqjGGckayk64sz+F0TGJPHvauTiAn27UKk= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:PE/fYBiHzB52gnQMg0soBfQyJCzmWHti48kCe2TBt9w= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:hy/3lPV11pKAAojDFnb95l9NpwOym6kME7FxS9p8sXs= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= +github.com/sagernet/gomobile v0.1.12/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= +github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= +github.com/sagernet/sing v0.8.5-0.20260414061014-3597f84f897c h1:FsN+aA8CwkwlthCGZl0/vA1PIE+xV+CjVBYPm6h45Gg= +github.com/sagernet/sing v0.8.5-0.20260414061014-3597f84f897c/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa h1:165HiOfgfofJIirEp1NGSmsoJAi+++WhR29IhtAu4A4= +github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= +github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= +github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= +github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc= +github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= +github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= +github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= +github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= +github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= +github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= +github.com/sagernet/sing-tun v0.8.8-0.20260410061515-018f5eaae695 h1:2maqN3XuorEo5faXHIyYZQZ1/ybim4hImfCEWZwdPbk= +github.com/sagernet/sing-tun v0.8.8-0.20260410061515-018f5eaae695/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= +github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= +github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= +github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= +github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= +github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= +lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +zombiezen.com/go/capnproto2 v2.18.2+incompatible h1:v3BD1zbruvffn7zjJUU5Pn8nZAB11bhZSQC4W+YnnKo= +zombiezen.com/go/capnproto2 v2.18.2+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ= diff --git a/include/acme.go b/include/acme.go new file mode 100644 index 00000000..093fd508 --- /dev/null +++ b/include/acme.go @@ -0,0 +1,12 @@ +//go:build with_acme + +package include + +import ( + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/service/acme" +) + +func registerACMECertificateProvider(registry *certificate.Registry) { + acme.RegisterCertificateProvider(registry) +} diff --git a/include/acme_stub.go b/include/acme_stub.go new file mode 100644 index 00000000..bceab3d7 --- /dev/null +++ b/include/acme_stub.go @@ -0,0 +1,20 @@ +//go:build !with_acme + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + 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" +) + +func registerACMECertificateProvider(registry *certificate.Registry) { + certificate.Register[option.ACMECertificateProviderOptions](registry, C.TypeACME, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) { + return nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) + }) +} diff --git a/include/ccm.go b/include/ccm.go new file mode 100644 index 00000000..a7520148 --- /dev/null +++ b/include/ccm.go @@ -0,0 +1,12 @@ +//go:build with_ccm && (!darwin || cgo) + +package include + +import ( + "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/service/ccm" +) + +func registerCCMService(registry *service.Registry) { + ccm.RegisterService(registry) +} diff --git a/include/ccm_stub.go b/include/ccm_stub.go new file mode 100644 index 00000000..eac29eeb --- /dev/null +++ b/include/ccm_stub.go @@ -0,0 +1,20 @@ +//go:build !with_ccm + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/service" + 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" +) + +func registerCCMService(registry *service.Registry) { + service.Register[option.CCMServiceOptions](registry, C.TypeCCM, func(ctx context.Context, logger log.ContextLogger, tag string, options option.CCMServiceOptions) (adapter.Service, error) { + return nil, E.New(`CCM is not included in this build, rebuild with -tags with_CCM`) + }) +} diff --git a/include/ccm_stub_darwin.go b/include/ccm_stub_darwin.go new file mode 100644 index 00000000..f2ad7381 --- /dev/null +++ b/include/ccm_stub_darwin.go @@ -0,0 +1,20 @@ +//go:build with_ccm && darwin && !cgo + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/service" + 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" +) + +func registerCCMService(registry *service.Registry) { + service.Register[option.CCMServiceOptions](registry, C.TypeCCM, func(ctx context.Context, logger log.ContextLogger, tag string, options option.CCMServiceOptions) (adapter.Service, error) { + return nil, E.New(`CCM requires CGO on darwin, rebuild with CGO_ENABLED=1`) + }) +} diff --git a/include/clashapi.go b/include/clashapi.go new file mode 100644 index 00000000..550a5db6 --- /dev/null +++ b/include/clashapi.go @@ -0,0 +1,5 @@ +//go:build with_clash_api + +package include + +import _ "github.com/sagernet/sing-box/experimental/clashapi" diff --git a/include/clashapi_stub.go b/include/clashapi_stub.go new file mode 100644 index 00000000..e7d5304f --- /dev/null +++ b/include/clashapi_stub.go @@ -0,0 +1,19 @@ +//go:build !with_clash_api + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func init() { + experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { + return nil, E.New(`clash api is not included in this build, rebuild with -tags with_clash_api`) + }) +} diff --git a/include/cloudflared.go b/include/cloudflared.go new file mode 100644 index 00000000..63200108 --- /dev/null +++ b/include/cloudflared.go @@ -0,0 +1,12 @@ +//go:build with_cloudflared + +package include + +import ( + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/protocol/cloudflare" +) + +func registerCloudflaredInbound(registry *inbound.Registry) { + cloudflare.RegisterInbound(registry) +} diff --git a/include/cloudflared_stub.go b/include/cloudflared_stub.go new file mode 100644 index 00000000..8f49aecc --- /dev/null +++ b/include/cloudflared_stub.go @@ -0,0 +1,20 @@ +//go:build !with_cloudflared + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + 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" +) + +func registerCloudflaredInbound(registry *inbound.Registry) { + inbound.Register[option.CloudflaredInboundOptions](registry, C.TypeCloudflared, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.CloudflaredInboundOptions) (adapter.Inbound, error) { + return nil, E.New(`Cloudflared is not included in this build, rebuild with -tags with_cloudflared`) + }) +} diff --git a/include/dhcp.go b/include/dhcp.go new file mode 100644 index 00000000..8cf074be --- /dev/null +++ b/include/dhcp.go @@ -0,0 +1,12 @@ +//go:build with_dhcp + +package include + +import ( + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/dhcp" +) + +func registerDHCPTransport(registry *dns.TransportRegistry) { + dhcp.RegisterTransport(registry) +} diff --git a/include/dhcp_stub.go b/include/dhcp_stub.go new file mode 100644 index 00000000..272f313a --- /dev/null +++ b/include/dhcp_stub.go @@ -0,0 +1,20 @@ +//go:build !with_dhcp + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerDHCPTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.DHCPDNSServerOptions](registry, C.DNSTypeDHCP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) { + return nil, E.New(`DHCP is not included in this build, rebuild with -tags with_dhcp`) + }) +} diff --git a/include/naive_outbound.go b/include/naive_outbound.go new file mode 100644 index 00000000..d15d0450 --- /dev/null +++ b/include/naive_outbound.go @@ -0,0 +1,12 @@ +//go:build with_naive_outbound + +package include + +import ( + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/protocol/naive" +) + +func registerNaiveOutbound(registry *outbound.Registry) { + naive.RegisterOutbound(registry) +} diff --git a/include/naive_outbound_stub.go b/include/naive_outbound_stub.go new file mode 100644 index 00000000..cf892091 --- /dev/null +++ b/include/naive_outbound_stub.go @@ -0,0 +1,20 @@ +//go:build !with_naive_outbound + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + 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" +) + +func registerNaiveOutbound(registry *outbound.Registry) { + outbound.Register[option.NaiveOutboundOptions](registry, C.TypeNaive, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveOutboundOptions) (adapter.Outbound, error) { + return nil, E.New(`naive outbound is not included in this build, rebuild with -tags with_naive_outbound`) + }) +} diff --git a/include/ocm.go b/include/ocm.go new file mode 100644 index 00000000..cdea9eea --- /dev/null +++ b/include/ocm.go @@ -0,0 +1,12 @@ +//go:build with_ocm + +package include + +import ( + "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/service/ocm" +) + +func registerOCMService(registry *service.Registry) { + ocm.RegisterService(registry) +} diff --git a/include/ocm_stub.go b/include/ocm_stub.go new file mode 100644 index 00000000..d5a94fcb --- /dev/null +++ b/include/ocm_stub.go @@ -0,0 +1,20 @@ +//go:build !with_ocm + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/service" + 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" +) + +func registerOCMService(registry *service.Registry) { + service.Register[option.OCMServiceOptions](registry, C.TypeOCM, func(ctx context.Context, logger log.ContextLogger, tag string, options option.OCMServiceOptions) (adapter.Service, error) { + return nil, E.New(`OCM is not included in this build, rebuild with -tags with_ocm`) + }) +} diff --git a/include/oom_killer.go b/include/oom_killer.go new file mode 100644 index 00000000..3f70d9d0 --- /dev/null +++ b/include/oom_killer.go @@ -0,0 +1,10 @@ +package include + +import ( + "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/service/oomkiller" +) + +func registerOOMKillerService(registry *service.Registry) { + oomkiller.RegisterService(registry) +} diff --git a/include/quic.go b/include/quic.go new file mode 100644 index 00000000..6a3f3017 --- /dev/null +++ b/include/quic.go @@ -0,0 +1,32 @@ +//go:build with_quic + +package include + +import ( + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/quic" + "github.com/sagernet/sing-box/protocol/hysteria" + "github.com/sagernet/sing-box/protocol/hysteria2" + _ "github.com/sagernet/sing-box/protocol/naive/quic" + "github.com/sagernet/sing-box/protocol/tuic" + _ "github.com/sagernet/sing-box/transport/v2rayquic" +) + +func registerQUICInbounds(registry *inbound.Registry) { + hysteria.RegisterInbound(registry) + tuic.RegisterInbound(registry) + hysteria2.RegisterInbound(registry) +} + +func registerQUICOutbounds(registry *outbound.Registry) { + hysteria.RegisterOutbound(registry) + tuic.RegisterOutbound(registry) + hysteria2.RegisterOutbound(registry) +} + +func registerQUICTransports(registry *dns.TransportRegistry) { + quic.RegisterTransport(registry) + quic.RegisterHTTP3Transport(registry) +} diff --git a/include/quic_stub.go b/include/quic_stub.go new file mode 100644 index 00000000..d2c03b98 --- /dev/null +++ b/include/quic_stub.go @@ -0,0 +1,71 @@ +//go:build !with_quic + +package include + +import ( + "context" + "io" + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/naive" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func init() { + v2ray.RegisterQUICConstructor( + func(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + return nil, C.ErrQUICNotIncluded + }, + func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + return nil, C.ErrQUICNotIncluded + }, + ) +} + +func registerQUICInbounds(registry *inbound.Registry) { + inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { + return nil, C.ErrQUICNotIncluded + }) + inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { + return nil, C.ErrQUICNotIncluded + }) + inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { + return nil, C.ErrQUICNotIncluded + }) + naive.ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) { + return nil, C.ErrQUICNotIncluded + } +} + +func registerQUICOutbounds(registry *outbound.Registry) { + outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { + return nil, C.ErrQUICNotIncluded + }) + outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { + return nil, C.ErrQUICNotIncluded + }) + outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { + return nil, C.ErrQUICNotIncluded + }) +} + +func registerQUICTransports(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, func(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { + return nil, C.ErrQUICNotIncluded + }) + dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, func(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { + return nil, C.ErrQUICNotIncluded + }) +} diff --git a/include/registry.go b/include/registry.go new file mode 100644 index 00000000..5a1a2f97 --- /dev/null +++ b/include/registry.go @@ -0,0 +1,168 @@ +package include + +import ( + "context" + + "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "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" + "github.com/sagernet/sing-box/dns/transport/fakeip" + "github.com/sagernet/sing-box/dns/transport/hosts" + "github.com/sagernet/sing-box/dns/transport/local" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/anytls" + "github.com/sagernet/sing-box/protocol/block" + "github.com/sagernet/sing-box/protocol/direct" + "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing-box/protocol/http" + "github.com/sagernet/sing-box/protocol/mixed" + "github.com/sagernet/sing-box/protocol/naive" + "github.com/sagernet/sing-box/protocol/redirect" + "github.com/sagernet/sing-box/protocol/shadowsocks" + "github.com/sagernet/sing-box/protocol/shadowtls" + "github.com/sagernet/sing-box/protocol/socks" + "github.com/sagernet/sing-box/protocol/ssh" + "github.com/sagernet/sing-box/protocol/tor" + "github.com/sagernet/sing-box/protocol/trojan" + "github.com/sagernet/sing-box/protocol/tun" + "github.com/sagernet/sing-box/protocol/vless" + "github.com/sagernet/sing-box/protocol/vmess" + originca "github.com/sagernet/sing-box/service/origin_ca" + "github.com/sagernet/sing-box/service/resolved" + "github.com/sagernet/sing-box/service/ssmapi" + E "github.com/sagernet/sing/common/exceptions" +) + +func Context(ctx context.Context) context.Context { + return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry(), CertificateProviderRegistry()) +} + +func InboundRegistry() *inbound.Registry { + registry := inbound.NewRegistry() + + tun.RegisterInbound(registry) + redirect.RegisterRedirect(registry) + redirect.RegisterTProxy(registry) + direct.RegisterInbound(registry) + + socks.RegisterInbound(registry) + http.RegisterInbound(registry) + mixed.RegisterInbound(registry) + + shadowsocks.RegisterInbound(registry) + vmess.RegisterInbound(registry) + trojan.RegisterInbound(registry) + naive.RegisterInbound(registry) + shadowtls.RegisterInbound(registry) + vless.RegisterInbound(registry) + anytls.RegisterInbound(registry) + + registerQUICInbounds(registry) + registerCloudflaredInbound(registry) + registerStubForRemovedInbounds(registry) + + return registry +} + +func OutboundRegistry() *outbound.Registry { + registry := outbound.NewRegistry() + + direct.RegisterOutbound(registry) + + block.RegisterOutbound(registry) + + group.RegisterSelector(registry) + group.RegisterURLTest(registry) + + socks.RegisterOutbound(registry) + http.RegisterOutbound(registry) + shadowsocks.RegisterOutbound(registry) + vmess.RegisterOutbound(registry) + trojan.RegisterOutbound(registry) + registerNaiveOutbound(registry) + tor.RegisterOutbound(registry) + ssh.RegisterOutbound(registry) + shadowtls.RegisterOutbound(registry) + vless.RegisterOutbound(registry) + anytls.RegisterOutbound(registry) + + registerQUICOutbounds(registry) + registerStubForRemovedOutbounds(registry) + + return registry +} + +func EndpointRegistry() *endpoint.Registry { + registry := endpoint.NewRegistry() + + registerWireGuardEndpoint(registry) + registerTailscaleEndpoint(registry) + + return registry +} + +func DNSTransportRegistry() *dns.TransportRegistry { + registry := dns.NewTransportRegistry() + + transport.RegisterTCP(registry) + transport.RegisterUDP(registry) + transport.RegisterTLS(registry) + transport.RegisterHTTPS(registry) + hosts.RegisterTransport(registry) + local.RegisterTransport(registry) + fakeip.RegisterTransport(registry) + resolved.RegisterTransport(registry) + + registerQUICTransports(registry) + registerDHCPTransport(registry) + registerTailscaleTransport(registry) + + return registry +} + +func ServiceRegistry() *service.Registry { + registry := service.NewRegistry() + + resolved.RegisterService(registry) + ssmapi.RegisterService(registry) + + registerDERPService(registry) + registerCCMService(registry) + registerOCMService(registry) + registerOOMKillerService(registry) + + return registry +} + +func CertificateProviderRegistry() *certificate.Registry { + registry := certificate.NewRegistry() + + registerACMECertificateProvider(registry) + registerTailscaleCertificateProvider(registry) + originca.RegisterCertificateProvider(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") + }) +} + +func registerStubForRemovedOutbounds(registry *outbound.Registry) { + outbound.Register[option.ShadowsocksROutboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) { + return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") + }) + outbound.Register[option.StubOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) { + return nil, E.New("WireGuard outbound is deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use WireGuard endpoint instead") + }) +} diff --git a/include/tailscale.go b/include/tailscale.go new file mode 100644 index 00000000..6f85aaac --- /dev/null +++ b/include/tailscale.go @@ -0,0 +1,28 @@ +//go:build with_tailscale + +package include + +import ( + "github.com/sagernet/sing-box/adapter/certificate" + "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) { + tailscale.RegisterEndpoint(registry) +} + +func registerTailscaleTransport(registry *dns.TransportRegistry) { + tailscale.RegistryTransport(registry) +} + +func registerTailscaleCertificateProvider(registry *certificate.Registry) { + tailscale.RegisterCertificateProvider(registry) +} + +func registerDERPService(registry *service.Registry) { + derp.Register(registry) +} diff --git a/include/tailscale_stub.go b/include/tailscale_stub.go new file mode 100644 index 00000000..e6f97f1e --- /dev/null +++ b/include/tailscale_stub.go @@ -0,0 +1,41 @@ +//go:build !with_tailscale + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "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" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerTailscaleEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) { + return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) + }) +} + +func registerTailscaleTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) { + return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) + }) +} + +func registerTailscaleCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.TailscaleCertificateProviderOptions](registry, C.TypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleCertificateProviderOptions) (adapter.CertificateProviderService, error) { + 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`) + }) +} diff --git a/include/tz_android.go b/include/tz_android.go new file mode 100644 index 00000000..7be1c2da --- /dev/null +++ b/include/tz_android.go @@ -0,0 +1,21 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 + +package include + +// #include +import "C" +import "time" + +func init() { + var currentT C.time_t + var currentTM C.struct_tm + C.time(¤tT) + C.localtime_r(¤tT, ¤tTM) + tzOffset := int(currentTM.tm_gmtoff) + tz := C.GoString(currentTM.tm_zone) + time.Local = time.FixedZone(tz, tzOffset) +} diff --git a/include/tz_ios.go b/include/tz_ios.go new file mode 100644 index 00000000..fc30479c --- /dev/null +++ b/include/tz_ios.go @@ -0,0 +1,30 @@ +package include + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation +#import +const char* getSystemTimeZone() { + NSTimeZone *timeZone = [NSTimeZone systemTimeZone]; + NSString *timeZoneName = [timeZone description]; + return [timeZoneName UTF8String]; +} +*/ +import "C" + +import ( + "strings" + "time" +) + +func init() { + tzDescription := C.GoString(C.getSystemTimeZone()) + if len(tzDescription) == 0 { + return + } + location, err := time.LoadLocation(strings.Split(tzDescription, " ")[0]) + if err != nil { + return + } + time.Local = location +} diff --git a/include/v2rayapi.go b/include/v2rayapi.go new file mode 100644 index 00000000..acd3e9a8 --- /dev/null +++ b/include/v2rayapi.go @@ -0,0 +1,5 @@ +//go:build with_v2ray_api + +package include + +import _ "github.com/sagernet/sing-box/experimental/v2rayapi" diff --git a/include/v2rayapi_stub.go b/include/v2rayapi_stub.go new file mode 100644 index 00000000..7f1f6b9e --- /dev/null +++ b/include/v2rayapi_stub.go @@ -0,0 +1,17 @@ +//go:build !with_v2ray_api + +package include + +import ( + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func init() { + experimental.RegisterV2RayServerConstructor(func(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) { + return nil, E.New(`v2ray api is not included in this build, rebuild with -tags with_v2ray_api`) + }) +} diff --git a/include/wireguard.go b/include/wireguard.go new file mode 100644 index 00000000..fa7cfe6f --- /dev/null +++ b/include/wireguard.go @@ -0,0 +1,12 @@ +//go:build with_wireguard + +package include + +import ( + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/protocol/wireguard" +) + +func registerWireGuardEndpoint(registry *endpoint.Registry) { + wireguard.RegisterEndpoint(registry) +} diff --git a/include/wireguard_stub.go b/include/wireguard_stub.go new file mode 100644 index 00000000..e03a9d9c --- /dev/null +++ b/include/wireguard_stub.go @@ -0,0 +1,20 @@ +//go:build !with_wireguard + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerWireGuardEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) { + return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) + }) +} diff --git a/log/export.go b/log/export.go new file mode 100644 index 00000000..60a0abbb --- /dev/null +++ b/log/export.go @@ -0,0 +1,84 @@ +package log + +import ( + "context" + "os" + "time" +) + +var std ContextLogger + +func init() { + std = NewDefaultFactory( + context.Background(), + Formatter{BaseTime: time.Now()}, + os.Stderr, + "", + nil, + false, + ).Logger() +} + +func StdLogger() ContextLogger { + return std +} + +func SetStdLogger(logger ContextLogger) { + std = logger +} + +func Trace(args ...any) { + std.Trace(args...) +} + +func Debug(args ...any) { + std.Debug(args...) +} + +func Info(args ...any) { + std.Info(args...) +} + +func Warn(args ...any) { + std.Warn(args...) +} + +func Error(args ...any) { + std.Error(args...) +} + +func Fatal(args ...any) { + std.Fatal(args...) +} + +func Panic(args ...any) { + std.Panic(args...) +} + +func TraceContext(ctx context.Context, args ...any) { + std.TraceContext(ctx, args...) +} + +func DebugContext(ctx context.Context, args ...any) { + std.DebugContext(ctx, args...) +} + +func InfoContext(ctx context.Context, args ...any) { + std.InfoContext(ctx, args...) +} + +func WarnContext(ctx context.Context, args ...any) { + std.WarnContext(ctx, args...) +} + +func ErrorContext(ctx context.Context, args ...any) { + std.ErrorContext(ctx, args...) +} + +func FatalContext(ctx context.Context, args ...any) { + std.FatalContext(ctx, args...) +} + +func PanicContext(ctx context.Context, args ...any) { + std.PanicContext(ctx, args...) +} diff --git a/log/factory.go b/log/factory.go new file mode 100644 index 00000000..54a88228 --- /dev/null +++ b/log/factory.go @@ -0,0 +1,30 @@ +package log + +import ( + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/observable" +) + +type ( + Logger logger.Logger + ContextLogger logger.ContextLogger +) + +type Factory interface { + Start() error + Close() error + Level() Level + SetLevel(level Level) + Logger() ContextLogger + NewLogger(tag string) ContextLogger +} + +type ObservableFactory interface { + Factory + observable.Observable[Entry] +} + +type Entry struct { + Level Level + Message string +} diff --git a/log/format.go b/log/format.go new file mode 100644 index 00000000..6f4347b1 --- /dev/null +++ b/log/format.go @@ -0,0 +1,174 @@ +package log + +import ( + "context" + "strconv" + "strings" + "time" + + F "github.com/sagernet/sing/common/format" + + "github.com/logrusorgru/aurora" +) + +type Formatter struct { + BaseTime time.Time + DisableColors bool + DisableTimestamp bool + FullTimestamp bool + TimestampFormat string + DisableLineBreak bool +} + +func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string { + levelString := strings.ToUpper(FormatLevel(level)) + if !f.DisableColors { + switch level { + case LevelDebug, LevelTrace: + levelString = aurora.White(levelString).String() + case LevelInfo: + levelString = aurora.Cyan(levelString).String() + case LevelWarn: + levelString = aurora.Yellow(levelString).String() + case LevelError, LevelFatal, LevelPanic: + levelString = aurora.Red(levelString).String() + } + } + if tag != "" { + message = tag + ": " + message + } + var id ID + var hasId bool + if ctx != nil { + id, hasId = IDFromContext(ctx) + } + if hasId { + activeDuration := FormatDuration(time.Since(id.CreatedAt)) + if !f.DisableColors { + var color aurora.Color + color = aurora.Color(uint8(id.ID)) + color %= 215 + row := uint(color / 36) + column := uint(color % 36) + + var r, g, b float32 + r = float32(row * 51) + g = float32(column / 6 * 51) + b = float32((column % 6) * 51) + luma := 0.2126*r + 0.7152*g + 0.0722*b + if luma < 60 { + row = 5 - row + column = 35 - column + color = aurora.Color(row*36 + column) + } + color += 16 + color = color << 16 + color |= 1 << 14 + message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message) + } else { + message = F.ToString("[", id.ID, " ", activeDuration, "] ", message) + } + } + switch { + case f.DisableTimestamp: + message = levelString + " " + message + case f.FullTimestamp: + message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message + default: + message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message + } + if f.DisableLineBreak { + if message[len(message)-1] == '\n' { + message = message[:len(message)-1] + } + } else { + if message[len(message)-1] != '\n' { + message += "\n" + } + } + return message +} + +func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string, message string, timestamp time.Time) (string, string) { + levelString := strings.ToUpper(FormatLevel(level)) + if !f.DisableColors { + switch level { + case LevelDebug, LevelTrace: + levelString = aurora.White(levelString).String() + case LevelInfo: + levelString = aurora.Cyan(levelString).String() + case LevelWarn: + levelString = aurora.Yellow(levelString).String() + case LevelError, LevelFatal, LevelPanic: + levelString = aurora.Red(levelString).String() + } + } + if tag != "" { + message = tag + ": " + message + } + messageSimple := message + var id ID + var hasId bool + if ctx != nil { + id, hasId = IDFromContext(ctx) + } + if hasId { + activeDuration := FormatDuration(time.Since(id.CreatedAt)) + if !f.DisableColors { + var color aurora.Color + color = aurora.Color(uint8(id.ID)) + color %= 215 + row := uint(color / 36) + column := uint(color % 36) + + var r, g, b float32 + r = float32(row * 51) + g = float32(column / 6 * 51) + b = float32((column % 6) * 51) + luma := 0.2126*r + 0.7152*g + 0.0722*b + if luma < 60 { + row = 5 - row + column = 35 - column + color = aurora.Color(row*36 + column) + } + color += 16 + color = color << 16 + color |= 1 << 14 + message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message) + } else { + message = F.ToString("[", id.ID, " ", activeDuration, "] ", message) + } + messageSimple = F.ToString("[", id.ID, " ", activeDuration, "] ", messageSimple) + + } + switch { + case f.DisableTimestamp: + message = levelString + " " + message + case f.FullTimestamp: + message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message + default: + message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message + } + if message[len(message)-1] != '\n' { + message += "\n" + } + return message, messageSimple +} + +func xd(value int, x int) string { + message := strconv.Itoa(value) + for len(message) < x { + message = "0" + message + } + return message +} + +func FormatDuration(duration time.Duration) string { + if duration < time.Second { + return F.ToString(duration.Milliseconds(), "ms") + } else if duration < time.Minute { + return F.ToString(int64(duration.Seconds()), ".", int64(duration.Seconds()*100)%100, "s") + } else { + return F.ToString(int64(duration.Minutes()), "m", int64(duration.Seconds())%60, "s") + } +} diff --git a/log/id.go b/log/id.go new file mode 100644 index 00000000..7cac29d2 --- /dev/null +++ b/log/id.go @@ -0,0 +1,36 @@ +package log + +import ( + "context" + "math/rand" + "time" + + "github.com/sagernet/sing/common/random" +) + +func init() { + random.InitializeSeed() +} + +type idKey struct{} + +type ID struct { + ID uint32 + CreatedAt time.Time +} + +func ContextWithNewID(ctx context.Context) context.Context { + return ContextWithID(ctx, ID{ + ID: rand.Uint32(), + CreatedAt: time.Now(), + }) +} + +func ContextWithID(ctx context.Context, id ID) context.Context { + return context.WithValue(ctx, (*idKey)(nil), id) +} + +func IDFromContext(ctx context.Context) (ID, bool) { + id, loaded := ctx.Value((*idKey)(nil)).(ID) + return id, loaded +} diff --git a/log/level.go b/log/level.go new file mode 100644 index 00000000..b216fa34 --- /dev/null +++ b/log/level.go @@ -0,0 +1,59 @@ +package log + +import ( + E "github.com/sagernet/sing/common/exceptions" +) + +type Level = uint8 + +const ( + LevelPanic Level = iota + LevelFatal + LevelError + LevelWarn + LevelInfo + LevelDebug + LevelTrace +) + +func FormatLevel(level Level) string { + switch level { + case LevelTrace: + return "trace" + case LevelDebug: + return "debug" + case LevelInfo: + return "info" + case LevelWarn: + return "warn" + case LevelError: + return "error" + case LevelFatal: + return "fatal" + case LevelPanic: + return "panic" + default: + return "unknown" + } +} + +func ParseLevel(level string) (Level, error) { + switch level { + case "trace": + return LevelTrace, nil + case "debug": + return LevelDebug, nil + case "info": + return LevelInfo, nil + case "warn", "warning": + return LevelWarn, nil + case "error": + return LevelError, nil + case "fatal": + return LevelFatal, nil + case "panic": + return LevelPanic, nil + default: + return LevelTrace, E.New("unknown log level: ", level) + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000..3a1c6537 --- /dev/null +++ b/log/log.go @@ -0,0 +1,71 @@ +package log + +import ( + "context" + "io" + "os" + "time" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +type Options struct { + Context context.Context + Options option.LogOptions + Observable bool + DefaultWriter io.Writer + BaseTime time.Time + PlatformWriter PlatformWriter +} + +func New(options Options) (Factory, error) { + logOptions := options.Options + + if logOptions.Disabled { + return NewNOPFactory(), nil + } + + var logWriter io.Writer + var logFilePath string + + switch logOptions.Output { + case "": + logWriter = options.DefaultWriter + if logWriter == nil { + logWriter = os.Stderr + } + case "stderr": + logWriter = os.Stderr + case "stdout": + logWriter = os.Stdout + default: + logWriter = io.Discard + logFilePath = logOptions.Output + } + logFormatter := Formatter{ + BaseTime: options.BaseTime, + DisableColors: logOptions.DisableColor || logFilePath != "", + DisableTimestamp: !logOptions.Timestamp && logFilePath != "", + FullTimestamp: logOptions.Timestamp, + TimestampFormat: "-0700 2006-01-02 15:04:05", + } + factory := NewDefaultFactory( + options.Context, + logFormatter, + logWriter, + logFilePath, + options.PlatformWriter, + options.Observable, + ) + if logOptions.Level != "" { + logLevel, err := ParseLevel(logOptions.Level) + if err != nil { + return nil, E.Cause(err, "parse log level") + } + factory.SetLevel(logLevel) + } else { + factory.SetLevel(LevelTrace) + } + return factory, nil +} diff --git a/log/nop.go b/log/nop.go new file mode 100644 index 00000000..6369e99b --- /dev/null +++ b/log/nop.go @@ -0,0 +1,88 @@ +package log + +import ( + "context" + "os" + + "github.com/sagernet/sing/common/observable" +) + +var _ ObservableFactory = (*nopFactory)(nil) + +type nopFactory struct{} + +func NewNOPFactory() ObservableFactory { + return (*nopFactory)(nil) +} + +func (f *nopFactory) Start() error { + return nil +} + +func (f *nopFactory) Close() error { + return nil +} + +func (f *nopFactory) Level() Level { + return LevelTrace +} + +func (f *nopFactory) SetLevel(level Level) { +} + +func (f *nopFactory) Logger() ContextLogger { + return f +} + +func (f *nopFactory) NewLogger(tag string) ContextLogger { + return f +} + +func (f *nopFactory) Trace(args ...any) { +} + +func (f *nopFactory) Debug(args ...any) { +} + +func (f *nopFactory) Info(args ...any) { +} + +func (f *nopFactory) Warn(args ...any) { +} + +func (f *nopFactory) Error(args ...any) { +} + +func (f *nopFactory) Fatal(args ...any) { +} + +func (f *nopFactory) Panic(args ...any) { +} + +func (f *nopFactory) TraceContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) DebugContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) InfoContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) WarnContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) ErrorContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) FatalContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) PanicContext(ctx context.Context, args ...any) { +} + +func (f *nopFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { + return nil, nil, os.ErrInvalid +} + +func (f *nopFactory) UnSubscribe(subscription observable.Subscription[Entry]) { +} diff --git a/log/observable.go b/log/observable.go new file mode 100644 index 00000000..768942bd --- /dev/null +++ b/log/observable.go @@ -0,0 +1,199 @@ +package log + +import ( + "context" + "io" + "os" + "time" + + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/service/filemanager" +) + +var _ Factory = (*defaultFactory)(nil) + +type defaultFactory struct { + ctx context.Context + formatter Formatter + platformFormatter Formatter + writer io.Writer + file *os.File + filePath string + platformWriter PlatformWriter + needObservable bool + level Level + subscriber *observable.Subscriber[Entry] + observer *observable.Observer[Entry] +} + +func NewDefaultFactory( + ctx context.Context, + formatter Formatter, + writer io.Writer, + filePath string, + platformWriter PlatformWriter, + needObservable bool, +) ObservableFactory { + factory := &defaultFactory{ + ctx: ctx, + formatter: formatter, + platformFormatter: Formatter{ + BaseTime: formatter.BaseTime, + DisableLineBreak: true, + }, + writer: writer, + filePath: filePath, + platformWriter: platformWriter, + needObservable: needObservable, + level: LevelTrace, + subscriber: observable.NewSubscriber[Entry](128), + } + /*if platformWriter != nil { + factory.platformFormatter.DisableColors = platformWriter.DisableColors() + }*/ + if needObservable { + factory.observer = observable.NewObserver[Entry](factory.subscriber, 64) + } + return factory +} + +func (f *defaultFactory) Start() error { + if f.filePath != "" { + logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + f.writer = logFile + f.file = logFile + } + return nil +} + +func (f *defaultFactory) Close() error { + return common.Close( + common.PtrOrNil(f.file), + f.subscriber, + ) +} + +func (f *defaultFactory) Level() Level { + return f.level +} + +func (f *defaultFactory) SetLevel(level Level) { + f.level = level +} + +func (f *defaultFactory) Logger() ContextLogger { + return f.NewLogger("") +} + +func (f *defaultFactory) NewLogger(tag string) ContextLogger { + return &observableLogger{f, tag} +} + +func (f *defaultFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { + return f.observer.Subscribe() +} + +func (f *defaultFactory) UnSubscribe(sub observable.Subscription[Entry]) { + f.observer.UnSubscribe(sub) +} + +var _ ContextLogger = (*observableLogger)(nil) + +type observableLogger struct { + *defaultFactory + tag string +} + +func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { + level = OverrideLevelFromContext(level, ctx) + if level > l.level && l.platformWriter == nil { + return + } + nowTime := time.Now() + if level <= l.level { + if l.needObservable { + message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) + if level == LevelPanic { + panic(message) + } + l.writer.Write([]byte(message)) + if level == LevelFatal { + os.Exit(1) + } + l.subscriber.Emit(Entry{level, messageSimple}) + } else { + message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) + if level == LevelPanic { + panic(message) + } + l.writer.Write([]byte(message)) + if level == LevelFatal { + os.Exit(1) + } + } + } + if l.platformWriter != nil { + l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) + } +} + +func (l *observableLogger) Trace(args ...any) { + l.TraceContext(context.Background(), args...) +} + +func (l *observableLogger) Debug(args ...any) { + l.DebugContext(context.Background(), args...) +} + +func (l *observableLogger) Info(args ...any) { + l.InfoContext(context.Background(), args...) +} + +func (l *observableLogger) Warn(args ...any) { + l.WarnContext(context.Background(), args...) +} + +func (l *observableLogger) Error(args ...any) { + l.ErrorContext(context.Background(), args...) +} + +func (l *observableLogger) Fatal(args ...any) { + l.FatalContext(context.Background(), args...) +} + +func (l *observableLogger) Panic(args ...any) { + l.PanicContext(context.Background(), args...) +} + +func (l *observableLogger) TraceContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelTrace, args) +} + +func (l *observableLogger) DebugContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelDebug, args) +} + +func (l *observableLogger) InfoContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelInfo, args) +} + +func (l *observableLogger) WarnContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelWarn, args) +} + +func (l *observableLogger) ErrorContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelError, args) +} + +func (l *observableLogger) FatalContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelFatal, args) +} + +func (l *observableLogger) PanicContext(ctx context.Context, args ...any) { + l.Log(ctx, LevelPanic, args) +} diff --git a/log/override.go b/log/override.go new file mode 100644 index 00000000..42e26e60 --- /dev/null +++ b/log/override.go @@ -0,0 +1,19 @@ +package log + +import ( + "context" +) + +type overrideLevelKey struct{} + +func ContextWithOverrideLevel(ctx context.Context, level Level) context.Context { + return context.WithValue(ctx, (*overrideLevelKey)(nil), level) +} + +func OverrideLevelFromContext(origin Level, ctx context.Context) Level { + level, loaded := ctx.Value((*overrideLevelKey)(nil)).(Level) + if !loaded || origin > level { + return origin + } + return level +} diff --git a/log/platform.go b/log/platform.go new file mode 100644 index 00000000..a8881d4c --- /dev/null +++ b/log/platform.go @@ -0,0 +1,5 @@ +package log + +type PlatformWriter interface { + WriteMessage(level Level, message string) +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..5387be9d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,295 @@ +site_name: sing-box +site_url: https://sing-box.sagernet.org/ +site_author: nekohasekai +repo_url: https://github.com/SagerNet/sing-box +repo_name: SagerNet/sing-box +copyright: Copyright © 2022 nekohasekai +site_description: The universal proxy platform. +remote_branch: docs +edit_uri: "" +theme: + name: material + logo: assets/icon.svg + favicon: assets/icon.svg + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/link + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + toggle: + icon: material/toggle-switch + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + toggle: + icon: material/toggle-switch-off + name: Switch to system preference + features: + # - navigation.instant + - navigation.tracking + - navigation.tabs + - navigation.indexes + - navigation.expand + - navigation.sections + - header.autohide + - content.code.copy + - content.code.select + - content.code.annotate + icon: + admonition: + question: material/new-box +nav: + - Home: + - index.md + - Change Log: changelog.md + - Migration: migration.md + - Deprecated: deprecated.md + - Support: support.md + - Sponsors: sponsors.md + - Installation: + - Package Manager: installation/package-manager.md + - Docker: installation/docker.md + - Build from source: installation/build-from-source.md + - Graphical Clients: + - clients/index.md + - Android: + - clients/android/index.md + - Features: clients/android/features.md + - Apple platforms: + - clients/apple/index.md + - Features: clients/apple/features.md + - General: clients/general.md + - Privacy policy: clients/privacy.md + - Manual: + - Proxy: + - Server: manual/proxy/server.md + - Client: manual/proxy/client.md + # - TUN: manual/proxy/tun.md + - Proxy Protocol: + - Shadowsocks: manual/proxy-protocol/shadowsocks.md + - Trojan: manual/proxy-protocol/trojan.md + - Hysteria 2: manual/proxy-protocol/hysteria2.md + - Misc: + - TunnelVision: manual/misc/tunnelvision.md + - Configuration: + - configuration/index.md + - Log: + - configuration/log/index.md + - DNS: + - configuration/dns/index.md + - DNS Server: + - configuration/dns/server/index.md + - Legacy: configuration/dns/server/legacy.md + - Local: configuration/dns/server/local.md + - Hosts: configuration/dns/server/hosts.md + - TCP: configuration/dns/server/tcp.md + - UDP: configuration/dns/server/udp.md + - TLS: configuration/dns/server/tls.md + - QUIC: configuration/dns/server/quic.md + - HTTPS: configuration/dns/server/https.md + - HTTP3: configuration/dns/server/http3.md + - DHCP: configuration/dns/server/dhcp.md + - FakeIP: configuration/dns/server/fakeip.md + - Tailscale: configuration/dns/server/tailscale.md + - Resolved: configuration/dns/server/resolved.md + - DNS Rule: configuration/dns/rule.md + - DNS Rule Action: configuration/dns/rule_action.md + - FakeIP: configuration/dns/fakeip.md + - NTP: configuration/ntp/index.md + - Certificate: configuration/certificate/index.md + - Route: + - configuration/route/index.md + - GeoIP: configuration/route/geoip.md + - Geosite: configuration/route/geosite.md + - Route Rule: configuration/route/rule.md + - Rule Action: configuration/route/rule_action.md + - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md + - AdGuard DNS Filer: configuration/rule-set/adguard.md + - Experimental: + - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md + - Shared: + - Listen Fields: configuration/shared/listen.md + - Dial Fields: configuration/shared/dial.md + - TLS: configuration/shared/tls.md + - Certificate Provider: + - configuration/shared/certificate-provider/index.md + - ACME: configuration/shared/certificate-provider/acme.md + - Tailscale: configuration/shared/certificate-provider/tailscale.md + - Cloudflare Origin CA: configuration/shared/certificate-provider/cloudflare-origin-ca.md + - DNS01 Challenge Fields: configuration/shared/dns01_challenge.md + - Pre-match: configuration/shared/pre-match.md + - Multiplex: configuration/shared/multiplex.md + - V2Ray Transport: configuration/shared/v2ray-transport.md + - UDP over TCP: configuration/shared/udp-over-tcp.md + - TCP Brutal: configuration/shared/tcp-brutal.md + - Wi-Fi State: configuration/shared/wifi-state.md + - Neighbor Resolution: configuration/shared/neighbor.md + - Endpoint: + - configuration/endpoint/index.md + - WireGuard: configuration/endpoint/wireguard.md + - Tailscale: configuration/endpoint/tailscale.md + - Inbound: + - configuration/inbound/index.md + - Direct: configuration/inbound/direct.md + - Mixed: configuration/inbound/mixed.md + - SOCKS: configuration/inbound/socks.md + - HTTP: configuration/inbound/http.md + - Shadowsocks: configuration/inbound/shadowsocks.md + - VMess: configuration/inbound/vmess.md + - Trojan: configuration/inbound/trojan.md + - Naive: configuration/inbound/naive.md + - Hysteria: configuration/inbound/hysteria.md + - ShadowTLS: configuration/inbound/shadowtls.md + - VLESS: configuration/inbound/vless.md + - TUIC: configuration/inbound/tuic.md + - Hysteria2: configuration/inbound/hysteria2.md + - AnyTLS: configuration/inbound/anytls.md + - Tun: configuration/inbound/tun.md + - Redirect: configuration/inbound/redirect.md + - TProxy: configuration/inbound/tproxy.md + - Cloudflared: configuration/inbound/cloudflared.md + - Outbound: + - configuration/outbound/index.md + - Direct: configuration/outbound/direct.md + - Block: configuration/outbound/block.md + - SOCKS: configuration/outbound/socks.md + - HTTP: configuration/outbound/http.md + - Shadowsocks: configuration/outbound/shadowsocks.md + - VMess: configuration/outbound/vmess.md + - Trojan: configuration/outbound/trojan.md + - Naive: configuration/outbound/naive.md + - WireGuard: configuration/outbound/wireguard.md + - Hysteria: configuration/outbound/hysteria.md + - ShadowTLS: configuration/outbound/shadowtls.md + - VLESS: configuration/outbound/vless.md + - TUIC: configuration/outbound/tuic.md + - Hysteria2: configuration/outbound/hysteria2.md + - AnyTLS: configuration/outbound/anytls.md + - Tor: configuration/outbound/tor.md + - SSH: configuration/outbound/ssh.md + - DNS: configuration/outbound/dns.md + - Selector: configuration/outbound/selector.md + - URLTest: configuration/outbound/urltest.md + - Service: + - configuration/service/index.md + - DERP: configuration/service/derp.md + - Resolved: configuration/service/resolved.md + - SSM API: configuration/service/ssm-api.md + - CCM: configuration/service/ccm.md + - OCM: configuration/service/ocm.md +markdown_extensions: + - toc: + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.details + - pymdownx.critic + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde + - pymdownx.magiclink + - admonition + - attr_list + - md_in_html + - footnotes + - def_list + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/SagerNet/sing-box + generator: false +plugins: + - search + - i18n: + docs_structure: suffix + fallback_to_default: true + languages: + - build: true + default: true + locale: en + name: English + - build: true + default: false + locale: zh + name: 简体中文 + nav_translations: + Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 + Deprecated: 废弃功能列表 + Support: 支持 + + Installation: 安装 + Package Manager: 包管理器 + Build from source: 从源代码构建 + + Graphical Clients: 图形界面客户端 + Features: 特性 + Apple platforms: Apple 平台 + General: 通用 + Privacy policy: 隐私政策 + + Configuration: 配置 + Log: 日志 + DNS Server: DNS 服务器 + DNS Rule: DNS 规则 + DNS Rule Action: DNS 规则动作 + + Route: 路由 + Route Rule: 路由规则 + Rule Action: 规则动作 + Protocol Sniff: 协议探测 + + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + + Experimental: 实验性 + Cache File: 缓存文件 + + Shared: 通用 + Listen Fields: 监听字段 + Dial Fields: 拨号字段 + Certificate Provider Fields: 证书提供者字段 + DNS01 Challenge Fields: DNS01 验证字段 + Multiplex: 多路复用 + V2Ray Transport: V2Ray 传输层 + Wi-Fi State: Wi-Fi 状态 + + Endpoint: 端点 + Inbound: 入站 + Outbound: 出站 + Certificate Provider: 证书提供者 + + Manual: 手册 + reconfigure_material: true + reconfigure_search: true diff --git a/option/acme.go b/option/acme.go new file mode 100644 index 00000000..ea9349b7 --- /dev/null +++ b/option/acme.go @@ -0,0 +1,106 @@ +package option + +import ( + "strings" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type ACMECertificateProviderOptions struct { + Domain badoption.Listable[string] `json:"domain,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + DefaultServerName string `json:"default_server_name,omitempty"` + Email string `json:"email,omitempty"` + Provider string `json:"provider,omitempty"` + AccountKey string `json:"account_key,omitempty"` + DisableHTTPChallenge bool `json:"disable_http_challenge,omitempty"` + DisableTLSALPNChallenge bool `json:"disable_tls_alpn_challenge,omitempty"` + AlternativeHTTPPort uint16 `json:"alternative_http_port,omitempty"` + AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"` + ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"` + DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` + KeyType ACMEKeyType `json:"key_type,omitempty"` + Detour string `json:"detour,omitempty"` +} + +type _ACMEProviderDNS01ChallengeOptions struct { + TTL badoption.Duration `json:"ttl,omitempty"` + PropagationDelay badoption.Duration `json:"propagation_delay,omitempty"` + PropagationTimeout badoption.Duration `json:"propagation_timeout,omitempty"` + Resolvers badoption.Listable[string] `json:"resolvers,omitempty"` + OverrideDomain string `json:"override_domain,omitempty"` + Provider string `json:"provider,omitempty"` + AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"` + CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"` + ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"` +} + +type ACMEProviderDNS01ChallengeOptions _ACMEProviderDNS01ChallengeOptions + +func (o ACMEProviderDNS01ChallengeOptions) MarshalJSON() ([]byte, error) { + var v any + switch o.Provider { + case C.DNSProviderAliDNS: + v = o.AliDNSOptions + case C.DNSProviderCloudflare: + v = o.CloudflareOptions + case C.DNSProviderACMEDNS: + v = o.ACMEDNSOptions + case "": + return nil, E.New("missing provider type") + default: + return nil, E.New("unknown provider type: ", o.Provider) + } + return badjson.MarshallObjects((_ACMEProviderDNS01ChallengeOptions)(o), v) +} + +func (o *ACMEProviderDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o)) + if err != nil { + return err + } + var v any + switch o.Provider { + case C.DNSProviderAliDNS: + v = &o.AliDNSOptions + case C.DNSProviderCloudflare: + v = &o.CloudflareOptions + case C.DNSProviderACMEDNS: + v = &o.ACMEDNSOptions + case "": + return E.New("missing provider type") + default: + return E.New("unknown provider type: ", o.Provider) + } + return badjson.UnmarshallExcluded(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o), v) +} + +type ACMEKeyType string + +const ( + ACMEKeyTypeED25519 = ACMEKeyType("ed25519") + ACMEKeyTypeP256 = ACMEKeyType("p256") + ACMEKeyTypeP384 = ACMEKeyType("p384") + ACMEKeyTypeRSA2048 = ACMEKeyType("rsa2048") + ACMEKeyTypeRSA4096 = ACMEKeyType("rsa4096") +) + +func (t *ACMEKeyType) UnmarshalJSON(data []byte) error { + var value string + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + value = strings.ToLower(value) + switch ACMEKeyType(value) { + case "", ACMEKeyTypeED25519, ACMEKeyTypeP256, ACMEKeyTypeP384, ACMEKeyTypeRSA2048, ACMEKeyTypeRSA4096: + *t = ACMEKeyType(value) + default: + return E.New("unknown ACME key type: ", value) + } + return nil +} diff --git a/option/anytls.go b/option/anytls.go new file mode 100644 index 00000000..0f785263 --- /dev/null +++ b/option/anytls.go @@ -0,0 +1,25 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type AnyTLSInboundOptions struct { + ListenOptions + InboundTLSOptionsContainer + Users []AnyTLSUser `json:"users,omitempty"` + PaddingScheme badoption.Listable[string] `json:"padding_scheme,omitempty"` +} + +type AnyTLSUser struct { + Name string `json:"name,omitempty"` + Password string `json:"password,omitempty"` +} + +type AnyTLSOutboundOptions struct { + DialerOptions + ServerOptions + OutboundTLSOptionsContainer + Password string `json:"password,omitempty"` + IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"` + IdleSessionTimeout badoption.Duration `json:"idle_session_timeout,omitempty"` + MinIdleSession int `json:"min_idle_session,omitempty"` +} diff --git a/option/ccm.go b/option/ccm.go new file mode 100644 index 00000000..c916aaf2 --- /dev/null +++ b/option/ccm.go @@ -0,0 +1,20 @@ +package option + +import ( + "github.com/sagernet/sing/common/json/badoption" +) + +type CCMServiceOptions struct { + ListenOptions + InboundTLSOptionsContainer + CredentialPath string `json:"credential_path,omitempty"` + Users []CCMUser `json:"users,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + Detour string `json:"detour,omitempty"` + UsagesPath string `json:"usages_path,omitempty"` +} + +type CCMUser struct { + Name string `json:"name,omitempty"` + Token string `json:"token,omitempty"` +} diff --git a/option/certificate.go b/option/certificate.go new file mode 100644 index 00000000..ab524b99 --- /dev/null +++ b/option/certificate.go @@ -0,0 +1,36 @@ +package option + +import ( + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" +) + +type _CertificateOptions struct { + Store string `json:"store,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"` + CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"` +} + +type CertificateOptions _CertificateOptions + +func (o CertificateOptions) MarshalJSON() ([]byte, error) { + switch o.Store { + case C.CertificateStoreSystem: + o.Store = "" + } + return json.Marshal((*_CertificateOptions)(&o)) +} + +func (o *CertificateOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_CertificateOptions)(o)) + if err != nil { + return err + } + switch o.Store { + case C.CertificateStoreSystem, "": + o.Store = C.CertificateStoreSystem + } + return nil +} diff --git a/option/certificate_provider.go b/option/certificate_provider.go new file mode 100644 index 00000000..a24abdc5 --- /dev/null +++ b/option/certificate_provider.go @@ -0,0 +1,100 @@ +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 CertificateProviderOptionsRegistry interface { + CreateOptions(providerType string) (any, bool) +} + +type _CertificateProvider struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type CertificateProvider _CertificateProvider + +func (h *CertificateProvider) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_CertificateProvider)(h), h.Options) +} + +func (h *CertificateProvider) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_CertificateProvider)(h)) + if err != nil { + return err + } + registry := service.FromContext[CertificateProviderOptionsRegistry](ctx) + if registry == nil { + return E.New("missing certificate provider options registry in context") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown certificate provider type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_CertificateProvider)(h), options) + if err != nil { + return err + } + h.Options = options + return nil +} + +type CertificateProviderOptions struct { + Tag string `json:"-"` + Type string `json:"-"` + Options any `json:"-"` +} + +type _CertificateProviderInline struct { + Type string `json:"type"` +} + +func (o *CertificateProviderOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) { + if o.Tag != "" { + return json.Marshal(o.Tag) + } + return badjson.MarshallObjectsContext(ctx, _CertificateProviderInline{Type: o.Type}, o.Options) +} + +func (o *CertificateProviderOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { + if len(content) == 0 { + return E.New("empty certificate_provider value") + } + if content[0] == '"' { + return json.UnmarshalContext(ctx, content, &o.Tag) + } + var inline _CertificateProviderInline + err := json.UnmarshalContext(ctx, content, &inline) + if err != nil { + return err + } + o.Type = inline.Type + if o.Type == "" { + return E.New("missing certificate provider type") + } + registry := service.FromContext[CertificateProviderOptionsRegistry](ctx) + if registry == nil { + return E.New("missing certificate provider options registry in context") + } + options, loaded := registry.CreateOptions(o.Type) + if !loaded { + return E.New("unknown certificate provider type: ", o.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, &inline, options) + if err != nil { + return err + } + o.Options = options + return nil +} + +func (o *CertificateProviderOptions) IsShared() bool { + return o.Tag != "" +} diff --git a/option/cloudflared.go b/option/cloudflared.go new file mode 100644 index 00000000..e94a20fe --- /dev/null +++ b/option/cloudflared.go @@ -0,0 +1,16 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type CloudflaredInboundOptions struct { + Token string `json:"token,omitempty"` + HighAvailabilityConnections int `json:"ha_connections,omitempty"` + Protocol string `json:"protocol,omitempty"` + PostQuantum bool `json:"post_quantum,omitempty"` + EdgeIPVersion int `json:"edge_ip_version,omitempty"` + DatagramVersion string `json:"datagram_version,omitempty"` + GracePeriod badoption.Duration `json:"grace_period,omitempty"` + Region string `json:"region,omitempty"` + ControlDialer DialerOptions `json:"control_dialer,omitempty"` + TunnelDialer DialerOptions `json:"tunnel_dialer,omitempty"` +} diff --git a/option/debug.go b/option/debug.go new file mode 100644 index 00000000..3dfef7b0 --- /dev/null +++ b/option/debug.go @@ -0,0 +1,14 @@ +package option + +import "github.com/sagernet/sing/common/byteformats" + +type DebugOptions struct { + Listen string `json:"listen,omitempty"` + GCPercent *int `json:"gc_percent,omitempty"` + MaxStack *int `json:"max_stack,omitempty"` + MaxThreads *int `json:"max_threads,omitempty"` + PanicOnFault *bool `json:"panic_on_fault,omitempty"` + TraceBack string `json:"trace_back,omitempty"` + MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"` + OOMKiller *bool `json:"oom_killer,omitempty"` +} diff --git a/option/direct.go b/option/direct.go new file mode 100644 index 00000000..a03f98d4 --- /dev/null +++ b/option/direct.go @@ -0,0 +1,39 @@ +package option + +import ( + "context" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" +) + +type DirectInboundOptions struct { + ListenOptions + Network NetworkList `json:"network,omitempty"` + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` +} + +type _DirectOutboundOptions struct { + DialerOptions + // Deprecated: Use Route Action instead + OverrideAddress string `json:"override_address,omitempty"` + // Deprecated: Use Route Action instead + OverridePort uint16 `json:"override_port,omitempty"` + // Deprecated: removed + ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` +} + +type DirectOutboundOptions _DirectOutboundOptions + +func (d *DirectOutboundOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalDisallowUnknownFields(content, (*_DirectOutboundOptions)(d)) + if err != nil { + return err + } + //nolint:staticcheck + if d.OverrideAddress != "" || d.OverridePort != 0 { + return E.New("destination override fields in direct outbound are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use route options instead") + } + return nil +} diff --git a/option/dns.go b/option/dns.go new file mode 100644 index 00000000..c09b3d5f --- /dev/null +++ b/option/dns.go @@ -0,0 +1,184 @@ +package option + +import ( + "context" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/service" +) + +type RawDNSOptions struct { + Servers []DNSServerOptions `json:"servers,omitempty"` + Rules []DNSRule `json:"rules,omitempty"` + Final string `json:"final,omitempty"` + ReverseMapping bool `json:"reverse_mapping,omitempty"` + DNSClientOptions +} + +type DNSOptions struct { + RawDNSOptions +} + +const ( + legacyDNSFakeIPRemovedMessage = "legacy DNS fakeip options are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats" + legacyDNSServerRemovedMessage = "legacy DNS server formats are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats" +) + +type removedLegacyDNSOptions struct { + FakeIP json.RawMessage `json:"fakeip,omitempty"` +} + +func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { + var legacyOptions removedLegacyDNSOptions + err := json.UnmarshalContext(ctx, content, &legacyOptions) + if err != nil { + return err + } + if len(legacyOptions.FakeIP) != 0 { + return E.New(legacyDNSFakeIPRemovedMessage) + } + return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) +} + +type DNSClientOptions struct { + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableExpire bool `json:"disable_expire,omitempty"` + IndependentCache bool `json:"independent_cache,omitempty"` + CacheCapacity uint32 `json:"cache_capacity,omitempty"` + Optimistic *OptimisticDNSOptions `json:"optimistic,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` +} + +type _OptimisticDNSOptions struct { + Enabled bool `json:"enabled,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` +} + +type OptimisticDNSOptions _OptimisticDNSOptions + +func (o OptimisticDNSOptions) MarshalJSON() ([]byte, error) { + if o.Timeout == 0 { + return json.Marshal(o.Enabled) + } + return json.Marshal((_OptimisticDNSOptions)(o)) +} + +func (o *OptimisticDNSOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, &o.Enabled) + if err == nil { + return nil + } + return json.UnmarshalDisallowUnknownFields(bytes, (*_OptimisticDNSOptions)(o)) +} + +type DNSTransportOptionsRegistry interface { + CreateOptions(transportType string) (any, bool) +} +type _DNSServerOptions struct { + Type string `json:"type,omitempty"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type DNSServerOptions _DNSServerOptions + +func (o *DNSServerOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_DNSServerOptions)(o), o.Options) +} + +func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_DNSServerOptions)(o)) + if err != nil { + return err + } + registry := service.FromContext[DNSTransportOptionsRegistry](ctx) + if registry == nil { + return E.New("missing DNS transport options registry in context") + } + var options any + switch o.Type { + case "", C.DNSTypeLegacy: + return E.New(legacyDNSServerRemovedMessage) + default: + var loaded bool + options, loaded = registry.CreateOptions(o.Type) + if !loaded { + return E.New("unknown transport type: ", o.Type) + } + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_DNSServerOptions)(o), options) + if err != nil { + return err + } + o.Options = options + return nil +} + +type DNSServerAddressOptions struct { + Server string `json:"server"` + ServerPort uint16 `json:"server_port,omitempty"` +} + +func (o DNSServerAddressOptions) Build() M.Socksaddr { + return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) +} + +func (o DNSServerAddressOptions) ServerIsDomain() bool { + return o.Build().IsDomain() +} + +func (o *DNSServerAddressOptions) TakeServerOptions() ServerOptions { + return ServerOptions(*o) +} + +func (o *DNSServerAddressOptions) ReplaceServerOptions(options ServerOptions) { + *o = DNSServerAddressOptions(options) +} + +type HostsDNSServerOptions struct { + Path badoption.Listable[string] `json:"path,omitempty"` + Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"` +} + +type RawLocalDNSServerOptions struct { + DialerOptions +} + +type LocalDNSServerOptions struct { + RawLocalDNSServerOptions + PreferGo bool `json:"prefer_go,omitempty"` +} + +type RemoteDNSServerOptions struct { + RawLocalDNSServerOptions + DNSServerAddressOptions +} + +type RemoteTLSDNSServerOptions struct { + RemoteDNSServerOptions + OutboundTLSOptionsContainer +} + +type RemoteHTTPSDNSServerOptions struct { + RemoteTLSDNSServerOptions + Path string `json:"path,omitempty"` + Method string `json:"method,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` +} + +type FakeIPDNSServerOptions struct { + Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"` + Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"` +} + +type DHCPDNSServerOptions struct { + LocalDNSServerOptions + Interface string `json:"interface,omitempty"` +} diff --git a/option/dns_record.go b/option/dns_record.go new file mode 100644 index 00000000..f10e03d9 --- /dev/null +++ b/option/dns_record.go @@ -0,0 +1,125 @@ +package option + +import ( + "encoding/base64" + "strings" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + M "github.com/sagernet/sing/common/metadata" + + "github.com/miekg/dns" +) + +const defaultDNSRecordTTL uint32 = 3600 + +type DNSRCode int + +func (r DNSRCode) MarshalJSON() ([]byte, error) { + rCodeValue, loaded := dns.RcodeToString[int(r)] + if loaded { + return json.Marshal(rCodeValue) + } + return json.Marshal(int(r)) +} + +func (r *DNSRCode) UnmarshalJSON(bytes []byte) error { + var intValue int + err := json.Unmarshal(bytes, &intValue) + if err == nil { + *r = DNSRCode(intValue) + return nil + } + var stringValue string + err = json.Unmarshal(bytes, &stringValue) + if err != nil { + return err + } + rCodeValue, loaded := dns.StringToRcode[stringValue] + if !loaded { + return E.New("unknown rcode: " + stringValue) + } + *r = DNSRCode(rCodeValue) + return nil +} + +func (r *DNSRCode) Build() int { + if r == nil { + return dns.RcodeSuccess + } + return int(*r) +} + +type DNSRecordOptions struct { + dns.RR + fromBase64 bool +} + +func (o DNSRecordOptions) MarshalJSON() ([]byte, error) { + if o.fromBase64 { + buffer := buf.Get(dns.Len(o.RR)) + defer buf.Put(buffer) + offset, err := dns.PackRR(o.RR, buffer, 0, nil, false) + if err != nil { + return nil, err + } + return json.Marshal(base64.StdEncoding.EncodeToString(buffer[:offset])) + } + return json.Marshal(o.RR.String()) +} + +func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error { + var stringValue string + err := json.Unmarshal(data, &stringValue) + if err != nil { + return err + } + binary, err := base64.StdEncoding.DecodeString(stringValue) + if err == nil { + return o.unmarshalBase64(binary) + } + record, err := parseDNSRecord(stringValue) + if err != nil { + return err + } + if record == nil { + return E.New("empty DNS record") + } + if a, isA := record.(*dns.A); isA { + a.A = M.AddrFromIP(a.A).Unmap().AsSlice() + } + o.RR = record + return nil +} + +func parseDNSRecord(stringValue string) (dns.RR, error) { + if len(stringValue) > 0 && stringValue[len(stringValue)-1] != '\n' { + stringValue += "\n" + } + parser := dns.NewZoneParser(strings.NewReader(stringValue), "", "") + parser.SetDefaultTTL(defaultDNSRecordTTL) + record, _ := parser.Next() + return record, parser.Err() +} + +func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error { + record, _, err := dns.UnpackRR(binary, 0) + if err != nil { + return E.New("parse binary DNS record") + } + o.RR = record + o.fromBase64 = true + return nil +} + +func (o DNSRecordOptions) Build() dns.RR { + return o.RR +} + +func (o DNSRecordOptions) Match(record dns.RR) bool { + if o.RR == nil || record == nil { + return false + } + return dns.IsDuplicate(o.RR, record) +} diff --git a/option/dns_record_test.go b/option/dns_record_test.go new file mode 100644 index 00000000..759ef5fc --- /dev/null +++ b/option/dns_record_test.go @@ -0,0 +1,40 @@ +package option + +import ( + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func mustRecordOptions(t *testing.T, record string) DNSRecordOptions { + t.Helper() + var value DNSRecordOptions + require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`))) + return value +} + +func TestDNSRecordOptionsUnmarshalJSONRejectsRelativeNames(t *testing.T) { + t.Parallel() + + for _, record := range []string{ + "@ IN A 1.1.1.1", + "www IN CNAME example.com.", + "example.com. IN CNAME @", + "example.com. IN CNAME www", + } { + var value DNSRecordOptions + err := value.UnmarshalJSON([]byte(`"` + record + `"`)) + require.Error(t, err) + } +} + +func TestDNSRecordOptionsMatchIgnoresTTL(t *testing.T) { + t.Parallel() + + expected := mustRecordOptions(t, "example.com. 600 IN A 1.1.1.1") + record, err := dns.NewRR("example.com. 60 IN A 1.1.1.1") + require.NoError(t, err) + + require.True(t, expected.Match(record)) +} diff --git a/option/dns_test.go b/option/dns_test.go new file mode 100644 index 00000000..4e7bf9a9 --- /dev/null +++ b/option/dns_test.go @@ -0,0 +1,54 @@ +package option + +import ( + "context" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/service" + + "github.com/stretchr/testify/require" +) + +type stubDNSTransportOptionsRegistry struct{} + +func (stubDNSTransportOptionsRegistry) CreateOptions(transportType string) (any, bool) { + switch transportType { + case C.DNSTypeUDP: + return new(RemoteDNSServerOptions), true + case C.DNSTypeFakeIP: + return new(FakeIPDNSServerOptions), true + default: + return nil, false + } +} + +func TestDNSOptionsRejectsLegacyFakeIPOptions(t *testing.T) { + t.Parallel() + + ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{}) + var options DNSOptions + err := json.UnmarshalContext(ctx, []byte(`{ + "fakeip": { + "enabled": true, + "inet4_range": "198.18.0.0/15" + } + }`), &options) + require.EqualError(t, err, legacyDNSFakeIPRemovedMessage) +} + +func TestDNSServerOptionsRejectsLegacyFormats(t *testing.T) { + t.Parallel() + + ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{}) + testCases := []string{ + `{"address":"1.1.1.1"}`, + `{"type":"legacy","address":"1.1.1.1"}`, + } + for _, content := range testCases { + var options DNSServerOptions + err := json.UnmarshalContext(ctx, []byte(content), &options) + require.EqualError(t, err, legacyDNSServerRemovedMessage) + } +} diff --git a/option/endpoint.go b/option/endpoint.go new file mode 100644 index 00000000..45c4f831 --- /dev/null +++ b/option/endpoint.go @@ -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 EndpointOptionsRegistry interface { + CreateOptions(endpointType string) (any, bool) +} + +type _Endpoint struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type Endpoint _Endpoint + +func (h *Endpoint) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Endpoint)(h), h.Options) +} + +func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_Endpoint)(h)) + if err != nil { + return err + } + registry := service.FromContext[EndpointOptionsRegistry](ctx) + if registry == nil { + return E.New("missing endpoint fields registry in context") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown endpoint type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options) + if err != nil { + return err + } + h.Options = options + return nil +} diff --git a/option/experimental.go b/option/experimental.go new file mode 100644 index 00000000..2f00decf --- /dev/null +++ b/option/experimental.go @@ -0,0 +1,55 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type ExperimentalOptions struct { + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` + StoreRDRC bool `json:"store_rdrc,omitempty"` + RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"` + StoreDNS bool `json:"store_dns,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"` + AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` + + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` +} diff --git a/option/group.go b/option/group.go new file mode 100644 index 00000000..02b3a5ec --- /dev/null +++ b/option/group.go @@ -0,0 +1,18 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval badoption.Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/hysteria.go b/option/hysteria.go new file mode 100644 index 00000000..18675901 --- /dev/null +++ b/option/hysteria.go @@ -0,0 +1,46 @@ +package option + +import ( + "github.com/sagernet/sing/common/byteformats" + "github.com/sagernet/sing/common/json/badoption" +) + +type HysteriaInboundOptions struct { + ListenOptions + Up *byteformats.NetworkBytesCompat `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down *byteformats.NetworkBytesCompat `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Users []HysteriaUser `json:"users,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"` + MaxConnClient int `json:"max_conn_client,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + InboundTLSOptionsContainer +} + +type HysteriaUser struct { + Name string `json:"name,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` +} + +type HysteriaOutboundOptions struct { + DialerOptions + ServerOptions + ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` + HopInterval badoption.Duration `json:"hop_interval,omitempty"` + Up *byteformats.NetworkBytesCompat `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down *byteformats.NetworkBytesCompat `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindow uint64 `json:"recv_window,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/option/hysteria2.go b/option/hysteria2.go new file mode 100644 index 00000000..e31c8de3 --- /dev/null +++ b/option/hysteria2.go @@ -0,0 +1,127 @@ +package option + +import ( + "net/url" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type Hysteria2InboundOptions struct { + ListenOptions + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Users []Hysteria2User `json:"users,omitempty"` + IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` + InboundTLSOptionsContainer + Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` + BBRProfile string `json:"bbr_profile,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` +} + +type Hysteria2Obfs struct { + Type string `json:"type,omitempty"` + Password string `json:"password,omitempty"` +} + +type Hysteria2User struct { + Name string `json:"name,omitempty"` + Password string `json:"password,omitempty"` +} + +type _Hysteria2Masquerade struct { + Type string `json:"type,omitempty"` + FileOptions Hysteria2MasqueradeFile `json:"-"` + ProxyOptions Hysteria2MasqueradeProxy `json:"-"` + StringOptions Hysteria2MasqueradeString `json:"-"` +} + +type Hysteria2Masquerade _Hysteria2Masquerade + +func (m Hysteria2Masquerade) MarshalJSON() ([]byte, error) { + var v any + switch m.Type { + case C.Hysterai2MasqueradeTypeFile: + v = m.FileOptions + case C.Hysterai2MasqueradeTypeProxy: + v = m.ProxyOptions + case C.Hysterai2MasqueradeTypeString: + v = m.StringOptions + default: + return nil, E.New("unknown masquerade type: ", m.Type) + } + return badjson.MarshallObjects((_Hysteria2Masquerade)(m), v) +} + +func (m *Hysteria2Masquerade) UnmarshalJSON(bytes []byte) error { + var urlString string + err := json.Unmarshal(bytes, &urlString) + if err == nil { + masqueradeURL, err := url.Parse(urlString) + if err != nil { + return E.Cause(err, "invalid masquerade URL") + } + switch masqueradeURL.Scheme { + case "file": + m.Type = C.Hysterai2MasqueradeTypeFile + m.FileOptions.Directory = masqueradeURL.Path + case "http", "https": + m.Type = C.Hysterai2MasqueradeTypeProxy + m.ProxyOptions.URL = urlString + default: + return E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) + } + return nil + } + err = json.Unmarshal(bytes, (*_Hysteria2Masquerade)(m)) + if err != nil { + return err + } + var v any + switch m.Type { + case C.Hysterai2MasqueradeTypeFile: + v = &m.FileOptions + case C.Hysterai2MasqueradeTypeProxy: + v = &m.ProxyOptions + case C.Hysterai2MasqueradeTypeString: + v = &m.StringOptions + default: + return E.New("unknown masquerade type: ", m.Type) + } + return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Masquerade)(m), v) +} + +type Hysteria2MasqueradeFile struct { + Directory string `json:"directory"` +} + +type Hysteria2MasqueradeProxy struct { + URL string `json:"url"` + RewriteHost bool `json:"rewrite_host,omitempty"` +} + +type Hysteria2MasqueradeString struct { + StatusCode int `json:"status_code,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + Content string `json:"content"` +} + +type Hysteria2OutboundOptions struct { + DialerOptions + ServerOptions + ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` + HopInterval badoption.Duration `json:"hop_interval,omitempty"` + HopIntervalMax badoption.Duration `json:"hop_interval_max,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + BBRProfile string `json:"bbr_profile,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` +} diff --git a/option/inbound.go b/option/inbound.go new file mode 100644 index 00000000..21497a3f --- /dev/null +++ b/option/inbound.go @@ -0,0 +1,117 @@ +package option + +import ( + "context" + "time" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/service" +) + +type InboundOptionsRegistry interface { + CreateOptions(outboundType string) (any, bool) +} + +type _Inbound struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type Inbound _Inbound + +func (h *Inbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Inbound)(h), h.Options) +} + +func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_Inbound)(h)) + if err != nil { + return err + } + registry := service.FromContext[InboundOptionsRegistry](ctx) + if registry == nil { + return E.New("missing inbound 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, (*_Inbound)(h), options) + if err != nil { + return err + } + if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen { + //nolint:staticcheck + if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) { + return E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions") + } + } + h.Options = options + return nil +} + +// Deprecated: Use rule action instead +type InboundOptions struct { + SniffEnabled bool `json:"sniff,omitempty"` + SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` + SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` +} + +type ListenOptions struct { + Listen *badoption.Addr `json:"listen,omitempty"` + ListenPort uint16 `json:"listen_port,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + RoutingMark FwMark `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + NetNs string `json:"netns,omitempty"` + DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` + TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` + TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Detour string `json:"detour,omitempty"` + + // Deprecated: removed + ProxyProtocol bool `json:"proxy_protocol,omitempty"` + // Deprecated: removed + ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"` + InboundOptions +} + +type UDPTimeoutCompat badoption.Duration + +func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) { + return json.Marshal((time.Duration)(c).String()) +} + +func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error { + var valueNumber int64 + err := json.Unmarshal(data, &valueNumber) + if err == nil { + *c = UDPTimeoutCompat(time.Second * time.Duration(valueNumber)) + return nil + } + return json.Unmarshal(data, (*badoption.Duration)(c)) +} + +type ListenOptionsWrapper interface { + TakeListenOptions() ListenOptions + ReplaceListenOptions(options ListenOptions) +} + +func (o *ListenOptions) TakeListenOptions() ListenOptions { + return *o +} + +func (o *ListenOptions) ReplaceListenOptions(options ListenOptions) { + *o = options +} diff --git a/option/multiplex.go b/option/multiplex.go new file mode 100644 index 00000000..309d8bdc --- /dev/null +++ b/option/multiplex.go @@ -0,0 +1,23 @@ +package option + +type InboundMultiplexOptions struct { + Enabled bool `json:"enabled,omitempty"` + Padding bool `json:"padding,omitempty"` + Brutal *BrutalOptions `json:"brutal,omitempty"` +} + +type OutboundMultiplexOptions struct { + Enabled bool `json:"enabled,omitempty"` + Protocol string `json:"protocol,omitempty"` + MaxConnections int `json:"max_connections,omitempty"` + MinStreams int `json:"min_streams,omitempty"` + MaxStreams int `json:"max_streams,omitempty"` + Padding bool `json:"padding,omitempty"` + Brutal *BrutalOptions `json:"brutal,omitempty"` +} + +type BrutalOptions struct { + Enabled bool `json:"enabled,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` +} diff --git a/option/naive.go b/option/naive.go new file mode 100644 index 00000000..da3a88db --- /dev/null +++ b/option/naive.go @@ -0,0 +1,40 @@ +package option + +import ( + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/byteformats" + "github.com/sagernet/sing/common/json/badoption" +) + +type QuicheCongestionControl string + +const ( + QuicheCongestionControlDefault QuicheCongestionControl = "" + QuicheCongestionControlBBR QuicheCongestionControl = "TBBR" + QuicheCongestionControlBBRv2 QuicheCongestionControl = "B2ON" + QuicheCongestionControlCubic QuicheCongestionControl = "QBIC" + QuicheCongestionControlReno QuicheCongestionControl = "RENO" +) + +type NaiveInboundOptions struct { + ListenOptions + Users []auth.User `json:"users,omitempty"` + Network NetworkList `json:"network,omitempty"` + QUICCongestionControl string `json:"quic_congestion_control,omitempty"` + InboundTLSOptionsContainer +} + +type NaiveOutboundOptions struct { + DialerOptions + ServerOptions + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + InsecureConcurrency int `json:"insecure_concurrency,omitempty"` + ExtraHeaders badoption.HTTPHeader `json:"extra_headers,omitempty"` + ReceiveWindow *byteformats.MemoryBytes `json:"stream_receive_window,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` + QUIC bool `json:"quic,omitempty"` + QUICCongestionControl string `json:"quic_congestion_control,omitempty"` + QUICSessionReceiveWindow *byteformats.MemoryBytes `json:"quic_session_receive_window,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/option/ntp.go b/option/ntp.go new file mode 100644 index 00000000..d441d95e --- /dev/null +++ b/option/ntp.go @@ -0,0 +1,11 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type NTPOptions struct { + Enabled bool `json:"enabled,omitempty"` + Interval badoption.Duration `json:"interval,omitempty"` + WriteToSystem bool `json:"write_to_system,omitempty"` + ServerOptions + DialerOptions +} diff --git a/option/ocm.go b/option/ocm.go new file mode 100644 index 00000000..c13a1c1f --- /dev/null +++ b/option/ocm.go @@ -0,0 +1,20 @@ +package option + +import ( + "github.com/sagernet/sing/common/json/badoption" +) + +type OCMServiceOptions struct { + ListenOptions + InboundTLSOptionsContainer + CredentialPath string `json:"credential_path,omitempty"` + Users []OCMUser `json:"users,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + Detour string `json:"detour,omitempty"` + UsagesPath string `json:"usages_path,omitempty"` +} + +type OCMUser struct { + Name string `json:"name,omitempty"` + Token string `json:"token,omitempty"` +} diff --git a/option/oom_killer.go b/option/oom_killer.go new file mode 100644 index 00000000..1183b502 --- /dev/null +++ b/option/oom_killer.go @@ -0,0 +1,15 @@ +package option + +import ( + "github.com/sagernet/sing/common/byteformats" + "github.com/sagernet/sing/common/json/badoption" +) + +type OOMKillerServiceOptions struct { + MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"` + SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"` + MinInterval badoption.Duration `json:"min_interval,omitempty"` + MaxInterval badoption.Duration `json:"max_interval,omitempty"` + KillerDisabled bool `json:"-"` + MemoryLimitOverride uint64 `json:"-"` +} diff --git a/option/options.go b/option/options.go new file mode 100644 index 00000000..a08dcbc0 --- /dev/null +++ b/option/options.go @@ -0,0 +1,120 @@ +package option + +import ( + "bytes" + "context" + + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" +) + +type _Options struct { + RawMessage json.RawMessage `json:"-"` + 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"` + CertificateProviders []CertificateProvider `json:"certificate_providers,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"` +} + +type Options _Options + +func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) error { + decoder := json.NewDecoderContext(ctx, bytes.NewReader(content)) + decoder.DisallowUnknownFields() + err := decoder.Decode((*_Options)(o)) + if err != nil { + return err + } + o.RawMessage = content + return checkOptions(o) +} + +type LogOptions struct { + Disabled bool `json:"disabled,omitempty"` + Level string `json:"level,omitempty"` + Output string `json:"output,omitempty"` + Timestamp bool `json:"timestamp,omitempty"` + DisableColor bool `json:"-"` +} + +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 + } + err = checkCertificateProviders(options.CertificateProviders) + if err != nil { + return err + } + return nil +} + +func checkCertificateProviders(providers []CertificateProvider) error { + seen := make(map[string]bool) + for i, provider := range providers { + tag := provider.Tag + if tag == "" { + tag = F.ToString(i) + } + if seen[tag] { + return E.New("duplicate certificate provider tag: ", tag) + } + seen[tag] = true + } + return nil +} + +func checkInbounds(inbounds []Inbound) error { + seen := make(map[string]bool) + for i, inbound := range inbounds { + tag := inbound.Tag + if tag == "" { + tag = F.ToString(i) + } + if seen[tag] { + return E.New("duplicate inbound tag: ", tag) + } + seen[tag] = true + } + return nil +} + +func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error { + seen := make(map[string]bool) + for i, outbound := range outbounds { + tag := outbound.Tag + if tag == "" { + tag = F.ToString(i) + } + if seen[tag] { + return E.New("duplicate outbound/endpoint tag: ", tag) + } + seen[tag] = true + } + for i, endpoint := range endpoints { + tag := endpoint.Tag + if tag == "" { + tag = F.ToString(i) + } + if seen[tag] { + return E.New("duplicate outbound/endpoint tag: ", tag) + } + seen[tag] = true + } + return nil +} diff --git a/option/origin_ca.go b/option/origin_ca.go new file mode 100644 index 00000000..ee8b3704 --- /dev/null +++ b/option/origin_ca.go @@ -0,0 +1,76 @@ +package option + +import ( + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" +) + +type CloudflareOriginCACertificateProviderOptions struct { + Domain badoption.Listable[string] `json:"domain,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + APIToken string `json:"api_token,omitempty"` + OriginCAKey string `json:"origin_ca_key,omitempty"` + RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"` + RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"` + Detour string `json:"detour,omitempty"` +} + +type CloudflareOriginCARequestType string + +const ( + CloudflareOriginCARequestTypeOriginRSA = CloudflareOriginCARequestType("origin-rsa") + CloudflareOriginCARequestTypeOriginECC = CloudflareOriginCARequestType("origin-ecc") +) + +func (t *CloudflareOriginCARequestType) UnmarshalJSON(data []byte) error { + var value string + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + value = strings.ToLower(value) + switch CloudflareOriginCARequestType(value) { + case "", CloudflareOriginCARequestTypeOriginRSA, CloudflareOriginCARequestTypeOriginECC: + *t = CloudflareOriginCARequestType(value) + default: + return E.New("unsupported Cloudflare Origin CA request type: ", value) + } + return nil +} + +type CloudflareOriginCARequestValidity uint16 + +const ( + CloudflareOriginCARequestValidity7 = CloudflareOriginCARequestValidity(7) + CloudflareOriginCARequestValidity30 = CloudflareOriginCARequestValidity(30) + CloudflareOriginCARequestValidity90 = CloudflareOriginCARequestValidity(90) + CloudflareOriginCARequestValidity365 = CloudflareOriginCARequestValidity(365) + CloudflareOriginCARequestValidity730 = CloudflareOriginCARequestValidity(730) + CloudflareOriginCARequestValidity1095 = CloudflareOriginCARequestValidity(1095) + CloudflareOriginCARequestValidity5475 = CloudflareOriginCARequestValidity(5475) +) + +func (v *CloudflareOriginCARequestValidity) UnmarshalJSON(data []byte) error { + var value uint16 + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + switch CloudflareOriginCARequestValidity(value) { + case 0, + CloudflareOriginCARequestValidity7, + CloudflareOriginCARequestValidity30, + CloudflareOriginCARequestValidity90, + CloudflareOriginCARequestValidity365, + CloudflareOriginCARequestValidity730, + CloudflareOriginCARequestValidity1095, + CloudflareOriginCARequestValidity5475: + *v = CloudflareOriginCARequestValidity(value) + default: + return E.New("unsupported Cloudflare Origin CA requested validity: ", value) + } + return nil +} diff --git a/option/outbound.go b/option/outbound.go new file mode 100644 index 00000000..d8fcb822 --- /dev/null +++ b/option/outbound.go @@ -0,0 +1,169 @@ +package option + +import ( + "context" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/service" +) + +type OutboundOptionsRegistry interface { + CreateOptions(outboundType string) (any, bool) +} + +type _Outbound struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type Outbound _Outbound + +func (h *Outbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Outbound)(h), h.Options) +} + +func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_Outbound)(h)) + if err != nil { + return err + } + registry := service.FromContext[OutboundOptionsRegistry](ctx) + if registry == nil { + return E.New("missing outbound options registry in context") + } + switch h.Type { + case C.TypeDNS: + return E.New("dns outbound is deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use rule actions instead") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown outbound type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_Outbound)(h), options) + if err != nil { + return err + } + if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen { + //nolint:staticcheck + if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) { + return E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use rule actions instead") + } + } + h.Options = options + return nil +} + +type DialerOptionsWrapper interface { + TakeDialerOptions() DialerOptions + ReplaceDialerOptions(options DialerOptions) +} + +type DialerOptions struct { + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` + Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` + BindAddressNoPort bool `json:"bind_address_no_port,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark FwMark `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + NetNs string `json:"netns,omitempty"` + ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` + TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` + TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` + NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"` + FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` + + // Deprecated: migrated to domain resolver + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` +} + +type _DomainResolveOptions struct { + Server string `json:"server"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` +} + +type DomainResolveOptions _DomainResolveOptions + +func (o DomainResolveOptions) MarshalJSON() ([]byte, error) { + if o.Server == "" { + return []byte("{}"), nil + } else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) && + !o.DisableCache && + !o.DisableOptimisticCache && + o.RewriteTTL == nil && + o.ClientSubnet == nil { + return json.Marshal(o.Server) + } else { + return json.Marshal((_DomainResolveOptions)(o)) + } +} + +func (o *DomainResolveOptions) UnmarshalJSON(bytes []byte) error { + var stringValue string + err := json.Unmarshal(bytes, &stringValue) + if err == nil { + o.Server = stringValue + return nil + } + err = json.Unmarshal(bytes, (*_DomainResolveOptions)(o)) + if err != nil { + return err + } + if o.Server == "" { + return E.New("empty domain_resolver.server") + } + return nil +} + +func (o *DialerOptions) TakeDialerOptions() DialerOptions { + return *o +} + +func (o *DialerOptions) ReplaceDialerOptions(options DialerOptions) { + *o = options +} + +type ServerOptionsWrapper interface { + TakeServerOptions() ServerOptions + ReplaceServerOptions(options ServerOptions) +} + +type ServerOptions struct { + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` +} + +func (o ServerOptions) Build() M.Socksaddr { + return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) +} + +func (o ServerOptions) ServerIsDomain() bool { + return o.Build().IsDomain() +} + +func (o *ServerOptions) TakeServerOptions() ServerOptions { + return *o +} + +func (o *ServerOptions) ReplaceServerOptions(options ServerOptions) { + *o = options +} diff --git a/option/platform.go b/option/platform.go new file mode 100644 index 00000000..e4ecd6fa --- /dev/null +++ b/option/platform.go @@ -0,0 +1,105 @@ +package option + +import ( + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" +) + +type OnDemandOptions struct { + Enabled bool `json:"enabled,omitempty"` + Rules []OnDemandRule `json:"rules,omitempty"` +} + +type OnDemandRule struct { + Action *OnDemandRuleAction `json:"action,omitempty"` + DNSSearchDomainMatch badoption.Listable[string] `json:"dns_search_domain_match,omitempty"` + DNSServerAddressMatch badoption.Listable[string] `json:"dns_server_address_match,omitempty"` + InterfaceTypeMatch *OnDemandRuleInterfaceType `json:"interface_type_match,omitempty"` + SSIDMatch badoption.Listable[string] `json:"ssid_match,omitempty"` + ProbeURL string `json:"probe_url,omitempty"` +} + +type OnDemandRuleAction int + +func (r *OnDemandRuleAction) MarshalJSON() ([]byte, error) { + if r == nil { + return nil, nil + } + value := *r + var actionName string + switch value { + case 1: + actionName = "connect" + case 2: + actionName = "disconnect" + case 3: + actionName = "evaluate_connection" + default: + return nil, E.New("unknown action: ", value) + } + return json.Marshal(actionName) +} + +func (r *OnDemandRuleAction) UnmarshalJSON(bytes []byte) error { + var actionName string + if err := json.Unmarshal(bytes, &actionName); err != nil { + return err + } + var actionValue int + switch actionName { + case "connect": + actionValue = 1 + case "disconnect": + actionValue = 2 + case "evaluate_connection": + actionValue = 3 + case "ignore": + actionValue = 4 + default: + return E.New("unknown action name: ", actionName) + } + *r = OnDemandRuleAction(actionValue) + return nil +} + +type OnDemandRuleInterfaceType int + +func (r *OnDemandRuleInterfaceType) MarshalJSON() ([]byte, error) { + if r == nil { + return nil, nil + } + value := *r + var interfaceTypeName string + switch value { + case 1: + interfaceTypeName = "any" + case 2: + interfaceTypeName = "wifi" + case 3: + interfaceTypeName = "cellular" + default: + return nil, E.New("unknown interface type: ", value) + } + return json.Marshal(interfaceTypeName) +} + +func (r *OnDemandRuleInterfaceType) UnmarshalJSON(bytes []byte) error { + var interfaceTypeName string + if err := json.Unmarshal(bytes, &interfaceTypeName); err != nil { + return err + } + var interfaceTypeValue int + switch interfaceTypeName { + case "any": + interfaceTypeValue = 1 + case "wifi": + interfaceTypeValue = 2 + case "cellular": + interfaceTypeValue = 3 + default: + return E.New("unknown interface type name: ", interfaceTypeName) + } + *r = OnDemandRuleInterfaceType(interfaceTypeValue) + return nil +} diff --git a/option/redir.go b/option/redir.go new file mode 100644 index 00000000..743a6e10 --- /dev/null +++ b/option/redir.go @@ -0,0 +1,10 @@ +package option + +type RedirectInboundOptions struct { + ListenOptions +} + +type TProxyInboundOptions struct { + ListenOptions + Network NetworkList `json:"network,omitempty"` +} diff --git a/option/resolved.go b/option/resolved.go new file mode 100644 index 00000000..cb9f579d --- /dev/null +++ b/option/resolved.go @@ -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"` +} diff --git a/option/route.go b/option/route.go new file mode 100644 index 00000000..0c3e576d --- /dev/null +++ b/option/route.go @@ -0,0 +1,35 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type RouteOptions struct { + GeoIP *GeoIPOptions `json:"geoip,omitempty"` + Geosite *GeositeOptions `json:"geosite,omitempty"` + Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` + Final string `json:"final,omitempty"` + FindProcess bool `json:"find_process,omitempty"` + FindNeighbor bool `json:"find_neighbor,omitempty"` + DHCPLeaseFiles badoption.Listable[string] `json:"dhcp_lease_files,omitempty"` + AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` + OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` + DefaultInterface string `json:"default_interface,omitempty"` + DefaultMark FwMark `json:"default_mark,omitempty"` + DefaultDomainResolver *DomainResolveOptions `json:"default_domain_resolver,omitempty"` + DefaultNetworkStrategy *NetworkStrategy `json:"default_network_strategy,omitempty"` + DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"` + DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"` + DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"` +} + +type GeoIPOptions struct { + Path string `json:"path,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + DownloadDetour string `json:"download_detour,omitempty"` +} + +type GeositeOptions struct { + Path string `json:"path,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + DownloadDetour string `json:"download_detour,omitempty"` +} diff --git a/option/rule.go b/option/rule.go new file mode 100644 index 00000000..5759cf56 --- /dev/null +++ b/option/rule.go @@ -0,0 +1,223 @@ +package option + +import ( + "context" + "reflect" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type _Rule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultRule `json:"-"` + LogicalOptions LogicalRule `json:"-"` +} + +type Rule _Rule + +func (r Rule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return badjson.MarshallObjects((_Rule)(r), v) +} + +func (r *Rule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { + err := json.UnmarshalContext(ctx, bytes, (*_Rule)(r)) + if err != nil { + return err + } + payload, err := rulePayloadWithoutType(ctx, bytes) + if err != nil { + return err + } + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions) + case C.RuleTypeLogical: + return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions) + default: + return E.New("unknown rule type: " + r.Type) + } +} + +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type RawDefaultRule struct { + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Client badoption.Listable[string] `json:"client,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"` + NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` + DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` + SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` + SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` + PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + + // Deprecated: renamed to rule_set_ip_cidr_match_source + Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` +} + +type DefaultRule struct { + RawDefaultRule + RuleAction +} + +func (r DefaultRule) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(r.RawDefaultRule, r.RuleAction) +} + +func (r *DefaultRule) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &r.RawDefaultRule) + if err != nil { + return err + } + return badjson.UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction) +} + +func (r DefaultRule) IsValid() bool { + var defaultValue DefaultRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type RawLogicalRule struct { + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +type LogicalRule struct { + RawLogicalRule + RuleAction +} + +func (r LogicalRule) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(r.RawLogicalRule, r.RuleAction) +} + +func (r *LogicalRule) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &r.RawLogicalRule) + if err != nil { + return err + } + return badjson.UnmarshallExcluded(data, &r.RawLogicalRule, &r.RuleAction) +} + +func rulePayloadWithoutType(ctx context.Context, data []byte) ([]byte, error) { + var content badjson.JSONObject + err := content.UnmarshalJSONContext(ctx, data) + if err != nil { + return nil, err + } + content.Remove("type") + return content.MarshalJSONContext(ctx) +} + +func unmarshalDefaultRuleContext(ctx context.Context, data []byte, rule *DefaultRule) error { + rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedRouteRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(ctx, data, &rule.RawDefaultRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawDefaultRule, &rule.RuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) { + rule.RuleAction = RuleAction{} + } + return nil +} + +func unmarshalLogicalRuleContext(ctx context.Context, data []byte, rule *LogicalRule) error { + rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedRouteRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &rule.RawLogicalRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawLogicalRule, &rule.RuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) { + rule.RuleAction = RuleAction{} + } + return nil +} + +func (r *LogicalRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) +} diff --git a/option/rule_action.go b/option/rule_action.go new file mode 100644 index 00000000..c369cfeb --- /dev/null +++ b/option/rule_action.go @@ -0,0 +1,339 @@ +package option + +import ( + "context" + "fmt" + "net/netip" + "time" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type _RuleAction struct { + Action string `json:"action,omitempty"` + RouteOptions RouteActionOptions `json:"-"` + RouteOptionsOptions RouteOptionsActionOptions `json:"-"` + DirectOptions DirectActionOptions `json:"-"` + BypassOptions RouteActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` + SniffOptions RouteActionSniff `json:"-"` + ResolveOptions RouteActionResolve `json:"-"` +} + +type RuleAction _RuleAction + +func (r RuleAction) MarshalJSON() ([]byte, error) { + if r.Action == "" { + return json.Marshal(struct{}{}) + } + var v any + switch r.Action { + case C.RuleActionTypeRoute: + r.Action = "" + v = r.RouteOptions + case C.RuleActionTypeRouteOptions: + v = r.RouteOptionsOptions + case C.RuleActionTypeDirect: + v = r.DirectOptions + case C.RuleActionTypeBypass: + v = r.BypassOptions + case C.RuleActionTypeReject: + v = r.RejectOptions + case C.RuleActionTypeHijackDNS: + v = nil + case C.RuleActionTypeSniff: + v = r.SniffOptions + case C.RuleActionTypeResolve: + v = r.ResolveOptions + default: + return nil, E.New("unknown rule action: " + r.Action) + } + if v == nil { + return badjson.MarshallObjects((_RuleAction)(r)) + } + return badjson.MarshallObjects((_RuleAction)(r), v) +} + +func (r *RuleAction) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_RuleAction)(r)) + if err != nil { + return err + } + var v any + switch r.Action { + case "", C.RuleActionTypeRoute: + r.Action = C.RuleActionTypeRoute + v = &r.RouteOptions + case C.RuleActionTypeRouteOptions: + v = &r.RouteOptionsOptions + case C.RuleActionTypeDirect: + v = &r.DirectOptions + case C.RuleActionTypeBypass: + v = &r.BypassOptions + case C.RuleActionTypeReject: + v = &r.RejectOptions + case C.RuleActionTypeHijackDNS: + v = nil + case C.RuleActionTypeSniff: + v = &r.SniffOptions + case C.RuleActionTypeResolve: + v = &r.ResolveOptions + default: + return E.New("unknown rule action: " + r.Action) + } + if v == nil { + // check unknown fields + return json.UnmarshalDisallowUnknownFields(data, &_RuleAction{}) + } + err = badjson.UnmarshallExcluded(data, (*_RuleAction)(r), v) + if err != nil { + return err + } + return nil +} + +type _DNSRuleAction struct { + Action string `json:"action,omitempty"` + RouteOptions DNSRouteActionOptions `json:"-"` + RouteOptionsOptions DNSRouteOptionsActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` + PredefinedOptions DNSRouteActionPredefined `json:"-"` +} + +type DNSRuleAction _DNSRuleAction + +func (r DNSRuleAction) MarshalJSON() ([]byte, error) { + if r.Action == "" { + return json.Marshal(struct{}{}) + } + var v any + switch r.Action { + case C.RuleActionTypeRoute: + r.Action = "" + v = r.RouteOptions + case C.RuleActionTypeEvaluate: + v = r.RouteOptions + case C.RuleActionTypeRespond: + v = nil + case C.RuleActionTypeRouteOptions: + v = r.RouteOptionsOptions + case C.RuleActionTypeReject: + v = r.RejectOptions + case C.RuleActionTypePredefined: + v = r.PredefinedOptions + default: + return nil, E.New("unknown DNS rule action: " + r.Action) + } + if v == nil { + return badjson.MarshallObjects((_DNSRuleAction)(r)) + } + return badjson.MarshallObjects((_DNSRuleAction)(r), v) +} + +func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) error { + err := json.Unmarshal(data, (*_DNSRuleAction)(r)) + if err != nil { + return err + } + var v any + switch r.Action { + case "", C.RuleActionTypeRoute: + r.Action = C.RuleActionTypeRoute + v = &r.RouteOptions + case C.RuleActionTypeEvaluate: + v = &r.RouteOptions + case C.RuleActionTypeRespond: + v = nil + case C.RuleActionTypeRouteOptions: + v = &r.RouteOptionsOptions + case C.RuleActionTypeReject: + v = &r.RejectOptions + case C.RuleActionTypePredefined: + v = &r.PredefinedOptions + default: + return E.New("unknown DNS rule action: " + r.Action) + } + if v == nil { + return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{}) + } + return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v) +} + +type RouteActionOptions struct { + Outbound string `json:"outbound,omitempty"` + RawRouteOptionsActionOptions +} + +type RawRouteOptionsActionOptions struct { + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` + + NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` + FallbackDelay uint32 `json:"fallback_delay,omitempty"` + + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` + UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` + + TLSFragment bool `json:"tls_fragment,omitempty"` + TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"` + TLSRecordFragment bool `json:"tls_record_fragment,omitempty"` +} + +type RouteOptionsActionOptions RawRouteOptionsActionOptions + +func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*RawRouteOptionsActionOptions)(r)) + if err != nil { + return err + } + if *r == (RouteOptionsActionOptions{}) { + return E.New("empty route option action") + } + if r.TLSFragment && r.TLSRecordFragment { + return E.New("`tls_fragment` and `tls_record_fragment` are mutually exclusive") + } + return nil +} + +type DNSRouteActionOptions struct { + Server string `json:"server,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` +} + +type _DNSRouteOptionsActionOptions struct { + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` +} + +type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions + +func (r *DNSRouteOptionsActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_DNSRouteOptionsActionOptions)(r)) + if err != nil { + return err + } + if *r == (DNSRouteOptionsActionOptions{}) { + return E.New("empty DNS route option action") + } + return nil +} + +type _DirectActionOptions DialerOptions + +type DirectActionOptions _DirectActionOptions + +func (d DirectActionOptions) Descriptions() []string { + var descriptions []string + if d.BindInterface != "" { + descriptions = append(descriptions, "bind_interface="+d.BindInterface) + } + if d.Inet4BindAddress != nil { + descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build(netip.IPv4Unspecified()).String()) + } + if d.Inet6BindAddress != nil { + descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build(netip.IPv6Unspecified()).String()) + } + if d.RoutingMark != 0 { + descriptions = append(descriptions, "routing_mark="+fmt.Sprintf("0x%x", d.RoutingMark)) + } + if d.ReuseAddr { + descriptions = append(descriptions, "reuse_addr") + } + if d.ConnectTimeout != 0 { + descriptions = append(descriptions, "connect_timeout="+time.Duration(d.ConnectTimeout).String()) + } + if d.TCPFastOpen { + descriptions = append(descriptions, "tcp_fast_open") + } + if d.TCPMultiPath { + descriptions = append(descriptions, "tcp_multi_path") + } + if d.UDPFragment != nil { + descriptions = append(descriptions, "udp_fragment="+fmt.Sprint(*d.UDPFragment)) + } + if d.DomainStrategy != DomainStrategy(C.DomainStrategyAsIS) { + descriptions = append(descriptions, "domain_strategy="+d.DomainStrategy.String()) + } + if d.FallbackDelay != 0 { + descriptions = append(descriptions, "fallback_delay="+time.Duration(d.FallbackDelay).String()) + } + return descriptions +} + +func (d *DirectActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_DirectActionOptions)(d)) + if err != nil { + return err + } + if d.Detour != "" { + return E.New("detour is not available in the current context") + } + return nil +} + +type _RejectActionOptions struct { + Method string `json:"method,omitempty"` + NoDrop bool `json:"no_drop,omitempty"` +} + +type RejectActionOptions _RejectActionOptions + +func (r RejectActionOptions) MarshalJSON() ([]byte, error) { + switch r.Method { + case C.RuleActionRejectMethodDefault: + r.Method = "" + } + return json.Marshal((_RejectActionOptions)(r)) +} + +func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RejectActionOptions)(r)) + if err != nil { + return err + } + switch r.Method { + case "", C.RuleActionRejectMethodDefault: + r.Method = C.RuleActionRejectMethodDefault + case C.RuleActionRejectMethodDrop: + case C.RuleActionRejectMethodReply: + default: + return E.New("unknown reject method: " + r.Method) + } + if r.Method == C.RuleActionRejectMethodDrop && r.NoDrop { + return E.New("no_drop is not available in current context") + } + return nil +} + +type RouteActionSniff struct { + Sniffer badoption.Listable[string] `json:"sniffer,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` +} + +type RouteActionResolve struct { + Server string `json:"server,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` +} + +type DNSRouteActionPredefined struct { + Rcode *DNSRCode `json:"rcode,omitempty"` + Answer badoption.Listable[DNSRecordOptions] `json:"answer,omitempty"` + Ns badoption.Listable[DNSRecordOptions] `json:"ns,omitempty"` + Extra badoption.Listable[DNSRecordOptions] `json:"extra,omitempty"` +} diff --git a/option/rule_action_test.go b/option/rule_action_test.go new file mode 100644 index 00000000..0007cd36 --- /dev/null +++ b/option/rule_action_test.go @@ -0,0 +1,29 @@ +package option + +import ( + "context" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/json" + + "github.com/stretchr/testify/require" +) + +func TestDNSRuleActionRespondUnmarshalJSON(t *testing.T) { + t.Parallel() + + var action DNSRuleAction + err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond"}`), &action) + require.NoError(t, err) + require.Equal(t, C.RuleActionTypeRespond, action.Action) + require.Equal(t, DNSRouteActionOptions{}, action.RouteOptions) +} + +func TestDNSRuleActionRespondRejectsUnknownFields(t *testing.T) { + t.Parallel() + + var action DNSRuleAction + err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond","disable_cache":true}`), &action) + require.ErrorContains(t, err, "unknown field") +} diff --git a/option/rule_dns.go b/option/rule_dns.go new file mode 100644 index 00000000..74058a65 --- /dev/null +++ b/option/rule_dns.go @@ -0,0 +1,208 @@ +package option + +import ( + "context" + "reflect" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type _DNSRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultDNSRule `json:"-"` + LogicalOptions LogicalDNSRule `json:"-"` +} + +type DNSRule _DNSRule + +func (r DNSRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return badjson.MarshallObjects((_DNSRule)(r), v) +} + +func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { + err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, bytes, (*_DNSRule)(r), v) + if err != nil { + return err + } + return nil +} + +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + +type RawDefaultDNSRule struct { + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + Outbound badoption.Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"` + NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` + DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` + SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` + SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + MatchResponse bool `json:"match_response,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + IPAcceptAny bool `json:"ip_accept_any,omitempty"` + ResponseRcode *DNSRCode `json:"response_rcode,omitempty"` + ResponseAnswer badoption.Listable[DNSRecordOptions] `json:"response_answer,omitempty"` + ResponseNs badoption.Listable[DNSRecordOptions] `json:"response_ns,omitempty"` + ResponseExtra badoption.Listable[DNSRecordOptions] `json:"response_extra,omitempty"` + Invert bool `json:"invert,omitempty"` + + // Deprecated: removed in sing-box 1.12.0 + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + // Deprecated: removed in sing-box 1.11.0 + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` + // Deprecated: renamed to rule_set_ip_cidr_match_source + Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` +} + +type DefaultDNSRule struct { + RawDefaultDNSRule + DNSRuleAction +} + +func (r DefaultDNSRule) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) +} + +func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { + rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedDNSRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) { + r.DNSRuleAction = DNSRuleAction{} + } + return nil +} + +func (r DefaultDNSRule) IsValid() bool { + var defaultValue DefaultDNSRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type RawLogicalDNSRule struct { + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +type LogicalDNSRule struct { + RawLogicalDNSRule + DNSRuleAction +} + +func (r LogicalDNSRule) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(r.RawLogicalDNSRule, r.DNSRuleAction) +} + +func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { + rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedDNSRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &r.RawLogicalDNSRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) { + r.DNSRuleAction = DNSRuleAction{} + } + return nil +} + +func (r *LogicalDNSRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) +} diff --git a/option/rule_nested.go b/option/rule_nested.go new file mode 100644 index 00000000..17216572 --- /dev/null +++ b/option/rule_nested.go @@ -0,0 +1,133 @@ +package option + +import ( + "context" + "reflect" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" +) + +type nestedRuleDepthContextKey struct{} + +const ( + RouteRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules" + DNSRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules" +) + +var ( + routeRuleActionKeys = jsonFieldNames(reflect.TypeFor[_RuleAction](), reflect.TypeFor[RouteActionOptions]()) + dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions]()) +) + +func nestedRuleChildContext(ctx context.Context) context.Context { + return context.WithValue(ctx, nestedRuleDepthContextKey{}, nestedRuleDepth(ctx)+1) +} + +func rejectNestedRouteRuleAction(ctx context.Context, content []byte) error { + return rejectNestedRuleAction(ctx, content, routeRuleActionKeys, RouteRuleActionNestedUnsupportedMessage) +} + +func rejectNestedDNSRuleAction(ctx context.Context, content []byte) error { + return rejectNestedRuleAction(ctx, content, dnsRuleActionKeys, DNSRuleActionNestedUnsupportedMessage) +} + +func nestedRuleDepth(ctx context.Context) int { + depth, _ := ctx.Value(nestedRuleDepthContextKey{}).(int) + return depth +} + +func rejectNestedRuleAction(ctx context.Context, content []byte, keys []string, message string) error { + if nestedRuleDepth(ctx) == 0 { + return nil + } + hasActionKey, err := hasAnyJSONKey(ctx, content, keys...) + if err != nil { + return err + } + if hasActionKey { + return E.New(message) + } + return nil +} + +func hasAnyJSONKey(ctx context.Context, content []byte, keys ...string) (bool, error) { + var object badjson.JSONObject + err := object.UnmarshalJSONContext(ctx, content) + if err != nil { + return false, err + } + for _, key := range keys { + if object.ContainsKey(key) { + return true, nil + } + } + return false, nil +} + +func inspectRouteRuleAction(ctx context.Context, content []byte) (string, RouteActionOptions, error) { + var rawAction _RuleAction + err := json.UnmarshalContext(ctx, content, &rawAction) + if err != nil { + return "", RouteActionOptions{}, err + } + var routeOptions RouteActionOptions + err = json.UnmarshalContext(ctx, content, &routeOptions) + if err != nil { + return "", RouteActionOptions{}, err + } + return rawAction.Action, routeOptions, nil +} + +func inspectDNSRuleAction(ctx context.Context, content []byte) (string, DNSRouteActionOptions, error) { + var rawAction _DNSRuleAction + err := json.UnmarshalContext(ctx, content, &rawAction) + if err != nil { + return "", DNSRouteActionOptions{}, err + } + var routeOptions DNSRouteActionOptions + err = json.UnmarshalContext(ctx, content, &routeOptions) + if err != nil { + return "", DNSRouteActionOptions{}, err + } + return rawAction.Action, routeOptions, nil +} + +func jsonFieldNames(types ...reflect.Type) []string { + fieldMap := make(map[string]struct{}) + for _, fieldType := range types { + appendJSONFieldNames(fieldMap, fieldType) + } + fieldNames := make([]string, 0, len(fieldMap)) + for fieldName := range fieldMap { + fieldNames = append(fieldNames, fieldName) + } + return fieldNames +} + +func appendJSONFieldNames(fieldMap map[string]struct{}, fieldType reflect.Type) { + for fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + } + if fieldType.Kind() != reflect.Struct { + return + } + for i := range fieldType.NumField() { + field := fieldType.Field(i) + tagValue := field.Tag.Get("json") + tagName, _, _ := strings.Cut(tagValue, ",") + if tagName == "-" { + continue + } + if field.Anonymous && tagName == "" { + appendJSONFieldNames(fieldMap, field.Type) + continue + } + if tagName == "" { + tagName = field.Name + } + fieldMap[tagName] = struct{}{} + } +} diff --git a/option/rule_nested_test.go b/option/rule_nested_test.go new file mode 100644 index 00000000..3b2ef2e5 --- /dev/null +++ b/option/rule_nested_test.go @@ -0,0 +1,68 @@ +package option + +import ( + "context" + "testing" + + "github.com/sagernet/sing/common/json" + + "github.com/stretchr/testify/require" +) + +func TestRuleRejectsNestedDefaultRuleAction(t *testing.T) { + t.Parallel() + + var rule Rule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "outbound": "direct"} + ] + }`), &rule) + require.ErrorContains(t, err, RouteRuleActionNestedUnsupportedMessage) +} + +func TestRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) { + t.Parallel() + + var rule Rule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "foo": "bar"} + ] + }`), &rule) + require.ErrorContains(t, err, "unknown field") + require.NotContains(t, err.Error(), RouteRuleActionNestedUnsupportedMessage) +} + +func TestDNSRuleRejectsNestedDefaultRuleAction(t *testing.T) { + t.Parallel() + + var rule DNSRule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "server": "default"} + ] + }`), &rule) + require.ErrorContains(t, err, DNSRuleActionNestedUnsupportedMessage) +} + +func TestDNSRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) { + t.Parallel() + + var rule DNSRule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "foo": "bar"} + ] + }`), &rule) + require.ErrorContains(t, err, "unknown field") + require.NotContains(t, err.Error(), DNSRuleActionNestedUnsupportedMessage) +} diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 00000000..2ca2529a --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,288 @@ +package option + +import ( + "net/url" + "path/filepath" + "reflect" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type,omitempty"` + Tag string `json:"tag"` + Format string `json:"format,omitempty"` + InlineOptions PlainRuleSet `json:"-"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + if r.Type != C.RuleSetTypeInline { + var defaultFormat string + switch r.Type { + case C.RuleSetTypeLocal: + defaultFormat = ruleSetDefaultFormat(r.LocalOptions.Path) + case C.RuleSetTypeRemote: + defaultFormat = ruleSetDefaultFormat(r.RemoteOptions.URL) + } + if r.Format == defaultFormat { + r.Format = "" + } + } + var v any + switch r.Type { + case "", C.RuleSetTypeInline: + r.Type = "" + v = r.InlineOptions + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule-set type: " + r.Type) + } + return badjson.MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing tag") + } + var v any + switch r.Type { + case "", C.RuleSetTypeInline: + r.Type = C.RuleSetTypeInline + v = &r.InlineOptions + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + default: + return E.New("unknown rule-set type: " + r.Type) + } + err = badjson.UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return err + } + if r.Type != C.RuleSetTypeInline { + if r.Format == "" { + switch r.Type { + case C.RuleSetTypeLocal: + r.Format = ruleSetDefaultFormat(r.LocalOptions.Path) + case C.RuleSetTypeRemote: + r.Format = ruleSetDefaultFormat(r.RemoteOptions.URL) + } + } + switch r.Format { + case "": + return E.New("missing format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule-set format: " + r.Format) + } + } else { + r.Format = "" + } + return nil +} + +func ruleSetDefaultFormat(path string) string { + if pathURL, err := url.Parse(path); err == nil { + path = pathURL.Path + } + switch filepath.Ext(path) { + case ".json": + return C.RuleSetFormatSource + case ".srs": + return C.RuleSetFormatBinary + default: + return "" + } +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval badoption.Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return badjson.MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = badjson.UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return err + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` + DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` + + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` + + AdGuardDomain badoption.Listable[string] `json:"-"` + AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version uint8 `json:"version"` + Options PlainRuleSet `json:"-"` + RawMessage json.RawMessage `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: + v = r.Options + default: + return nil, E.New("unknown rule-set version: ", r.Version) + } + return badjson.MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: + v = &r.Options + case 0: + return E.New("missing rule-set version") + default: + return E.New("unknown rule-set version: ", r.Version) + } + err = badjson.UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return err + } + r.RawMessage = bytes + return nil +} + +func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { + switch r.Version { + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: + default: + return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) + } + return r.Options, nil +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/service.go b/option/service.go new file mode 100644 index 00000000..7d45bc14 --- /dev/null +++ b/option/service.go @@ -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 +} diff --git a/option/shadowsocks.go b/option/shadowsocks.go new file mode 100644 index 00000000..7cb656f3 --- /dev/null +++ b/option/shadowsocks.go @@ -0,0 +1,35 @@ +package option + +type ShadowsocksInboundOptions struct { + ListenOptions + Network NetworkList `json:"network,omitempty"` + Method string `json:"method"` + Password string `json:"password,omitempty"` + Users []ShadowsocksUser `json:"users,omitempty"` + Destinations []ShadowsocksDestination `json:"destinations,omitempty"` + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` + Managed bool `json:"managed,omitempty"` +} + +type ShadowsocksUser struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type ShadowsocksDestination struct { + Name string `json:"name"` + Password string `json:"password"` + ServerOptions +} + +type ShadowsocksOutboundOptions struct { + DialerOptions + ServerOptions + Method string `json:"method"` + Password string `json:"password"` + Plugin string `json:"plugin,omitempty"` + PluginOptions string `json:"plugin_opts,omitempty"` + Network NetworkList `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` +} diff --git a/option/shadowsocksr.go b/option/shadowsocksr.go new file mode 100644 index 00000000..b87255bb --- /dev/null +++ b/option/shadowsocksr.go @@ -0,0 +1,13 @@ +package option + +type ShadowsocksROutboundOptions struct { + DialerOptions + ServerOptions + Method string `json:"method"` + Password string `json:"password"` + Obfs string `json:"obfs,omitempty"` + ObfsParam string `json:"obfs_param,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolParam string `json:"protocol_param,omitempty"` + Network NetworkList `json:"network,omitempty"` +} diff --git a/option/shadowtls.go b/option/shadowtls.go new file mode 100644 index 00000000..81ef9a43 --- /dev/null +++ b/option/shadowtls.go @@ -0,0 +1,81 @@ +package option + +import ( + "encoding/json" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" +) + +type ShadowTLSInboundOptions struct { + ListenOptions + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` + Users []ShadowTLSUser `json:"users,omitempty"` + Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"` + HandshakeForServerName *badjson.TypedMap[string, ShadowTLSHandshakeOptions] `json:"handshake_for_server_name,omitempty"` + StrictMode bool `json:"strict_mode,omitempty"` + WildcardSNI WildcardSNI `json:"wildcard_sni,omitempty"` +} + +type WildcardSNI int + +const ( + ShadowTLSWildcardSNIOff WildcardSNI = iota + ShadowTLSWildcardSNIAuthed + ShadowTLSWildcardSNIAll +) + +func (w WildcardSNI) MarshalJSON() ([]byte, error) { + return json.Marshal(w.String()) +} + +func (w WildcardSNI) String() string { + switch w { + case ShadowTLSWildcardSNIOff: + return "off" + case ShadowTLSWildcardSNIAuthed: + return "authed" + case ShadowTLSWildcardSNIAll: + return "all" + default: + panic("unknown wildcard SNI value") + } +} + +func (w *WildcardSNI) UnmarshalJSON(bytes []byte) error { + var valueString string + err := json.Unmarshal(bytes, &valueString) + if err != nil { + return err + } + switch valueString { + case "off", "": + *w = ShadowTLSWildcardSNIOff + case "authed": + *w = ShadowTLSWildcardSNIAuthed + case "all": + *w = ShadowTLSWildcardSNIAll + default: + return E.New("unknown wildcard SNI value: ", valueString) + } + return nil +} + +type ShadowTLSUser struct { + Name string `json:"name,omitempty"` + Password string `json:"password,omitempty"` +} + +type ShadowTLSHandshakeOptions struct { + ServerOptions + DialerOptions +} + +type ShadowTLSOutboundOptions struct { + DialerOptions + ServerOptions + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/option/simple.go b/option/simple.go new file mode 100644 index 00000000..f244ba18 --- /dev/null +++ b/option/simple.go @@ -0,0 +1,40 @@ +package option + +import ( + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/json/badoption" +) + +type SocksInboundOptions struct { + ListenOptions + Users []auth.User `json:"users,omitempty"` + DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` +} + +type HTTPMixedInboundOptions struct { + ListenOptions + Users []auth.User `json:"users,omitempty"` + DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` + SetSystemProxy bool `json:"set_system_proxy,omitempty"` + InboundTLSOptionsContainer +} + +type SOCKSOutboundOptions struct { + DialerOptions + ServerOptions + Version string `json:"version,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` +} + +type HTTPOutboundOptions struct { + DialerOptions + ServerOptions + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer + Path string `json:"path,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` +} diff --git a/option/ssh.go b/option/ssh.go new file mode 100644 index 00000000..1c6ca6bb --- /dev/null +++ b/option/ssh.go @@ -0,0 +1,16 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type SSHOutboundOptions struct { + DialerOptions + ServerOptions + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + PrivateKey badoption.Listable[string] `json:"private_key,omitempty"` + PrivateKeyPath string `json:"private_key_path,omitempty"` + PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` + HostKey badoption.Listable[string] `json:"host_key,omitempty"` + HostKeyAlgorithms badoption.Listable[string] `json:"host_key_algorithms,omitempty"` + ClientVersion string `json:"client_version,omitempty"` +} diff --git a/option/ssmapi.go b/option/ssmapi.go new file mode 100644 index 00000000..8d25f400 --- /dev/null +++ b/option/ssmapi.go @@ -0,0 +1,12 @@ +package option + +import ( + "github.com/sagernet/sing/common/json/badjson" +) + +type SSMAPIServiceOptions struct { + ListenOptions + Servers *badjson.TypedMap[string, string] `json:"servers"` + CachePath string `json:"cache_path,omitempty"` + InboundTLSOptionsContainer +} diff --git a/option/tailscale.go b/option/tailscale.go new file mode 100644 index 00000000..a4f82ce0 --- /dev/null +++ b/option/tailscale.go @@ -0,0 +1,126 @@ +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 { + DialerOptions + StateDirectory string `json:"state_directory,omitempty"` + AuthKey string `json:"auth_key,omitempty"` + ControlURL string `json:"control_url,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + Hostname string `json:"hostname,omitempty"` + AcceptRoutes bool `json:"accept_routes,omitempty"` + ExitNode string `json:"exit_node,omitempty"` + ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"` + AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"` + AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"` + AdvertiseTags badoption.Listable[string] `json:"advertise_tags,omitempty"` + RelayServerPort *uint16 `json:"relay_server_port,omitempty"` + RelayServerStaticEndpoints []netip.AddrPort `json:"relay_server_static_endpoints,omitempty"` + SystemInterface bool `json:"system_interface,omitempty"` + SystemInterfaceName string `json:"system_interface_name,omitempty"` + SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` +} + +type TailscaleDNSServerOptions struct { + Endpoint string `json:"endpoint,omitempty"` + AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` +} + +type TailscaleCertificateProviderOptions struct { + Endpoint string `json:"endpoint,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"` + Home string `json:"home,omitempty"` + MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"` + MeshPSK string `json:"mesh_psk,omitempty"` + MeshPSKFile string `json:"mesh_psk_file,omitempty"` + STUN *DERPSTUNListenOptions `json:"stun,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.ParseSocksaddr(verifyURL.Hostname()).IsDomain() +} + +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 _DERPSTUNListenOptions struct { + Enabled bool + ListenOptions +} + +type DERPSTUNListenOptions _DERPSTUNListenOptions + +func (d DERPSTUNListenOptions) MarshalJSON() ([]byte, error) { + portOptions := _DERPSTUNListenOptions{ + Enabled: d.Enabled, + ListenOptions: ListenOptions{ + ListenPort: d.ListenPort, + }, + } + if _DERPSTUNListenOptions(d) == portOptions { + return json.Marshal(d.Enabled) + } else { + return json.Marshal(_DERPSTUNListenOptions(d)) + } +} + +func (d *DERPSTUNListenOptions) UnmarshalJSON(bytes []byte) error { + var portValue uint16 + err := json.Unmarshal(bytes, &portValue) + if err == nil { + d.Enabled = true + d.ListenPort = portValue + return nil + } + return json.Unmarshal(bytes, (*_DERPSTUNListenOptions)(d)) +} diff --git a/option/tls.go b/option/tls.go new file mode 100644 index 00000000..dbbb7620 --- /dev/null +++ b/option/tls.go @@ -0,0 +1,242 @@ +package option + +import ( + "crypto/tls" + "encoding/json" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" +) + +type InboundTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN badoption.Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` + CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientAuthentication ClientAuthType `json:"client_authentication,omitempty"` + ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath badoption.Listable[string] `json:"client_certificate_path,omitempty"` + ClientCertificatePublicKeySHA256 badoption.Listable[[]byte] `json:"client_certificate_public_key_sha256,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + KernelTx bool `json:"kernel_tx,omitempty"` + KernelRx bool `json:"kernel_rx,omitempty"` + CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"` + + // Deprecated: use certificate_provider + ACME *InboundACMEOptions `json:"acme,omitempty"` + + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` +} + +type ClientAuthType tls.ClientAuthType + +func (t ClientAuthType) MarshalJSON() ([]byte, error) { + var stringValue string + switch t { + case ClientAuthType(tls.NoClientCert): + stringValue = "no" + case ClientAuthType(tls.RequestClientCert): + stringValue = "request" + case ClientAuthType(tls.RequireAnyClientCert): + stringValue = "require-any" + case ClientAuthType(tls.VerifyClientCertIfGiven): + stringValue = "verify-if-given" + case ClientAuthType(tls.RequireAndVerifyClientCert): + stringValue = "require-and-verify" + default: + return nil, E.New("unknown client authentication type: ", int(t)) + } + return json.Marshal(stringValue) +} + +func (t *ClientAuthType) UnmarshalJSON(data []byte) error { + var stringValue string + err := json.Unmarshal(data, &stringValue) + if err != nil { + return err + } + switch stringValue { + case "no": + *t = ClientAuthType(tls.NoClientCert) + case "request": + *t = ClientAuthType(tls.RequestClientCert) + case "require-any": + *t = ClientAuthType(tls.RequireAnyClientCert) + case "verify-if-given": + *t = ClientAuthType(tls.VerifyClientCertIfGiven) + case "require-and-verify": + *t = ClientAuthType(tls.RequireAndVerifyClientCert) + default: + return E.New("unknown client authentication type: ", stringValue) + } + return nil +} + +type InboundTLSOptionsContainer struct { + TLS *InboundTLSOptions `json:"tls,omitempty"` +} + +type InboundTLSOptionsWrapper interface { + TakeInboundTLSOptions() *InboundTLSOptions + ReplaceInboundTLSOptions(options *InboundTLSOptions) +} + +func (o *InboundTLSOptionsContainer) TakeInboundTLSOptions() *InboundTLSOptions { + return o.TLS +} + +func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTLSOptions) { + o.TLS = options +} + +type OutboundTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN badoption.Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` + CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + CertificatePublicKeySHA256 badoption.Listable[[]byte] `json:"certificate_public_key_sha256,omitempty"` + ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ClientKey badoption.Listable[string] `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + Fragment bool `json:"fragment,omitempty"` + FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"` + RecordFragment bool `json:"record_fragment,omitempty"` + KernelTx bool `json:"kernel_tx,omitempty"` + KernelRx bool `json:"kernel_rx,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` +} + +type OutboundTLSOptionsContainer struct { + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} + +type OutboundTLSOptionsWrapper interface { + TakeOutboundTLSOptions() *OutboundTLSOptions + ReplaceOutboundTLSOptions(options *OutboundTLSOptions) +} + +func (o *OutboundTLSOptionsContainer) TakeOutboundTLSOptions() *OutboundTLSOptions { + return o.TLS +} + +func (o *OutboundTLSOptionsContainer) ReplaceOutboundTLSOptions(options *OutboundTLSOptions) { + o.TLS = options +} + +type CurvePreference tls.CurveID + +const ( + CurveP256 = 23 + CurveP384 = 24 + CurveP521 = 25 + X25519 = 29 + X25519MLKEM768 = 4588 +) + +func (c CurvePreference) MarshalJSON() ([]byte, error) { + var stringValue string + switch c { + case CurvePreference(CurveP256): + stringValue = "P256" + case CurvePreference(CurveP384): + stringValue = "P384" + case CurvePreference(CurveP521): + stringValue = "P521" + case CurvePreference(X25519): + stringValue = "X25519" + case CurvePreference(X25519MLKEM768): + stringValue = "X25519MLKEM768" + default: + return nil, E.New("unknown curve id: ", int(c)) + } + return json.Marshal(stringValue) +} + +func (c *CurvePreference) UnmarshalJSON(data []byte) error { + var stringValue string + err := json.Unmarshal(data, &stringValue) + if err != nil { + return err + } + switch strings.ToUpper(stringValue) { + case "P256": + *c = CurvePreference(CurveP256) + case "P384": + *c = CurvePreference(CurveP384) + case "P521": + *c = CurvePreference(CurveP521) + case "X25519": + *c = CurvePreference(X25519) + case "X25519MLKEM768": + *c = CurvePreference(X25519MLKEM768) + default: + return E.New("unknown curve name: ", stringValue) + } + return nil +} + +type InboundRealityOptions struct { + Enabled bool `json:"enabled,omitempty"` + Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + ShortID badoption.Listable[string] `json:"short_id,omitempty"` + MaxTimeDifference badoption.Duration `json:"max_time_difference,omitempty"` +} + +type InboundRealityHandshakeOptions struct { + ServerOptions + DialerOptions +} + +type InboundECHOptions struct { + Enabled bool `json:"enabled,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + + // Deprecated: not supported by stdlib + PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` + // Deprecated: added by fault + DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` +} + +type OutboundECHOptions struct { + Enabled bool `json:"enabled,omitempty"` + Config badoption.Listable[string] `json:"config,omitempty"` + ConfigPath string `json:"config_path,omitempty"` + QueryServerName string `json:"query_server_name,omitempty"` + + // Deprecated: not supported by stdlib + PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` + // Deprecated: added by fault + DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` +} + +type OutboundUTLSOptions struct { + Enabled bool `json:"enabled,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` +} + +type OutboundRealityOptions struct { + Enabled bool `json:"enabled,omitempty"` + PublicKey string `json:"public_key,omitempty"` + ShortID string `json:"short_id,omitempty"` +} diff --git a/option/tls_acme.go b/option/tls_acme.go new file mode 100644 index 00000000..6dd8fa70 --- /dev/null +++ b/option/tls_acme.go @@ -0,0 +1,96 @@ +package option + +import ( + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type InboundACMEOptions struct { + Domain badoption.Listable[string] `json:"domain,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + DefaultServerName string `json:"default_server_name,omitempty"` + Email string `json:"email,omitempty"` + Provider string `json:"provider,omitempty"` + DisableHTTPChallenge bool `json:"disable_http_challenge,omitempty"` + DisableTLSALPNChallenge bool `json:"disable_tls_alpn_challenge,omitempty"` + AlternativeHTTPPort uint16 `json:"alternative_http_port,omitempty"` + AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"` + ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"` + DNS01Challenge *ACMEDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` +} + +type ACMEExternalAccountOptions struct { + KeyID string `json:"key_id,omitempty"` + MACKey string `json:"mac_key,omitempty"` +} + +type _ACMEDNS01ChallengeOptions struct { + Provider string `json:"provider,omitempty"` + AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"` + CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"` + ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"` +} + +type ACMEDNS01ChallengeOptions _ACMEDNS01ChallengeOptions + +func (o ACMEDNS01ChallengeOptions) MarshalJSON() ([]byte, error) { + var v any + switch o.Provider { + case C.DNSProviderAliDNS: + v = o.AliDNSOptions + case C.DNSProviderCloudflare: + v = o.CloudflareOptions + case C.DNSProviderACMEDNS: + v = o.ACMEDNSOptions + case "": + return nil, E.New("missing provider type") + default: + return nil, E.New("unknown provider type: " + o.Provider) + } + return badjson.MarshallObjects((_ACMEDNS01ChallengeOptions)(o), v) +} + +func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_ACMEDNS01ChallengeOptions)(o)) + if err != nil { + return err + } + var v any + switch o.Provider { + case C.DNSProviderAliDNS: + v = &o.AliDNSOptions + case C.DNSProviderCloudflare: + v = &o.CloudflareOptions + case C.DNSProviderACMEDNS: + v = &o.ACMEDNSOptions + default: + return E.New("unknown provider type: " + o.Provider) + } + err = badjson.UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v) + if err != nil { + return err + } + return nil +} + +type ACMEDNS01AliDNSOptions struct { + AccessKeyID string `json:"access_key_id,omitempty"` + AccessKeySecret string `json:"access_key_secret,omitempty"` + RegionID string `json:"region_id,omitempty"` + SecurityToken string `json:"security_token,omitempty"` +} + +type ACMEDNS01CloudflareOptions struct { + APIToken string `json:"api_token,omitempty"` + ZoneToken string `json:"zone_token,omitempty"` +} + +type ACMEDNS01ACMEDNSOptions struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Subdomain string `json:"subdomain,omitempty"` + ServerURL string `json:"server_url,omitempty"` +} diff --git a/option/tor.go b/option/tor.go new file mode 100644 index 00000000..a56f70ae --- /dev/null +++ b/option/tor.go @@ -0,0 +1,9 @@ +package option + +type TorOutboundOptions struct { + DialerOptions + ExecutablePath string `json:"executable_path,omitempty"` + ExtraArgs []string `json:"extra_args,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + Options map[string]string `json:"torrc,omitempty"` +} diff --git a/option/trojan.go b/option/trojan.go new file mode 100644 index 00000000..7d36c8d2 --- /dev/null +++ b/option/trojan.go @@ -0,0 +1,26 @@ +package option + +type TrojanInboundOptions struct { + ListenOptions + Users []TrojanUser `json:"users,omitempty"` + InboundTLSOptionsContainer + Fallback *ServerOptions `json:"fallback,omitempty"` + FallbackForALPN map[string]*ServerOptions `json:"fallback_for_alpn,omitempty"` + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} + +type TrojanUser struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type TrojanOutboundOptions struct { + DialerOptions + ServerOptions + Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} diff --git a/option/tuic.go b/option/tuic.go new file mode 100644 index 00000000..a9b739ec --- /dev/null +++ b/option/tuic.go @@ -0,0 +1,33 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type TUICInboundOptions struct { + ListenOptions + Users []TUICUser `json:"users,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + AuthTimeout badoption.Duration `json:"auth_timeout,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat badoption.Duration `json:"heartbeat,omitempty"` + InboundTLSOptionsContainer +} + +type TUICUser struct { + Name string `json:"name,omitempty"` + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` +} + +type TUICOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat badoption.Duration `json:"heartbeat,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer +} diff --git a/option/tun.go b/option/tun.go new file mode 100644 index 00000000..fda028b6 --- /dev/null +++ b/option/tun.go @@ -0,0 +1,88 @@ +package option + +import ( + "net/netip" + "strconv" + + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" +) + +type TunInboundOptions struct { + InterfaceName string `json:"interface_name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + Address badoption.Listable[netip.Prefix] `json:"address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` + IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` + AutoRedirect bool `json:"auto_redirect,omitempty"` + AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` + AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` + AutoRedirectResetMark FwMark `json:"auto_redirect_reset_mark,omitempty"` + AutoRedirectNFQueue uint16 `json:"auto_redirect_nfqueue,omitempty"` + AutoRedirectFallbackRuleIndex int `json:"auto_redirect_iproute2_fallback_rule_index,omitempty"` + ExcludeMPTCP bool `json:"exclude_mptcp,omitempty"` + LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"` + RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"` + RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` + RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"` + IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"` + ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"` + IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"` + IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"` + ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"` + ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"` + IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"` + IncludePackage badoption.Listable[string] `json:"include_package,omitempty"` + ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"` + IncludeMACAddress badoption.Listable[string] `json:"include_mac_address,omitempty"` + ExcludeMACAddress badoption.Listable[string] `json:"exclude_mac_address,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` + InboundOptions + + // Deprecated: removed + GSO bool `json:"gso,omitempty"` + // Deprecated: merged to Address + Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty"` + // Deprecated: merged to Address + Inet6Address badoption.Listable[netip.Prefix] `json:"inet6_address,omitempty"` + // Deprecated: merged to RouteAddress + Inet4RouteAddress badoption.Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` + // Deprecated: merged to RouteAddress + Inet6RouteAddress badoption.Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + // Deprecated: merged to RouteExcludeAddress + Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` + // Deprecated: merged to RouteExcludeAddress + Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` + // Deprecated: removed + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` +} + +type FwMark uint32 + +func (f FwMark) MarshalJSON() ([]byte, error) { + return json.Marshal(F.ToString("0x", strconv.FormatUint(uint64(f), 16))) +} + +func (f *FwMark) UnmarshalJSON(bytes []byte) error { + var stringValue string + err := json.Unmarshal(bytes, &stringValue) + if err != nil { + if rawErr := json.Unmarshal(bytes, (*uint32)(f)); rawErr == nil { + return nil + } + return E.Cause(err, "invalid number or string mark") + } + intValue, err := strconv.ParseUint(stringValue, 0, 32) + if err != nil { + return err + } + *f = FwMark(intValue) + return nil +} diff --git a/option/tun_platform.go b/option/tun_platform.go new file mode 100644 index 00000000..b42f6894 --- /dev/null +++ b/option/tun_platform.go @@ -0,0 +1,14 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type TunPlatformOptions struct { + HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"` +} + +type HTTPProxyOptions struct { + Enabled bool `json:"enabled,omitempty"` + ServerOptions + BypassDomain badoption.Listable[string] `json:"bypass_domain,omitempty"` + MatchDomain badoption.Listable[string] `json:"match_domain,omitempty"` +} diff --git a/option/types.go b/option/types.go new file mode 100644 index 00000000..fe7d4b3d --- /dev/null +++ b/option/types.go @@ -0,0 +1,196 @@ +package option + +import ( + "strings" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +type NetworkList string + +func (v *NetworkList) UnmarshalJSON(content []byte) error { + var networkList []string + err := json.Unmarshal(content, &networkList) + if err != nil { + var networkItem string + err = json.Unmarshal(content, &networkItem) + if err != nil { + return err + } + networkList = []string{networkItem} + } + for _, networkName := range networkList { + switch networkName { + case N.NetworkTCP, N.NetworkUDP: + break + default: + return E.New("unknown network: " + networkName) + } + } + *v = NetworkList(strings.Join(networkList, "\n")) + return nil +} + +func (v NetworkList) Build() []string { + if v == "" { + return []string{N.NetworkTCP, N.NetworkUDP} + } + return strings.Split(string(v), "\n") +} + +type DomainStrategy C.DomainStrategy + +func (s DomainStrategy) String() string { + switch C.DomainStrategy(s) { + case C.DomainStrategyAsIS: + return "" + case C.DomainStrategyPreferIPv4: + return "prefer_ipv4" + case C.DomainStrategyPreferIPv6: + return "prefer_ipv6" + case C.DomainStrategyIPv4Only: + return "ipv4_only" + case C.DomainStrategyIPv6Only: + return "ipv6_only" + default: + panic(E.New("unknown domain strategy: ", s)) + } +} + +func (s DomainStrategy) MarshalJSON() ([]byte, error) { + var value string + switch C.DomainStrategy(s) { + case C.DomainStrategyAsIS: + value = "" + // value = "as_is" + case C.DomainStrategyPreferIPv4: + value = "prefer_ipv4" + case C.DomainStrategyPreferIPv6: + value = "prefer_ipv6" + case C.DomainStrategyIPv4Only: + value = "ipv4_only" + case C.DomainStrategyIPv6Only: + value = "ipv6_only" + default: + return nil, E.New("unknown domain strategy: ", s) + } + return json.Marshal(value) +} + +func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error { + var value string + err := json.Unmarshal(bytes, &value) + if err != nil { + return err + } + switch value { + case "", "as_is": + *s = DomainStrategy(C.DomainStrategyAsIS) + case "prefer_ipv4": + *s = DomainStrategy(C.DomainStrategyPreferIPv4) + case "prefer_ipv6": + *s = DomainStrategy(C.DomainStrategyPreferIPv6) + case "ipv4_only": + *s = DomainStrategy(C.DomainStrategyIPv4Only) + case "ipv6_only": + *s = DomainStrategy(C.DomainStrategyIPv6Only) + default: + return E.New("unknown domain strategy: ", value) + } + return nil +} + +type DNSQueryType uint16 + +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + +func (t DNSQueryType) MarshalJSON() ([]byte, error) { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return json.Marshal(typeName) + } + return json.Marshal(uint16(t)) +} + +func (t *DNSQueryType) UnmarshalJSON(bytes []byte) error { + var valueNumber uint16 + err := json.Unmarshal(bytes, &valueNumber) + if err == nil { + *t = DNSQueryType(valueNumber) + return nil + } + var valueString string + err = json.Unmarshal(bytes, &valueString) + if err == nil { + queryType, loaded := mDNS.StringToType[valueString] + if loaded { + *t = DNSQueryType(queryType) + return nil + } + } + return E.New("unknown DNS query type: ", string(bytes)) +} + +func DNSQueryTypeToString(queryType uint16) string { + typeName, loaded := mDNS.TypeToString[queryType] + if loaded { + return typeName + } + return F.ToString(queryType) +} + +type NetworkStrategy C.NetworkStrategy + +func (n NetworkStrategy) MarshalJSON() ([]byte, error) { + return json.Marshal(C.NetworkStrategy(n).String()) +} + +func (n *NetworkStrategy) UnmarshalJSON(content []byte) error { + var value string + err := json.Unmarshal(content, &value) + if err != nil { + return err + } + strategy, loaded := C.StringToNetworkStrategy[value] + if !loaded { + return E.New("unknown network strategy: ", value) + } + *n = NetworkStrategy(strategy) + return nil +} + +type InterfaceType C.InterfaceType + +func (t InterfaceType) Build() C.InterfaceType { + return C.InterfaceType(t) +} + +func (t InterfaceType) MarshalJSON() ([]byte, error) { + return json.Marshal(C.InterfaceType(t).String()) +} + +func (t *InterfaceType) UnmarshalJSON(content []byte) error { + var value string + err := json.Unmarshal(content, &value) + if err != nil { + return err + } + interfaceType, loaded := C.StringToInterfaceType[value] + if !loaded { + return E.New("unknown interface type: ", value) + } + *t = InterfaceType(interfaceType) + return nil +} diff --git a/option/udp_over_tcp.go b/option/udp_over_tcp.go new file mode 100644 index 00000000..b496017a --- /dev/null +++ b/option/udp_over_tcp.go @@ -0,0 +1,30 @@ +package option + +import ( + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/uot" +) + +type _UDPOverTCPOptions struct { + Enabled bool `json:"enabled,omitempty"` + Version uint8 `json:"version,omitempty"` +} + +type UDPOverTCPOptions _UDPOverTCPOptions + +func (o UDPOverTCPOptions) MarshalJSON() ([]byte, error) { + switch o.Version { + case 0, uot.Version: + return json.Marshal(o.Enabled) + default: + return json.Marshal(_UDPOverTCPOptions(o)) + } +} + +func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, &o.Enabled) + if err == nil { + return nil + } + return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o)) +} diff --git a/option/v2ray.go b/option/v2ray.go new file mode 100644 index 00000000..774a651d --- /dev/null +++ b/option/v2ray.go @@ -0,0 +1 @@ +package option diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go new file mode 100644 index 00000000..68c23858 --- /dev/null +++ b/option/v2ray_transport.go @@ -0,0 +1,100 @@ +package option + +import ( + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type _V2RayTransportOptions struct { + Type string `json:"type"` + HTTPOptions V2RayHTTPOptions `json:"-"` + WebsocketOptions V2RayWebsocketOptions `json:"-"` + QUICOptions V2RayQUICOptions `json:"-"` + GRPCOptions V2RayGRPCOptions `json:"-"` + HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` +} + +type V2RayTransportOptions _V2RayTransportOptions + +func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { + var v any + switch o.Type { + case C.V2RayTransportTypeHTTP: + v = o.HTTPOptions + case C.V2RayTransportTypeWebsocket: + v = o.WebsocketOptions + case C.V2RayTransportTypeQUIC: + v = o.QUICOptions + case C.V2RayTransportTypeGRPC: + v = o.GRPCOptions + case C.V2RayTransportTypeHTTPUpgrade: + v = o.HTTPUpgradeOptions + case "": + return nil, E.New("missing transport type") + default: + return nil, E.New("unknown transport type: " + o.Type) + } + return badjson.MarshallObjects((_V2RayTransportOptions)(o), v) +} + +func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_V2RayTransportOptions)(o)) + if err != nil { + return err + } + var v any + switch o.Type { + case C.V2RayTransportTypeHTTP: + v = &o.HTTPOptions + case C.V2RayTransportTypeWebsocket: + v = &o.WebsocketOptions + case C.V2RayTransportTypeQUIC: + v = &o.QUICOptions + case C.V2RayTransportTypeGRPC: + v = &o.GRPCOptions + case C.V2RayTransportTypeHTTPUpgrade: + v = &o.HTTPUpgradeOptions + default: + return E.New("unknown transport type: " + o.Type) + } + err = badjson.UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v) + if err != nil { + return err + } + return nil +} + +type V2RayHTTPOptions struct { + Host badoption.Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Method string `json:"method,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + PingTimeout badoption.Duration `json:"ping_timeout,omitempty"` +} + +type V2RayWebsocketOptions struct { + Path string `json:"path,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + MaxEarlyData uint32 `json:"max_early_data,omitempty"` + EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` +} + +type V2RayQUICOptions struct{} + +type V2RayGRPCOptions struct { + ServiceName string `json:"service_name,omitempty"` + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + PingTimeout badoption.Duration `json:"ping_timeout,omitempty"` + PermitWithoutStream bool `json:"permit_without_stream,omitempty"` + ForceLite bool `json:"-"` // for test +} + +type V2RayHTTPUpgradeOptions struct { + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` +} diff --git a/option/vless.go b/option/vless.go new file mode 100644 index 00000000..5acf2aee --- /dev/null +++ b/option/vless.go @@ -0,0 +1,27 @@ +package option + +type VLESSInboundOptions struct { + ListenOptions + Users []VLESSUser `json:"users,omitempty"` + InboundTLSOptionsContainer + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} + +type VLESSUser struct { + Name string `json:"name"` + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` +} + +type VLESSOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` + PacketEncoding *string `json:"packet_encoding,omitempty"` +} diff --git a/option/vmess.go b/option/vmess.go new file mode 100644 index 00000000..8a38fb70 --- /dev/null +++ b/option/vmess.go @@ -0,0 +1,30 @@ +package option + +type VMessInboundOptions struct { + ListenOptions + Users []VMessUser `json:"users,omitempty"` + InboundTLSOptionsContainer + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} + +type VMessUser struct { + Name string `json:"name"` + UUID string `json:"uuid"` + AlterId int `json:"alterId,omitempty"` +} + +type VMessOutboundOptions struct { + DialerOptions + ServerOptions + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` +} diff --git a/option/wireguard.go b/option/wireguard.go new file mode 100644 index 00000000..c86abd11 --- /dev/null +++ b/option/wireguard.go @@ -0,0 +1,30 @@ +package option + +import ( + "net/netip" + + "github.com/sagernet/sing/common/json/badoption" +) + +type WireGuardEndpointOptions struct { + System bool `json:"system,omitempty"` + Name string `json:"name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + Address badoption.Listable[netip.Prefix] `json:"address"` + PrivateKey string `json:"private_key"` + ListenPort uint16 `json:"listen_port,omitempty"` + Peers []WireGuardPeer `json:"peers,omitempty"` + UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` + Workers int `json:"workers,omitempty"` + DialerOptions +} + +type WireGuardPeer struct { + Address string `json:"address,omitempty"` + Port uint16 `json:"port,omitempty"` + PublicKey string `json:"public_key,omitempty"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"` + PersistentKeepaliveInterval uint16 `json:"persistent_keepalive_interval,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` +} diff --git a/protocol/anytls/inbound.go b/protocol/anytls/inbound.go new file mode 100644 index 00000000..52d77353 --- /dev/null +++ b/protocol/anytls/inbound.go @@ -0,0 +1,134 @@ +package anytls + +import ( + "context" + "net" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + anytls "github.com/anytls/sing-anytls" + "github.com/anytls/sing-anytls/padding" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.AnyTLSInboundOptions](registry, C.TypeAnyTLS, NewInbound) +} + +type Inbound struct { + inbound.Adapter + tlsConfig tls.ServerConfig + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service *anytls.Service +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeAnyTLS, tag), + router: uot.NewRouter(router, logger), + logger: logger, + } + + if options.TLS != nil && options.TLS.Enabled { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + + paddingScheme := padding.DefaultPaddingScheme + if len(options.PaddingScheme) > 0 { + paddingScheme = []byte(strings.Join(options.PaddingScheme, "\n")) + } + + service, err := anytls.NewService(anytls.ServiceConfig{ + Users: common.Map(options.Users, func(it option.AnyTLSUser) anytls.User { + return (anytls.User)(it) + }), + PaddingScheme: paddingScheme, + Handler: (*inboundHandler)(inbound), + Logger: logger, + }) + if err != nil { + return nil, err + } + inbound.service = service + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return common.Close(h.listener, h.tlsConfig) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if h.tlsConfig != nil { + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return + } + conn = tlsConn + } + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +type inboundHandler Inbound + +func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.Source = source + metadata.Destination = destination.Unwrap() + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/anytls/outbound.go b/protocol/anytls/outbound.go new file mode 100644 index 00000000..2f24c2ef --- /dev/null +++ b/protocol/anytls/outbound.go @@ -0,0 +1,131 @@ +package anytls + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + 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/uot" + + anytls "github.com/anytls/sing-anytls" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.AnyTLSOutboundOptions](registry, C.TypeAnyTLS, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + dialer tls.Dialer + server M.Socksaddr + tlsConfig tls.Config + client *anytls.Client + uotClient *uot.Client + logger log.ContextLogger +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSOutboundOptions) (adapter.Outbound, error) { + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeAnyTLS, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), + server: options.ServerOptions.Build(), + logger: logger, + } + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + // TCP Fast Open is incompatible with anytls because TFO creates a lazy connection + // that only establishes on first write. The lazy connection returns an empty address + // before establishment, but anytls SOCKS wrapper tries to access the remote address + // during handshake, causing a null pointer dereference crash. + if options.DialerOptions.TCPFastOpen { + return nil, E.New("tcp_fast_open is not supported with anytls outbound") + } + + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + outbound.tlsConfig = tlsConfig + + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + }) + if err != nil { + return nil, err + } + + outbound.dialer = tls.NewDialer(outboundDialer, tlsConfig) + + client, err := anytls.NewClient(ctx, anytls.ClientConfig{ + Password: options.Password, + IdleSessionCheckInterval: options.IdleSessionCheckInterval.Build(), + IdleSessionTimeout: options.IdleSessionTimeout.Build(), + MinIdleSession: options.MinIdleSession, + DialOut: outbound.dialOut, + Logger: logger, + }) + if err != nil { + return nil, err + } + outbound.client = client + + outbound.uotClient = &uot.Client{ + Dialer: (anytlsDialer)(client.CreateProxy), + Version: uot.Version, + } + return outbound, nil +} + +type anytlsDialer func(ctx context.Context, destination M.Socksaddr) (net.Conn, error) + +func (d anytlsDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d(ctx, destination) +} + +func (d anytlsDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) { + return h.dialer.DialTLSContext(ctx, h.server) +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.CreateProxy(ctx, destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) + return h.uotClient.DialContext(ctx, network, destination) + } + return nil, os.ErrInvalid +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) + return h.uotClient.ListenPacket(ctx, destination) +} + +func (h *Outbound) Close() error { + return common.Close(h.client) +} diff --git a/protocol/block/outbound.go b/protocol/block/outbound.go new file mode 100644 index 00000000..fe1ccda7 --- /dev/null +++ b/protocol/block/outbound.go @@ -0,0 +1,42 @@ +package block + +import ( + "context" + "net" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.StubOptions](registry, C.TypeBlock, New) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger +} + +func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, _ option.StubOptions) (adapter.Outbound, error) { + return &Outbound{ + Adapter: outbound.NewAdapter(C.TypeBlock, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), + logger: logger, + }, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + h.logger.InfoContext(ctx, "blocked connection to ", destination) + return nil, syscall.EPERM +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + h.logger.InfoContext(ctx, "blocked packet connection to ", destination) + return nil, syscall.EPERM +} diff --git a/protocol/cloudflare/inbound.go b/protocol/cloudflare/inbound.go new file mode 100644 index 00000000..f445ab95 --- /dev/null +++ b/protocol/cloudflare/inbound.go @@ -0,0 +1,160 @@ +//go:build with_cloudflared + +package cloudflare + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + boxDialer "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + cloudflared "github.com/sagernet/sing-cloudflared" + tun "github.com/sagernet/sing-tun" + "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/pipe" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.CloudflaredInboundOptions](registry, C.TypeCloudflared, NewInbound) +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.CloudflaredInboundOptions) (adapter.Inbound, error) { + controlDialer, err := boxDialer.NewWithOptions(boxDialer.Options{ + Context: ctx, + Options: options.ControlDialer, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "build cloudflared control dialer") + } + tunnelDialer, err := boxDialer.NewWithOptions(boxDialer.Options{ + Context: ctx, + Options: options.TunnelDialer, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "build cloudflared tunnel dialer") + } + + service, err := cloudflared.NewService(cloudflared.ServiceOptions{ + Logger: logger, + ConnectionDialer: &routerDialer{router: router, tag: tag}, + ControlDialer: controlDialer, + TunnelDialer: tunnelDialer, + ICMPHandler: &icmpRouterHandler{router: router, logger: logger, tag: tag}, + ConnContext: func(connCtx context.Context) context.Context { + return adapter.WithContext(connCtx, &adapter.InboundContext{ + Inbound: tag, + InboundType: C.TypeCloudflared, + }) + }, + Token: options.Token, + HAConnections: options.HighAvailabilityConnections, + Protocol: options.Protocol, + PostQuantum: options.PostQuantum, + EdgeIPVersion: options.EdgeIPVersion, + DatagramVersion: options.DatagramVersion, + GracePeriod: time.Duration(options.GracePeriod), + Region: options.Region, + }) + if err != nil { + return nil, err + } + + return &Inbound{ + Adapter: inbound.NewAdapter(C.TypeCloudflared, tag), + service: service, + }, nil +} + +type Inbound struct { + inbound.Adapter + service *cloudflared.Service +} + +func (i *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return i.service.Start() +} + +func (i *Inbound) Close() error { + return i.service.Close() +} + +type routerDialer struct { + router adapter.Router + tag string +} + +func (d *routerDialer) newMetadata(network string, destination M.Socksaddr) adapter.InboundContext { + return adapter.InboundContext{ + Inbound: d.tag, + InboundType: C.TypeCloudflared, + Network: network, + Destination: destination, + } +} + +func (d *routerDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + input, output := pipe.Pipe() + go d.router.RouteConnectionEx(ctx, output, d.newMetadata(N.NetworkTCP, destination), N.OnceClose(func(it error) { + input.Close() + })) + return input, nil +} + +func (d *routerDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + input, output := pipe.Pipe() + routerConn := bufio.NewUnbindPacketConn(output) + go d.router.RoutePacketConnectionEx(ctx, routerConn, d.newMetadata(N.NetworkUDP, destination), N.OnceClose(func(it error) { + input.Close() + })) + return bufio.NewUnbindPacketConn(input), nil +} + +type icmpRouterHandler struct { + router adapter.Router + logger log.ContextLogger + tag string +} + +func (h *icmpRouterHandler) RouteICMPConnection(ctx context.Context, session tun.DirectRouteSession, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + var ipVersion uint8 + if session.Destination.Is4() { + ipVersion = 4 + } else { + ipVersion = 6 + } + destination := M.SocksaddrFrom(session.Destination, 0) + routeDestination, err := h.router.PreMatch(adapter.InboundContext{ + Inbound: h.tag, + InboundType: C.TypeCloudflared, + IPVersion: ipVersion, + Network: N.NetworkICMP, + Source: M.SocksaddrFrom(session.Source, 0), + Destination: destination, + OriginDestination: destination, + }, routeContext, timeout, false) + if err != nil { + switch { + case rule.IsBypassed(err): + err = nil + case rule.IsRejected(err): + h.logger.Trace("reject ICMP connection from ", session.Source, " to ", session.Destination) + default: + h.logger.Warn(E.Cause(err, "link ICMP connection from ", session.Source, " to ", session.Destination)) + } + } + return routeDestination, err +} diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go new file mode 100644 index 00000000..81353b65 --- /dev/null +++ b/protocol/direct/inbound.go @@ -0,0 +1,144 @@ +package direct + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/udpnat2" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.DirectInboundOptions](registry, C.TypeDirect, NewInbound) +} + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + udpNat *udpnat.Service + overrideOption int + overrideDestination M.Socksaddr +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectInboundOptions) (adapter.Inbound, error) { + options.UDPFragmentDefault = true + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeDirect, tag), + ctx: ctx, + router: router, + logger: logger, + } + if options.OverrideAddress != "" && options.OverridePort != 0 { + inbound.overrideOption = 1 + inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverrideAddress != "" { + inbound.overrideOption = 2 + inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverridePort != 0 { + inbound.overrideOption = 3 + inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort} + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false) + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + }) + return inbound, nil +} + +func (i *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return i.listener.Start() +} + +func (i *Inbound) Close() error { + return i.listener.Close() +} + +func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + i.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, i.listener.UDPAddr(), nil) +} + +func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + destination := metadata.OriginDestination + switch i.overrideOption { + case 1: + destination = i.overrideDestination + case 2: + destination.Addr = i.overrideDestination.Addr + case 3: + destination.Port = i.overrideDestination.Port + } + metadata.Destination = destination + if i.overrideOption != 0 { + i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } + i.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + i.logger.InfoContext(ctx, "inbound packet connection from ", source) + var metadata adapter.InboundContext + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + //nolint:staticcheck + metadata.InboundDetour = i.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.Source = source + destination = i.listener.UDPAddr() + switch i.overrideOption { + case 1: + destination = i.overrideDestination + case 2: + destination.Addr = i.overrideDestination.Addr + case 3: + destination.Port = i.overrideDestination.Port + default: + } + i.logger.InfoContext(ctx, "inbound packet connection to ", destination) + metadata.Destination = destination + if i.overrideOption != 0 { + conn = bufio.NewDestinationNATPacketConn(bufio.NewNetPacketConn(conn), i.listener.UDPAddr(), destination) + } + i.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (i *Inbound) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { + return true, log.ContextWithNewID(i.ctx), &directPacketWriter{i.listener.PacketWriter(), source}, nil +} + +type directPacketWriter struct { + writer N.PacketWriter + source M.Socksaddr +} + +func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error { + return w.writer.WritePacket(buffer, w.source) +} diff --git a/protocol/direct/loopback_detect.go b/protocol/direct/loopback_detect.go new file mode 100644 index 00000000..7a62164e --- /dev/null +++ b/protocol/direct/loopback_detect.go @@ -0,0 +1,186 @@ +package direct + +import ( + "net" + "net/netip" + "sync" + + "github.com/sagernet/sing-box/adapter" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type loopBackDetector struct { + networkManager adapter.NetworkManager + connAccess sync.RWMutex + packetConnAccess sync.RWMutex + connMap map[netip.AddrPort]netip.AddrPort + packetConnMap map[uint16]uint16 +} + +func newLoopBackDetector(networkManager adapter.NetworkManager) *loopBackDetector { + return &loopBackDetector{ + networkManager: networkManager, + connMap: make(map[netip.AddrPort]netip.AddrPort), + packetConnMap: make(map[uint16]uint16), + } +} + +func (l *loopBackDetector) NewConn(conn net.Conn) net.Conn { + source := M.AddrPortFromNet(conn.LocalAddr()) + if !source.IsValid() { + return conn + } + if udpConn, isUDPConn := conn.(abstractUDPConn); isUDPConn { + if !source.Addr().IsLoopback() { + _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) + if err != nil { + return conn + } + } + if !N.IsPublicAddr(source.Addr()) { + return conn + } + l.packetConnAccess.Lock() + l.packetConnMap[source.Port()] = M.AddrPortFromNet(conn.RemoteAddr()).Port() + l.packetConnAccess.Unlock() + return &loopBackDetectUDPWrapper{abstractUDPConn: udpConn, detector: l, connPort: source.Port()} + } else { + l.connAccess.Lock() + l.connMap[source] = M.AddrPortFromNet(conn.RemoteAddr()) + l.connAccess.Unlock() + return &loopBackDetectWrapper{Conn: conn, detector: l, connAddr: source} + } +} + +func (l *loopBackDetector) NewPacketConn(conn N.NetPacketConn, destination M.Socksaddr) N.NetPacketConn { + source := M.AddrPortFromNet(conn.LocalAddr()) + if !source.IsValid() { + return conn + } + if !source.Addr().IsLoopback() { + _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) + if err != nil { + return conn + } + } + l.packetConnAccess.Lock() + l.packetConnMap[source.Port()] = destination.AddrPort().Port() + l.packetConnAccess.Unlock() + return &loopBackDetectPacketWrapper{NetPacketConn: conn, detector: l, connPort: source.Port()} +} + +func (l *loopBackDetector) CheckConn(source netip.AddrPort, local netip.AddrPort) bool { + l.connAccess.RLock() + defer l.connAccess.RUnlock() + destination, loaded := l.connMap[source] + return loaded && destination != local +} + +func (l *loopBackDetector) CheckPacketConn(source netip.AddrPort, local netip.AddrPort) bool { + if !source.IsValid() { + return false + } + if !source.Addr().IsLoopback() { + _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) + if err != nil { + return false + } + } + if N.IsPublicAddr(source.Addr()) { + return false + } + l.packetConnAccess.RLock() + defer l.packetConnAccess.RUnlock() + destinationPort, loaded := l.packetConnMap[source.Port()] + return loaded && destinationPort != local.Port() +} + +type loopBackDetectWrapper struct { + net.Conn + detector *loopBackDetector + connAddr netip.AddrPort + closeOnce sync.Once +} + +func (w *loopBackDetectWrapper) Close() error { + w.closeOnce.Do(func() { + w.detector.connAccess.Lock() + delete(w.detector.connMap, w.connAddr) + w.detector.connAccess.Unlock() + }) + return w.Conn.Close() +} + +func (w *loopBackDetectWrapper) ReaderReplaceable() bool { + return true +} + +func (w *loopBackDetectWrapper) WriterReplaceable() bool { + return true +} + +func (w *loopBackDetectWrapper) Upstream() any { + return w.Conn +} + +type loopBackDetectPacketWrapper struct { + N.NetPacketConn + detector *loopBackDetector + connPort uint16 + closeOnce sync.Once +} + +func (w *loopBackDetectPacketWrapper) Close() error { + w.closeOnce.Do(func() { + w.detector.packetConnAccess.Lock() + delete(w.detector.packetConnMap, w.connPort) + w.detector.packetConnAccess.Unlock() + }) + return w.NetPacketConn.Close() +} + +func (w *loopBackDetectPacketWrapper) ReaderReplaceable() bool { + return true +} + +func (w *loopBackDetectPacketWrapper) WriterReplaceable() bool { + return true +} + +func (w *loopBackDetectPacketWrapper) Upstream() any { + return w.NetPacketConn +} + +type abstractUDPConn interface { + net.Conn + net.PacketConn +} + +type loopBackDetectUDPWrapper struct { + abstractUDPConn + detector *loopBackDetector + connPort uint16 + closeOnce sync.Once +} + +func (w *loopBackDetectUDPWrapper) Close() error { + w.closeOnce.Do(func() { + w.detector.packetConnAccess.Lock() + delete(w.detector.packetConnMap, w.connPort) + w.detector.packetConnAccess.Unlock() + }) + return w.abstractUDPConn.Close() +} + +func (w *loopBackDetectUDPWrapper) ReaderReplaceable() bool { + return true +} + +func (w *loopBackDetectUDPWrapper) WriterReplaceable() bool { + return true +} + +func (w *loopBackDetectUDPWrapper) Upstream() any { + return w.abstractUDPConn +} diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go new file mode 100644 index 00000000..9d24f31a --- /dev/null +++ b/protocol/direct/outbound.go @@ -0,0 +1,178 @@ +package direct + +import ( + "context" + "net" + "net/netip" + "reflect" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.DirectOutboundOptions](registry, C.TypeDirect, NewOutbound) +} + +var ( + _ N.ParallelDialer = (*Outbound)(nil) + _ dialer.ParallelNetworkDialer = (*Outbound)(nil) + _ dialer.DirectDialer = (*Outbound)(nil) + _ adapter.DirectRouteOutbound = (*Outbound)(nil) +) + +type Outbound struct { + outbound.Adapter + ctx context.Context + logger logger.ContextLogger + dialer dialer.ParallelInterfaceDialer + domainStrategy C.DomainStrategy + fallbackDelay time.Duration + isEmpty bool + // loopBack *loopBackDetector +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) { + options.UDPFragmentDefault = true + if options.Detour != "" { + return nil, E.New("`detour` is not supported in direct context") + } + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: true, + DirectOutbound: true, + }) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), + ctx: ctx, + logger: logger, + //nolint:staticcheck + domainStrategy: C.DomainStrategy(options.DomainStrategy), + fallbackDelay: time.Duration(options.FallbackDelay), + dialer: outboundDialer.(dialer.ParallelInterfaceDialer), + isEmpty: reflect.DeepEqual(options.DialerOptions, option.DialerOptions{UDPFragmentDefault: true}), + // loopBack: newLoopBackDetector(router), + } + //nolint:staticcheck + if options.ProxyProtocol != 0 { + return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") + } + return outbound, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + network = N.NetworkName(network) + switch network { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + /*conn, err := h.dialer.DialContext(ctx, network, destination) + if err != nil { + return nil, err + } + return h.loopBack.NewConn(conn), nil*/ + return h.dialer.DialContext(ctx, network, destination) +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + h.logger.InfoContext(ctx, "outbound packet connection") + conn, err := h.dialer.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + // conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination) + return conn, nil +} + +func (h *Outbound) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + ctx := log.ContextWithNewID(h.ctx) + destination, err := ping.ConnectDestination(ctx, h.logger, common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control, metadata.Destination.Addr, routeContext, timeout) + if err != nil { + return nil, err + } + h.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) + return destination, nil +} + +func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + network = N.NetworkName(network) + switch network { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, len(destinationAddresses) > 0 && destinationAddresses[0].Is6(), nil, nil, nil, h.fallbackDelay) +} + +func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + network = N.NetworkName(network) + switch network { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, len(destinationAddresses) > 0 && destinationAddresses[0].Is6(), networkStrategy, networkType, fallbackNetworkType, fallbackDelay) +} + +func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + h.logger.InfoContext(ctx, "outbound packet connection") + conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, networkType, fallbackNetworkType, fallbackDelay) + if err != nil { + return nil, netip.Addr{}, err + } + return conn, newDestination, nil +} + +func (h *Outbound) IsEmpty() bool { + return h.isEmpty +} + +/*func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + if h.loopBack.CheckConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { + return E.New("reject loopback connection to ", metadata.Destination) + } + return NewConnection(ctx, h, conn, metadata) +} + +func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + if h.loopBack.CheckPacketConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { + return E.New("reject loopback packet connection to ", metadata.Destination) + } + return NewPacketConnection(ctx, h, conn, metadata) +} +*/ diff --git a/protocol/dns/handle.go b/protocol/dns/handle.go new file mode 100644 index 00000000..e1323509 --- /dev/null +++ b/protocol/dns/handle.go @@ -0,0 +1,220 @@ +package dns + +import ( + "context" + "encoding/binary" + "net" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/canceler" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/task" + + mDNS "github.com/miekg/dns" +) + +func HandleStreamDNSRequest(ctx context.Context, router adapter.DNSRouter, conn net.Conn, metadata adapter.InboundContext) error { + var queryLength uint16 + err := binary.Read(conn, binary.BigEndian, &queryLength) + if err != nil { + return err + } + if queryLength == 0 { + return dns.RcodeFormatError + } + buffer := buf.NewSize(int(queryLength)) + defer buffer.Release() + _, err = buffer.ReadFullFrom(conn, int(queryLength)) + if err != nil { + return err + } + var message mDNS.Msg + err = message.Unpack(buffer.Bytes()) + if err != nil { + return err + } + metadataInQuery := metadata + go func() error { + response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message, adapter.DNSQueryOptions{}) + if err != nil { + conn.Close() + return err + } + responseLength := response.Len() + responseBuffer := buf.NewSize(3 + responseLength) + defer responseBuffer.Release() + responseBuffer.Resize(2, 0) + n, err := response.PackBuffer(responseBuffer.FreeBytes()) + if err != nil { + return err + } + responseBuffer.Truncate(len(n)) + binary.BigEndian.PutUint16(responseBuffer.ExtendHeader(2), uint16(len(n))) + _, err = conn.Write(responseBuffer.Bytes()) + return err + }() + return nil +} + +func NewDNSPacketConnection(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, cachedPackets []*N.PacketBuffer, metadata adapter.InboundContext) error { + metadata.Destination = M.Socksaddr{} + var reader N.PacketReader = conn + var counters []N.CountFunc + cachedPackets = common.Reverse(cachedPackets) + for { + reader, counters = N.UnwrapCountPacketReader(reader, counters) + if cachedReader, isCached := reader.(N.CachedPacketReader); isCached { + packet := cachedReader.ReadCachedPacket() + if packet != nil { + cachedPackets = append(cachedPackets, packet) + continue + } + } + if readWaiter, created := bufio.CreatePacketReadWaiter(reader); created { + readWaiter.InitializeReadWaiter(N.ReadWaitOptions{}) + return newDNSPacketConnection(ctx, router, conn, readWaiter, counters, cachedPackets, metadata) + } + break + } + fastClose, cancel := common.ContextWithCancelCause(ctx) + timeout := canceler.New(fastClose, cancel, C.DNSTimeout) + var group task.Group + group.Append0(func(_ context.Context) error { + for { + var message mDNS.Msg + var destination M.Socksaddr + var err error + if len(cachedPackets) > 0 { + packet := cachedPackets[0] + cachedPackets = cachedPackets[1:] + for _, counter := range counters { + counter(int64(packet.Buffer.Len())) + } + err = message.Unpack(packet.Buffer.Bytes()) + packet.Buffer.Release() + if err != nil { + cancel(err) + return err + } + destination = packet.Destination + } else { + buffer := buf.NewPacket() + destination, err = conn.ReadPacket(buffer) + if err != nil { + buffer.Release() + cancel(err) + return err + } + for _, counter := range counters { + counter(int64(buffer.Len())) + } + err = message.Unpack(buffer.Bytes()) + buffer.Release() + if err != nil { + cancel(err) + return err + } + timeout.Update() + } + metadataInQuery := metadata + go func() error { + response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message, adapter.DNSQueryOptions{}) + if err != nil { + cancel(err) + return err + } + timeout.Update() + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) + if err != nil { + cancel(err) + return err + } + err = conn.WritePacket(responseBuffer, destination) + if err != nil { + cancel(err) + } + return err + }() + } + }) + group.Cleanup(func() { + conn.Close() + }) + return group.Run(fastClose) +} + +func newDNSPacketConnection(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, readWaiter N.PacketReadWaiter, readCounters []N.CountFunc, cached []*N.PacketBuffer, metadata adapter.InboundContext) error { + fastClose, cancel := common.ContextWithCancelCause(ctx) + timeout := canceler.New(fastClose, cancel, C.DNSTimeout) + var group task.Group + group.Append0(func(_ context.Context) error { + for { + var ( + message mDNS.Msg + destination M.Socksaddr + err error + buffer *buf.Buffer + ) + if len(cached) > 0 { + packet := cached[0] + cached = cached[1:] + for _, counter := range readCounters { + counter(int64(packet.Buffer.Len())) + } + err = message.Unpack(packet.Buffer.Bytes()) + packet.Buffer.Release() + destination = packet.Destination + N.PutPacketBuffer(packet) + if err != nil { + cancel(err) + return err + } + } else { + buffer, destination, err = readWaiter.WaitReadPacket() + if err != nil { + cancel(err) + return err + } + for _, counter := range readCounters { + counter(int64(buffer.Len())) + } + err = message.Unpack(buffer.Bytes()) + buffer.Release() + if err != nil { + cancel(err) + return err + } + timeout.Update() + } + metadataInQuery := metadata + go func() error { + response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message, adapter.DNSQueryOptions{}) + if err != nil { + cancel(err) + return err + } + timeout.Update() + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) + if err != nil { + cancel(err) + return err + } + err = conn.WritePacket(responseBuffer, destination) + if err != nil { + cancel(err) + } + return err + }() + } + }) + group.Cleanup(func() { + conn.Close() + }) + return group.Run(fastClose) +} diff --git a/protocol/dns/outbound.go b/protocol/dns/outbound.go new file mode 100644 index 00000000..277d7454 --- /dev/null +++ b/protocol/dns/outbound.go @@ -0,0 +1,63 @@ +package dns + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.StubOptions](registry, C.TypeDNS, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + router adapter.DNSRouter + logger logger.ContextLogger +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) { + return &Outbound{ + Adapter: outbound.NewAdapter(C.TypeDNS, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), + router: service.FromContext[adapter.DNSRouter](ctx), + logger: logger, + }, nil +} + +func (d *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return nil, os.ErrInvalid +} + +func (d *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +func (d *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Destination = M.Socksaddr{} + for { + conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) + err := HandleStreamDNSRequest(ctx, d.router, conn, metadata) + if err != nil { + conn.Close() + if onClose != nil { + onClose(err) + } + return + } + } +} + +func (d *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) +} diff --git a/protocol/group/selector.go b/protocol/group/selector.go new file mode 100644 index 00000000..f3f7377b --- /dev/null +++ b/protocol/group/selector.go @@ -0,0 +1,192 @@ +package group + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/interrupt" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +func RegisterSelector(registry *outbound.Registry) { + outbound.Register[option.SelectorOutboundOptions](registry, C.TypeSelector, NewSelector) +} + +var ( + _ adapter.OutboundGroup = (*Selector)(nil) + _ adapter.ConnectionHandlerEx = (*Selector)(nil) + _ adapter.PacketConnectionHandlerEx = (*Selector)(nil) +) + +type Selector struct { + outbound.Adapter + ctx context.Context + outbound adapter.OutboundManager + connection adapter.ConnectionManager + logger logger.ContextLogger + tags []string + defaultTag string + outbounds map[string]adapter.Outbound + selected common.TypedValue[adapter.Outbound] + interruptGroup *interrupt.Group + interruptExternalConnections bool +} + +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (adapter.Outbound, error) { + outbound := &Selector{ + Adapter: outbound.NewAdapter(C.TypeSelector, tag, nil, options.Outbounds), + ctx: ctx, + outbound: service.FromContext[adapter.OutboundManager](ctx), + connection: service.FromContext[adapter.ConnectionManager](ctx), + logger: logger, + tags: options.Outbounds, + defaultTag: options.Default, + outbounds: make(map[string]adapter.Outbound), + interruptGroup: interrupt.NewGroup(), + interruptExternalConnections: options.InterruptExistConnections, + } + if len(outbound.tags) == 0 { + return nil, E.New("missing tags") + } + return outbound, nil +} + +func (s *Selector) Network() []string { + selected := s.selected.Load() + if selected == nil { + return []string{N.NetworkTCP, N.NetworkUDP} + } + return selected.Network() +} + +func (s *Selector) Start() error { + for i, tag := range s.tags { + detour, loaded := s.outbound.Outbound(tag) + if !loaded { + return E.New("outbound ", i, " not found: ", tag) + } + s.outbounds[tag] = detour + } + + if s.Tag() != "" { + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.Tag()) + if selected != "" { + detour, loaded := s.outbounds[selected] + if loaded { + s.selected.Store(detour) + return nil + } + } + } + } + + if s.defaultTag != "" { + detour, loaded := s.outbounds[s.defaultTag] + if !loaded { + return E.New("default outbound not found: ", s.defaultTag) + } + s.selected.Store(detour) + return nil + } + + s.selected.Store(s.outbounds[s.tags[0]]) + return nil +} + +func (s *Selector) Now() string { + selected := s.selected.Load() + if selected == nil { + return s.tags[0] + } + return selected.Tag() +} + +func (s *Selector) All() []string { + return s.tags +} + +func (s *Selector) SelectOutbound(tag string) bool { + detour, loaded := s.outbounds[tag] + if !loaded { + return false + } + if s.selected.Swap(detour) == detour { + return true + } + if s.Tag() != "" { + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.Tag(), tag) + if err != nil { + s.logger.Error("store selected: ", err) + } + } + } + s.interruptGroup.Interrupt(s.interruptExternalConnections) + return true +} + +func (s *Selector) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + conn, err := s.selected.Load().DialContext(ctx, network, destination) + if err != nil { + return nil, err + } + return s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil +} + +func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + conn, err := s.selected.Load().ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil +} + +func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = interrupt.ContextWithIsExternalConnection(ctx) + selected := s.selected.Load() + if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler { + outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) + } else { + s.connection.NewConnection(ctx, selected, conn, metadata, onClose) + } +} + +func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = interrupt.ContextWithIsExternalConnection(ctx) + selected := s.selected.Load() + if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler { + outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) + } else { + s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose) + } +} + +func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + selected := s.selected.Load() + if !common.Contains(selected.Network(), metadata.Network) { + return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag()) + } + return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout) +} + +func RealTag(detour adapter.Outbound) string { + if group, isGroup := detour.(adapter.OutboundGroup); isGroup { + return group.Now() + } + return detour.Tag() +} diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go new file mode 100644 index 00000000..26967279 --- /dev/null +++ b/protocol/group/urltest.go @@ -0,0 +1,429 @@ +package group + +import ( + "context" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/interrupt" + "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/batch" + 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/x/list" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +func RegisterURLTest(registry *outbound.Registry) { + outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest) +} + +var _ adapter.OutboundGroup = (*URLTest)(nil) + +type URLTest struct { + outbound.Adapter + ctx context.Context + router adapter.Router + outbound adapter.OutboundManager + connection adapter.ConnectionManager + logger log.ContextLogger + tags []string + link string + interval time.Duration + tolerance uint16 + idleTimeout time.Duration + group *URLTestGroup + interruptExternalConnections bool +} + +func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (adapter.Outbound, error) { + outbound := &URLTest{ + Adapter: outbound.NewAdapter(C.TypeURLTest, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.Outbounds), + ctx: ctx, + router: router, + outbound: service.FromContext[adapter.OutboundManager](ctx), + connection: service.FromContext[adapter.ConnectionManager](ctx), + logger: logger, + tags: options.Outbounds, + link: options.URL, + interval: time.Duration(options.Interval), + tolerance: options.Tolerance, + idleTimeout: time.Duration(options.IdleTimeout), + interruptExternalConnections: options.InterruptExistConnections, + } + if len(outbound.tags) == 0 { + return nil, E.New("missing tags") + } + return outbound, nil +} + +func (s *URLTest) Start() error { + outbounds := make([]adapter.Outbound, 0, len(s.tags)) + for i, tag := range s.tags { + detour, loaded := s.outbound.Outbound(tag) + if !loaded { + return E.New("outbound ", i, " not found: ", tag) + } + outbounds = append(outbounds, detour) + } + group, err := NewURLTestGroup(s.ctx, s.outbound, s.logger, outbounds, s.link, s.interval, s.tolerance, s.idleTimeout, s.interruptExternalConnections) + if err != nil { + return err + } + s.group = group + return nil +} + +func (s *URLTest) PostStart() error { + s.group.PostStart() + return nil +} + +func (s *URLTest) Close() error { + return common.Close( + common.PtrOrNil(s.group), + ) +} + +func (s *URLTest) Now() string { + if s.group.selectedOutboundTCP != nil { + return s.group.selectedOutboundTCP.Tag() + } else if s.group.selectedOutboundUDP != nil { + return s.group.selectedOutboundUDP.Tag() + } + return "" +} + +func (s *URLTest) All() []string { + return s.tags +} + +func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) { + return s.group.URLTest(ctx) +} + +func (s *URLTest) CheckOutbounds() { + s.group.CheckOutbounds(true) +} + +func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + s.group.Touch() + var outbound adapter.Outbound + switch N.NetworkName(network) { + case N.NetworkTCP: + outbound = s.group.selectedOutboundTCP + case N.NetworkUDP: + outbound = s.group.selectedOutboundUDP + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } + if outbound == nil { + outbound, _ = s.group.Select(network) + } + if outbound == nil { + return nil, E.New("missing supported outbound") + } + conn, err := outbound.DialContext(ctx, network, destination) + if err == nil { + return s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil + } + s.logger.ErrorContext(ctx, err) + s.group.history.DeleteURLTestHistory(outbound.Tag()) + return nil, err +} + +func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + s.group.Touch() + outbound := s.group.selectedOutboundUDP + if outbound == nil { + outbound, _ = s.group.Select(N.NetworkUDP) + } + if outbound == nil { + return nil, E.New("missing supported outbound") + } + conn, err := outbound.ListenPacket(ctx, destination) + if err == nil { + return s.group.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil + } + s.logger.ErrorContext(ctx, err) + s.group.history.DeleteURLTestHistory(outbound.Tag()) + return nil, err +} + +func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = interrupt.ContextWithIsExternalConnection(ctx) + s.connection.NewConnection(ctx, s, conn, metadata, onClose) +} + +func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = interrupt.ContextWithIsExternalConnection(ctx) + s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) +} + +func (s *URLTest) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + s.group.Touch() + selected := s.group.selectedOutboundTCP + if selected == nil { + selected, _ = s.group.Select(N.NetworkTCP) + } + if selected == nil { + return nil, E.New("missing supported outbound") + } + if !common.Contains(selected.Network(), metadata.Network) { + return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag()) + } + return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout) +} + +type URLTestGroup struct { + ctx context.Context + router adapter.Router + outbound adapter.OutboundManager + pause pause.Manager + pauseCallback *list.Element[pause.Callback] + logger log.Logger + outbounds []adapter.Outbound + link string + interval time.Duration + tolerance uint16 + idleTimeout time.Duration + history adapter.URLTestHistoryStorage + checking atomic.Bool + selectedOutboundTCP adapter.Outbound + selectedOutboundUDP adapter.Outbound + interruptGroup *interrupt.Group + interruptExternalConnections bool + access sync.Mutex + ticker *time.Ticker + close chan struct{} + started bool + lastActive common.TypedValue[time.Time] +} + +func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) { + if interval == 0 { + interval = C.DefaultURLTestInterval + } + if tolerance == 0 { + tolerance = 50 + } + if idleTimeout == 0 { + idleTimeout = C.DefaultURLTestIdleTimeout + } + if interval > idleTimeout { + return nil, E.New("interval must be less or equal than idle_timeout") + } + var history adapter.URLTestHistoryStorage + if historyFromCtx := service.PtrFromContext[urltest.HistoryStorage](ctx); historyFromCtx != nil { + history = historyFromCtx + } else if clashServer := service.FromContext[adapter.ClashServer](ctx); clashServer != nil { + history = clashServer.HistoryStorage() + } else { + history = urltest.NewHistoryStorage() + } + return &URLTestGroup{ + ctx: ctx, + outbound: outboundManager, + logger: logger, + outbounds: outbounds, + link: link, + interval: interval, + tolerance: tolerance, + idleTimeout: idleTimeout, + history: history, + close: make(chan struct{}), + pause: service.FromContext[pause.Manager](ctx), + interruptGroup: interrupt.NewGroup(), + interruptExternalConnections: interruptExternalConnections, + }, nil +} + +func (g *URLTestGroup) PostStart() { + g.access.Lock() + defer g.access.Unlock() + g.started = true + g.lastActive.Store(time.Now()) + go g.CheckOutbounds(false) +} + +func (g *URLTestGroup) Touch() { + if !g.started { + return + } + g.access.Lock() + defer g.access.Unlock() + if g.ticker != nil { + g.lastActive.Store(time.Now()) + return + } + g.ticker = time.NewTicker(g.interval) + go g.loopCheck() + g.pauseCallback = pause.RegisterTicker(g.pause, g.ticker, g.interval, nil) +} + +func (g *URLTestGroup) Close() error { + g.access.Lock() + defer g.access.Unlock() + if g.ticker == nil { + return nil + } + g.ticker.Stop() + g.pause.UnregisterCallback(g.pauseCallback) + close(g.close) + return nil +} + +func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) { + var minDelay uint16 + var minOutbound adapter.Outbound + switch network { + case N.NetworkTCP: + if g.selectedOutboundTCP != nil { + if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundTCP)); history != nil { + minOutbound = g.selectedOutboundTCP + minDelay = history.Delay + } + } + case N.NetworkUDP: + if g.selectedOutboundUDP != nil { + if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundUDP)); history != nil { + minOutbound = g.selectedOutboundUDP + minDelay = history.Delay + } + } + } + for _, detour := range g.outbounds { + if !common.Contains(detour.Network(), network) { + continue + } + history := g.history.LoadURLTestHistory(RealTag(detour)) + if history == nil { + continue + } + if minDelay == 0 || minDelay > history.Delay+g.tolerance { + minDelay = history.Delay + minOutbound = detour + } + } + if minOutbound == nil { + for _, detour := range g.outbounds { + if !common.Contains(detour.Network(), network) { + continue + } + return detour, false + } + return nil, false + } + return minOutbound, true +} + +func (g *URLTestGroup) loopCheck() { + if time.Since(g.lastActive.Load()) > g.interval { + g.lastActive.Store(time.Now()) + g.CheckOutbounds(false) + } + for { + select { + case <-g.close: + return + case <-g.ticker.C: + } + if time.Since(g.lastActive.Load()) > g.idleTimeout { + g.access.Lock() + g.ticker.Stop() + g.ticker = nil + g.pause.UnregisterCallback(g.pauseCallback) + g.pauseCallback = nil + g.access.Unlock() + return + } + g.CheckOutbounds(false) + } +} + +func (g *URLTestGroup) CheckOutbounds(force bool) { + _, _ = g.urlTest(g.ctx, force) +} + +func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) { + return g.urlTest(ctx, false) +} + +func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) { + result := make(map[string]uint16) + if g.checking.Swap(true) { + return result, nil + } + defer g.checking.Store(false) + b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) + checked := make(map[string]bool) + var resultAccess sync.Mutex + for _, detour := range g.outbounds { + tag := detour.Tag() + realTag := RealTag(detour) + if checked[realTag] { + continue + } + history := g.history.LoadURLTestHistory(realTag) + if !force && history != nil && time.Since(history.Time) < g.interval { + continue + } + checked[realTag] = true + p, loaded := g.outbound.Outbound(realTag) + if !loaded { + continue + } + b.Go(realTag, func() (any, error) { + testCtx, cancel := context.WithTimeout(g.ctx, C.TCPTimeout) + defer cancel() + t, err := urltest.URLTest(testCtx, g.link, p) + if err != nil { + g.logger.Debug("outbound ", tag, " unavailable: ", err) + g.history.DeleteURLTestHistory(realTag) + } else { + g.logger.Debug("outbound ", tag, " available: ", t, "ms") + g.history.StoreURLTestHistory(realTag, &adapter.URLTestHistory{ + Time: time.Now(), + Delay: t, + }) + resultAccess.Lock() + result[tag] = t + resultAccess.Unlock() + } + return nil, nil + }) + } + b.Wait() + g.performUpdateCheck() + return result, nil +} + +func (g *URLTestGroup) performUpdateCheck() { + var updated bool + if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) { + if g.selectedOutboundTCP != nil { + updated = true + } + g.selectedOutboundTCP = outbound + } + if outbound, exists := g.Select(N.NetworkUDP); outbound != nil && (g.selectedOutboundUDP == nil || (exists && outbound != g.selectedOutboundUDP)) { + if g.selectedOutboundUDP != nil { + updated = true + } + g.selectedOutboundUDP = outbound + } + if updated { + g.interruptGroup.Interrupt(g.interruptExternalConnections) + } +} diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go new file mode 100644 index 00000000..e8a9a3da --- /dev/null +++ b/protocol/http/inbound.go @@ -0,0 +1,132 @@ +package http + +import ( + std_bufio "bufio" + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/http" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeHTTP, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + authenticator *auth.Authenticator + tlsConfig tls.ServerConfig +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeHTTP, tag), + router: uot.NewRouter(router, logger), + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } + if options.TLS != nil { + tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ + Context: ctx, + Logger: logger, + Options: common.PtrValueOrDefault(options.TLS), + KTLSCompatible: true, + }) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + SetSystemProxy: options.SetSystemProxy, + SystemProxySOCKS: false, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return common.Close( + h.listener, + h.tlsConfig, + ) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if h.tlsConfig != nil { + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return + } + conn = tlsConn + } + err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/http/outbound.go b/protocol/http/outbound.go new file mode 100644 index 00000000..48c4be6b --- /dev/null +++ b/protocol/http/outbound.go @@ -0,0 +1,65 @@ +package http + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHTTP "github.com/sagernet/sing/protocol/http" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.HTTPOutboundOptions](registry, C.TypeHTTP, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + client *sHTTP.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) { + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + detour, err := tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, tag, []string{N.NetworkTCP}, options.DialerOptions), + logger: logger, + client: sHTTP.NewClient(sHTTP.Options{ + Dialer: detour, + Server: options.ServerOptions.Build(), + Username: options.Username, + Password: options.Password, + Path: options.Path, + Headers: options.Headers.Build(), + }), + }, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialContext(ctx, network, destination) +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} diff --git a/protocol/hysteria/inbound.go b/protocol/hysteria/inbound.go new file mode 100644 index 00000000..98d7cb81 --- /dev/null +++ b/protocol/hysteria/inbound.go @@ -0,0 +1,180 @@ +package hysteria + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic/hysteria" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, NewInbound) +} + +type Inbound struct { + inbound.Adapter + router adapter.Router + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + service *hysteria.Service[int] + userNameList []string +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { + options.UDPFragmentDefault = true + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeHysteria, tag), + router: router, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), + tlsConfig: tlsConfig, + } + var sendBps, receiveBps uint64 + if options.Up.Value() > 0 { + sendBps = options.Up.Value() + } else { + sendBps = uint64(options.UpMbps) * hysteria.MbpsToBps + } + if options.Down.Value() > 0 { + receiveBps = options.Down.Value() + } else { + receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + service, err := hysteria.NewService[int](hysteria.ServiceOptions{ + Context: ctx, + Logger: logger, + SendBPS: sendBps, + ReceiveBPS: receiveBps, + XPlusPassword: options.Obfs, + TLSConfig: tlsConfig, + UDPTimeout: udpTimeout, + Handler: inbound, + + // Legacy options + + ConnReceiveWindow: options.ReceiveWindowConn, + StreamReceiveWindow: options.ReceiveWindowClient, + MaxIncomingStreams: int64(options.MaxConnClient), + DisableMTUDiscovery: options.DisableMTUDiscovery, + }) + if err != nil { + return nil, err + } + userList := make([]int, 0, len(options.Users)) + userNameList := make([]string, 0, len(options.Users)) + userPasswordList := make([]string, 0, len(options.Users)) + for index, user := range options.Users { + userList = append(userList, index) + userNameList = append(userNameList, user.Name) + var password string + if user.AuthString != "" { + password = user.AuthString + } else { + password = string(user.Auth) + } + userPasswordList = append(userPasswordList, password) + } + service.UpdateUsers(userList, userPasswordList) + inbound.service = service + inbound.userNameList = userNameList + return inbound, nil +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + userID, _ := auth.UserFromContext[int](ctx) + if userName := h.userNameList[userID]; userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + userID, _ := auth.UserFromContext[int](ctx) + if userName := h.userNameList[userID]; userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } + } + packetConn, err := h.listener.ListenUDP() + if err != nil { + return err + } + return h.service.Start(packetConn) +} + +func (h *Inbound) Close() error { + return common.Close( + h.listener, + h.tlsConfig, + common.PtrOrNil(h.service), + ) +} diff --git a/protocol/hysteria/outbound.go b/protocol/hysteria/outbound.go new file mode 100644 index 00000000..bcadd878 --- /dev/null +++ b/protocol/hysteria/outbound.go @@ -0,0 +1,126 @@ +package hysteria + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/tuic" + "github.com/sagernet/sing-quic/hysteria" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, NewOutbound) +} + +var ( + _ adapter.Outbound = (*tuic.Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) +) + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + client *hysteria.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { + options.UDPFragmentDefault = true + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + networkList := options.Network.Build() + var password string + if options.AuthString != "" { + password = options.AuthString + } else { + password = string(options.Auth) + } + var sendBps, receiveBps uint64 + if options.Up.Value() > 0 { + sendBps = options.Up.Value() + } else { + sendBps = uint64(options.UpMbps) * hysteria.MbpsToBps + } + if options.Down.Value() > 0 { + receiveBps = options.Down.Value() + } else { + receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps + } + client, err := hysteria.NewClient(hysteria.ClientOptions{ + Context: ctx, + Dialer: outboundDialer, + Logger: logger, + ServerAddress: options.ServerOptions.Build(), + ServerPorts: options.ServerPorts, + HopInterval: time.Duration(options.HopInterval), + SendBPS: sendBps, + ReceiveBPS: receiveBps, + XPlusPassword: options.Obfs, + Password: password, + TLSConfig: tlsConfig, + UDPDisabled: !common.Contains(networkList, N.NetworkUDP), + ConnReceiveWindow: options.ReceiveWindowConn, + StreamReceiveWindow: options.ReceiveWindow, + DisableMTUDiscovery: options.DisableMTUDiscovery, + }) + if err != nil { + return nil, err + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria, tag, networkList, options.DialerOptions), + logger: logger, + client: client, + }, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialConn(ctx, destination) + case N.NetworkUDP: + conn, err := h.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(conn, destination), nil + default: + return nil, E.New("unsupported network: ", network) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return h.client.ListenPacket(ctx, destination) +} + +func (h *Outbound) InterfaceUpdated() { + h.client.CloseWithError(E.New("network changed")) +} + +func (h *Outbound) Close() error { + return h.client.CloseWithError(os.ErrClosed) +} diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go new file mode 100644 index 00000000..5fe8848d --- /dev/null +++ b/protocol/hysteria2/inbound.go @@ -0,0 +1,214 @@ +package hysteria2 + +import ( + "context" + "net" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic/hysteria" + "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, NewInbound) +} + +type Inbound struct { + inbound.Adapter + router adapter.Router + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + service *hysteria2.Service[int] + userNameList []string +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { + options.UDPFragmentDefault = true + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + var salamanderPassword string + if options.Obfs != nil { + if options.Obfs.Password == "" { + return nil, E.New("missing obfs password") + } + switch options.Obfs.Type { + case hysteria2.ObfsTypeSalamander: + salamanderPassword = options.Obfs.Password + default: + return nil, E.New("unknown obfs type: ", options.Obfs.Type) + } + } + var masqueradeHandler http.Handler + if options.Masquerade != nil && options.Masquerade.Type != "" { + switch options.Masquerade.Type { + case C.Hysterai2MasqueradeTypeFile: + masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.FileOptions.Directory)) + case C.Hysterai2MasqueradeTypeProxy: + masqueradeURL, err := url.Parse(options.Masquerade.ProxyOptions.URL) + if err != nil { + return nil, E.Cause(err, "parse masquerade URL") + } + masqueradeHandler = &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(masqueradeURL) + if !options.Masquerade.ProxyOptions.RewriteHost { + r.Out.Host = r.In.Host + } + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(http.StatusBadGateway) + }, + } + case C.Hysterai2MasqueradeTypeString: + masqueradeHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if options.Masquerade.StringOptions.StatusCode != 0 { + w.WriteHeader(options.Masquerade.StringOptions.StatusCode) + } + for key, values := range options.Masquerade.StringOptions.Headers { + for _, value := range values { + w.Header().Add(key, value) + } + } + w.Write([]byte(options.Masquerade.StringOptions.Content)) + }) + default: + return nil, E.New("unknown masquerade type: ", options.Masquerade.Type) + } + } + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeHysteria2, tag), + router: router, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), + tlsConfig: tlsConfig, + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ + Context: ctx, + Logger: logger, + BrutalDebug: options.BrutalDebug, + SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), + ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), + SalamanderPassword: salamanderPassword, + TLSConfig: tlsConfig, + IgnoreClientBandwidth: options.IgnoreClientBandwidth, + UDPTimeout: udpTimeout, + Handler: inbound, + MasqueradeHandler: masqueradeHandler, + BBRProfile: options.BBRProfile, + }) + if err != nil { + return nil, err + } + userList := make([]int, 0, len(options.Users)) + userNameList := make([]string, 0, len(options.Users)) + userPasswordList := make([]string, 0, len(options.Users)) + for index, user := range options.Users { + userList = append(userList, index) + userNameList = append(userNameList, user.Name) + userPasswordList = append(userPasswordList, user.Password) + } + service.UpdateUsers(userList, userPasswordList) + inbound.service = service + inbound.userNameList = userNameList + return inbound, nil +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + userID, _ := auth.UserFromContext[int](ctx) + if userName := h.userNameList[userID]; userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + userID, _ := auth.UserFromContext[int](ctx) + if userName := h.userNameList[userID]; userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } + } + packetConn, err := h.listener.ListenUDP() + if err != nil { + return err + } + return h.service.Start(packetConn) +} + +func (h *Inbound) Close() error { + return common.Close( + h.listener, + h.tlsConfig, + common.PtrOrNil(h.service), + ) +} diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go new file mode 100644 index 00000000..4a0c9f24 --- /dev/null +++ b/protocol/hysteria2/outbound.go @@ -0,0 +1,122 @@ +package hysteria2 + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/tuic" + "github.com/sagernet/sing-quic/hysteria" + "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, NewOutbound) +} + +var ( + _ adapter.Outbound = (*tuic.Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) +) + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + client *hysteria2.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { + options.UDPFragmentDefault = true + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + var salamanderPassword string + if options.Obfs != nil { + if options.Obfs.Password == "" { + return nil, E.New("missing obfs password") + } + switch options.Obfs.Type { + case hysteria2.ObfsTypeSalamander: + salamanderPassword = options.Obfs.Password + default: + return nil, E.New("unknown obfs type: ", options.Obfs.Type) + } + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + networkList := options.Network.Build() + client, err := hysteria2.NewClient(hysteria2.ClientOptions{ + Context: ctx, + Dialer: outboundDialer, + Logger: logger, + BrutalDebug: options.BrutalDebug, + ServerAddress: options.ServerOptions.Build(), + ServerPorts: options.ServerPorts, + HopInterval: time.Duration(options.HopInterval), + HopIntervalMax: time.Duration(options.HopIntervalMax), + SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), + ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), + SalamanderPassword: salamanderPassword, + Password: options.Password, + TLSConfig: tlsConfig, + UDPDisabled: !common.Contains(networkList, N.NetworkUDP), + BBRProfile: options.BBRProfile, + }) + if err != nil { + return nil, err + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, tag, networkList, options.DialerOptions), + logger: logger, + client: client, + }, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialConn(ctx, destination) + case N.NetworkUDP: + conn, err := h.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(conn, destination), nil + default: + return nil, E.New("unsupported network: ", network) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return h.client.ListenPacket(ctx) +} + +func (h *Outbound) InterfaceUpdated() { + h.client.CloseWithError(E.New("network changed")) +} + +func (h *Outbound) Close() error { + return h.client.CloseWithError(os.ErrClosed) +} diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go new file mode 100644 index 00000000..64c3edb5 --- /dev/null +++ b/protocol/mixed/inbound.go @@ -0,0 +1,168 @@ +package mixed + +import ( + std_bufio "bufio" + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/protocol/socks/socks4" + "github.com/sagernet/sing/protocol/socks/socks5" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeMixed, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + authenticator *auth.Authenticator + tlsConfig tls.ServerConfig + udpTimeout time.Duration +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeMixed, tag), + router: uot.NewRouter(router, logger), + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + udpTimeout: udpTimeout, + } + if options.TLS != nil { + tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ + Context: ctx, + Logger: logger, + Options: common.PtrValueOrDefault(options.TLS), + KTLSCompatible: true, + }) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + SetSystemProxy: options.SetSystemProxy, + SystemProxySOCKS: true, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return common.Close( + h.listener, + h.tlsConfig, + ) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.newConnection(ctx, conn, metadata, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } +} + +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + if h.tlsConfig != nil { + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + return E.Cause(err, "TLS handshake") + } + conn = tlsConn + } + reader := std_bufio.NewReader(conn) + headerBytes, err := reader.Peek(1) + if err != nil { + return E.Cause(err, "peek first byte") + } + switch headerBytes[0] { + case socks4.Version, socks5.Version: + return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) + default: + return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + } +} + +func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "inbound packet connection") + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go new file mode 100644 index 00000000..5613f196 --- /dev/null +++ b/protocol/naive/inbound.go @@ -0,0 +1,251 @@ +package naive + +import ( + "context" + "errors" + "io" + "net" + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHttp "github.com/sagernet/sing/protocol/http" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +var ( + ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) + WrapError func(error) error +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound) +} + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + options option.NaiveInboundOptions + listener *listener.Listener + network []string + networkIsDefault bool + authenticator *auth.Authenticator + tlsConfig tls.ServerConfig + httpServer *http.Server + h3Server io.Closer +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeNaive, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), + networkIsDefault: options.Network == "", + network: options.Network.Build(), + authenticator: auth.NewAuthenticator(options.Users), + } + if common.Contains(inbound.network, N.NetworkUDP) { + if options.TLS == nil || !options.TLS.Enabled { + return nil, E.New("TLS is required for QUIC server") + } + } + if len(options.Users) == 0 { + return nil, E.New("missing users") + } + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + return inbound, nil +} + +func (n *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if n.tlsConfig != nil { + err := n.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + if common.Contains(n.network, N.NetworkTCP) { + tcpListener, err := n.listener.ListenTCP() + if err != nil { + return err + } + n.httpServer = &http.Server{ + Handler: h2c.NewHandler(n, &http2.Server{}), + BaseContext: func(listener net.Listener) context.Context { + return n.ctx + }, + } + go func() { + listener := net.Listener(tcpListener) + if n.tlsConfig != nil { + if len(n.tlsConfig.NextProtos()) == 0 { + n.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) + } else if !common.Contains(n.tlsConfig.NextProtos(), http2.NextProtoTLS) { + n.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, n.tlsConfig.NextProtos()...)) + } + listener = aTLS.NewListener(tcpListener, n.tlsConfig) + } + sErr := n.httpServer.Serve(listener) + if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) { + n.logger.Error("http server serve error: ", sErr) + } + }() + } + + if common.Contains(n.network, N.NetworkUDP) { + http3Server, err := ConfigureHTTP3ListenerFunc(n.ctx, n.logger, n.listener, n, n.tlsConfig, n.options) + if err == nil { + n.h3Server = http3Server + } else if len(n.network) > 1 { + n.logger.Warn(E.Cause(err, "naive http3 disabled")) + } else { + return err + } + } + + return nil +} + +func (n *Inbound) Close() error { + return common.Close( + &n.listener, + common.PtrOrNil(n.httpServer), + n.h3Server, + n.tlsConfig, + ) +} + +func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + ctx := log.ContextWithNewID(request.Context()) + if request.Method != "CONNECT" { + rejectHTTP(writer, http.StatusBadRequest) + n.badRequest(ctx, request, E.New("not CONNECT request")) + return + } else if request.Header.Get("Padding") == "" { + rejectHTTP(writer, http.StatusBadRequest) + n.badRequest(ctx, request, E.New("missing naive padding")) + return + } + userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization")) + if authOk { + authOk = n.authenticator.Verify(userName, password) + } + if !authOk { + rejectHTTP(writer, http.StatusProxyAuthRequired) + n.badRequest(ctx, request, E.New("authorization failed")) + return + } + writer.Header().Set("Padding", generatePaddingHeader()) + writer.WriteHeader(http.StatusOK) + writer.(http.Flusher).Flush() + + hostPort := request.Header.Get("-connect-authority") + if hostPort == "" { + hostPort = request.URL.Host + if hostPort == "" { + hostPort = request.Host + } + } + source := sHttp.SourceAddress(request) + destination := M.ParseSocksaddr(hostPort).Unwrap() + + if hijacker, isHijacker := writer.(http.Hijacker); isHijacker { + conn, _, err := hijacker.Hijack() + if err != nil { + n.badRequest(ctx, request, E.New("hijack failed")) + return + } + n.newConnection(ctx, false, &naiveConn{Conn: conn}, userName, source, destination) + } else { + n.newConnection(ctx, true, &naiveH2Conn{ + reader: request.Body, + writer: writer, + flusher: writer.(http.Flusher), + remoteAddress: source, + }, userName, source, destination) + } +} + +func (n *Inbound) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) { + if userName != "" { + n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source) + n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination) + } else { + n.logger.InfoContext(ctx, "inbound connection from ", source) + n.logger.InfoContext(ctx, "inbound connection to ", destination) + } + var metadata adapter.InboundContext + metadata.Inbound = n.Tag() + metadata.InboundType = n.Type() + //nolint:staticcheck + metadata.InboundDetour = n.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.Source = source + metadata.Destination = destination + metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + metadata.User = userName + if !waitForClose { + n.router.RouteConnectionEx(ctx, conn, metadata, nil) + } else { + done := make(chan struct{}) + wrapper := v2rayhttp.NewHTTP2Wrapper(conn) + n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) { + close(done) + })) + <-done + wrapper.CloseWrapper() + } +} + +func (n *Inbound) badRequest(ctx context.Context, request *http.Request, err error) { + n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func rejectHTTP(writer http.ResponseWriter, statusCode int) { + hijacker, ok := writer.(http.Hijacker) + if !ok { + writer.WriteHeader(statusCode) + return + } + conn, _, err := hijacker.Hijack() + if err != nil { + writer.WriteHeader(statusCode) + return + } + if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP { + tcpConn.SetLinger(0) + } + conn.Close() +} diff --git a/protocol/naive/inbound_conn.go b/protocol/naive/inbound_conn.go new file mode 100644 index 00000000..8cc3ded2 --- /dev/null +++ b/protocol/naive/inbound_conn.go @@ -0,0 +1,257 @@ +package naive + +import ( + "encoding/binary" + "io" + "math/rand" + "net" + "net/http" + "os" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/baderror" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/rw" +) + +const paddingCount = 8 + +func generatePaddingHeader() string { + paddingLen := rand.Intn(32) + 30 + padding := make([]byte, paddingLen) + bits := rand.Uint64() + for i := 0; i < 16; i++ { + padding[i] = "!#$()+<>?@[]^`{}"[bits&15] + bits >>= 4 + } + for i := 16; i < paddingLen; i++ { + padding[i] = '~' + } + return string(padding) +} + +type paddingConn struct { + readPadding int + writePadding int + readRemaining int + paddingRemaining int +} + +func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) { + if p.readRemaining > 0 { + if len(buffer) > p.readRemaining { + buffer = buffer[:p.readRemaining] + } + n, err = reader.Read(buffer) + if err != nil { + return + } + p.readRemaining -= n + return + } + if p.paddingRemaining > 0 { + err = rw.SkipN(reader, p.paddingRemaining) + if err != nil { + return + } + p.paddingRemaining = 0 + } + if p.readPadding < paddingCount { + var paddingHeader []byte + if len(buffer) >= 3 { + paddingHeader = buffer[:3] + } else { + paddingHeader = make([]byte, 3) + } + _, err = io.ReadFull(reader, paddingHeader) + if err != nil { + return + } + originalDataSize := int(binary.BigEndian.Uint16(paddingHeader[:2])) + paddingSize := int(paddingHeader[2]) + if len(buffer) > originalDataSize { + buffer = buffer[:originalDataSize] + } + n, err = reader.Read(buffer) + if err != nil { + return + } + p.readPadding++ + p.readRemaining = originalDataSize - n + p.paddingRemaining = paddingSize + return + } + return reader.Read(buffer) +} + +func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) { + if p.writePadding < paddingCount { + paddingSize := rand.Intn(256) + buffer := buf.NewSize(3 + len(data) + paddingSize) + defer buffer.Release() + header := buffer.Extend(3) + binary.BigEndian.PutUint16(header, uint16(len(data))) + header[2] = byte(paddingSize) + common.Must1(buffer.Write(data)) + common.Must(buffer.WriteZeroN(paddingSize)) + _, err = writer.Write(buffer.Bytes()) + if err == nil { + n = len(data) + } + p.writePadding++ + return + } + return writer.Write(data) +} + +func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffer) error { + if p.writePadding < paddingCount { + bufferLen := buffer.Len() + if bufferLen > 65535 { + _, err := p.writeChunked(writer, buffer.Bytes()) + return err + } + paddingSize := rand.Intn(256) + header := buffer.ExtendHeader(3) + binary.BigEndian.PutUint16(header, uint16(bufferLen)) + header[2] = byte(paddingSize) + common.Must(buffer.WriteZeroN(paddingSize)) + p.writePadding++ + } + return common.Error(writer.Write(buffer.Bytes())) +} + +func (p *paddingConn) writeChunked(writer io.Writer, data []byte) (n int, err error) { + for len(data) > 0 { + var chunk []byte + if len(data) > 65535 { + chunk = data[:65535] + data = data[65535:] + } else { + chunk = data + data = nil + } + var written int + written, err = p.writeWithPadding(writer, chunk) + n += written + if err != nil { + return + } + } + return +} + +func (p *paddingConn) frontHeadroom() int { + if p.writePadding < paddingCount { + return 3 + } + return 0 +} + +func (p *paddingConn) rearHeadroom() int { + if p.writePadding < paddingCount { + return 255 + } + return 0 +} + +func (p *paddingConn) writerMTU() int { + if p.writePadding < paddingCount { + return 65535 + } + return 0 +} + +func (p *paddingConn) readerReplaceable() bool { + return p.readPadding == paddingCount +} + +func (p *paddingConn) writerReplaceable() bool { + return p.writePadding == paddingCount +} + +type naiveConn struct { + net.Conn + paddingConn +} + +func (c *naiveConn) Read(p []byte) (n int, err error) { + n, err = c.readWithPadding(c.Conn, p) + return n, wrapError(err) +} + +func (c *naiveConn) Write(p []byte) (n int, err error) { + n, err = c.writeChunked(c.Conn, p) + return n, wrapError(err) +} + +func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + err := c.writeBufferWithPadding(c.Conn, buffer) + return wrapError(err) +} + +func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() } +func (c *naiveConn) RearHeadroom() int { return c.rearHeadroom() } +func (c *naiveConn) WriterMTU() int { return c.writerMTU() } +func (c *naiveConn) Upstream() any { return c.Conn } +func (c *naiveConn) ReaderReplaceable() bool { return c.readerReplaceable() } +func (c *naiveConn) WriterReplaceable() bool { return c.writerReplaceable() } + +type naiveH2Conn struct { + reader io.Reader + writer io.Writer + flusher http.Flusher + remoteAddress net.Addr + paddingConn +} + +func (c *naiveH2Conn) Read(p []byte) (n int, err error) { + n, err = c.readWithPadding(c.reader, p) + return n, wrapError(err) +} + +func (c *naiveH2Conn) Write(p []byte) (n int, err error) { + n, err = c.writeChunked(c.writer, p) + if err == nil { + c.flusher.Flush() + } + return n, wrapError(err) +} + +func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + err := c.writeBufferWithPadding(c.writer, buffer) + if err == nil { + c.flusher.Flush() + } + return wrapError(err) +} + +func wrapError(err error) error { + err = baderror.WrapH2(err) + if WrapError != nil { + err = WrapError(err) + } + return err +} + +func (c *naiveH2Conn) Close() error { + return common.Close(c.reader, c.writer) +} + +func (c *naiveH2Conn) LocalAddr() net.Addr { return M.Socksaddr{} } +func (c *naiveH2Conn) RemoteAddr() net.Addr { return c.remoteAddress } +func (c *naiveH2Conn) SetDeadline(t time.Time) error { return os.ErrInvalid } +func (c *naiveH2Conn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } +func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } +func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool { return true } +func (c *naiveH2Conn) UpstreamReader() any { return c.reader } +func (c *naiveH2Conn) UpstreamWriter() any { return c.writer } +func (c *naiveH2Conn) FrontHeadroom() int { return c.frontHeadroom() } +func (c *naiveH2Conn) RearHeadroom() int { return c.rearHeadroom() } +func (c *naiveH2Conn) WriterMTU() int { return c.writerMTU() } +func (c *naiveH2Conn) ReaderReplaceable() bool { return c.readerReplaceable() } +func (c *naiveH2Conn) WriterReplaceable() bool { return c.writerReplaceable() } diff --git a/protocol/naive/outbound.go b/protocol/naive/outbound.go new file mode 100644 index 00000000..8249a1fe --- /dev/null +++ b/protocol/naive/outbound.go @@ -0,0 +1,275 @@ +//go:build with_naive_outbound + +package naive + +import ( + "context" + "encoding/pem" + "net" + "os" + "strings" + + "github.com/sagernet/cronet-go" + _ "github.com/sagernet/cronet-go/all" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.NaiveOutboundOptions](registry, C.TypeNaive, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + ctx context.Context + logger logger.ContextLogger + client *cronet.NaiveClient + uotClient *uot.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveOutboundOptions) (adapter.Outbound, error) { + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + if options.TLS.DisableSNI { + return nil, E.New("disable_sni is not supported on naive outbound") + } + if options.TLS.Insecure { + return nil, E.New("insecure is not supported on naive outbound") + } + if len(options.TLS.ALPN) > 0 { + return nil, E.New("alpn is not supported on naive outbound") + } + if options.TLS.MinVersion != "" { + return nil, E.New("min_version is not supported on naive outbound") + } + if options.TLS.MaxVersion != "" { + return nil, E.New("max_version is not supported on naive outbound") + } + if len(options.TLS.CipherSuites) > 0 { + return nil, E.New("cipher_suites is not supported on naive outbound") + } + if len(options.TLS.CurvePreferences) > 0 { + return nil, E.New("curve_preferences is not supported on naive outbound") + } + if len(options.TLS.ClientCertificate) > 0 || options.TLS.ClientCertificatePath != "" { + return nil, E.New("client_certificate is not supported on naive outbound") + } + if len(options.TLS.ClientKey) > 0 || options.TLS.ClientKeyPath != "" { + return nil, E.New("client_key is not supported on naive outbound") + } + if options.TLS.Fragment || options.TLS.RecordFragment { + return nil, E.New("fragment is not supported on naive outbound") + } + if options.TLS.KernelTx || options.TLS.KernelRx { + return nil, E.New("kernel TLS is not supported on naive outbound") + } + if options.TLS.UTLS != nil && options.TLS.UTLS.Enabled { + return nil, E.New("uTLS is not supported on naive outbound") + } + if options.TLS.Reality != nil && options.TLS.Reality.Enabled { + return nil, E.New("reality is not supported on naive outbound") + } + + serverAddress := options.ServerOptions.Build() + + var serverName string + if options.TLS.ServerName != "" { + serverName = options.TLS.ServerName + } else { + serverName = serverAddress.AddrString() + } + + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: true, + ResolverOnDetour: true, + NewDialer: true, + }) + if err != nil { + return nil, err + } + + var trustedRootCertificates string + if len(options.TLS.Certificate) > 0 { + trustedRootCertificates = strings.Join(options.TLS.Certificate, "\n") + } else if options.TLS.CertificatePath != "" { + content, err := os.ReadFile(options.TLS.CertificatePath) + if err != nil { + return nil, E.Cause(err, "read certificate") + } + trustedRootCertificates = string(content) + } + + extraHeaders := make(map[string]string) + for key, values := range options.ExtraHeaders.Build() { + if len(values) > 0 { + extraHeaders[key] = values[0] + } + } + + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + var dnsResolver cronet.DNSResolverFunc + if dnsRouter != nil { + dnsResolver = func(dnsContext context.Context, request *mDNS.Msg) *mDNS.Msg { + response, err := dnsRouter.Exchange(dnsContext, request, outboundDialer.(dialer.ResolveDialer).QueryOptions()) + if err != nil { + logger.Error("DNS exchange failed: ", err) + return dns.FixedResponseStatus(request, mDNS.RcodeServerFailure) + } + return response + } + } + + var echEnabled bool + var echConfigList []byte + var echQueryServerName string + if options.TLS.ECH != nil && options.TLS.ECH.Enabled { + echEnabled = true + echQueryServerName = options.TLS.ECH.QueryServerName + var echConfig []byte + if len(options.TLS.ECH.Config) > 0 { + echConfig = []byte(strings.Join(options.TLS.ECH.Config, "\n")) + } else if options.TLS.ECH.ConfigPath != "" { + content, err := os.ReadFile(options.TLS.ECH.ConfigPath) + if err != nil { + return nil, E.Cause(err, "read ECH config") + } + echConfig = content + } + if len(echConfig) > 0 { + block, rest := pem.Decode(echConfig) + if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { + return nil, E.New("invalid ECH configs pem") + } + echConfigList = block.Bytes + } + } + var quicCongestionControl cronet.QUICCongestionControl + switch options.QUICCongestionControl { + case "": + quicCongestionControl = cronet.QUICCongestionControlDefault + case "bbr": + quicCongestionControl = cronet.QUICCongestionControlBBR + case "bbr2": + quicCongestionControl = cronet.QUICCongestionControlBBRv2 + case "cubic": + quicCongestionControl = cronet.QUICCongestionControlCubic + case "reno": + quicCongestionControl = cronet.QUICCongestionControlReno + default: + return nil, E.New("unknown quic congestion control: ", options.QUICCongestionControl) + } + client, err := cronet.NewNaiveClient(cronet.NaiveClientOptions{ + Context: ctx, + Logger: logger, + ServerAddress: serverAddress, + ServerName: serverName, + Username: options.Username, + Password: options.Password, + InsecureConcurrency: options.InsecureConcurrency, + ExtraHeaders: extraHeaders, + TrustedRootCertificates: trustedRootCertificates, + Dialer: outboundDialer, + DNSResolver: dnsResolver, + ECHEnabled: echEnabled, + ECHConfigList: echConfigList, + ECHQueryServerName: echQueryServerName, + QUIC: options.QUIC, + QUICCongestionControl: quicCongestionControl, + }) + if err != nil { + return nil, err + } + var uotClient *uot.Client + uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) + if uotOptions.Enabled { + uotClient = &uot.Client{ + Dialer: &naiveDialer{client}, + Version: uotOptions.Version, + } + } + var networks []string + if uotClient != nil { + networks = []string{N.NetworkTCP, N.NetworkUDP} + } else { + networks = []string{N.NetworkTCP} + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeNaive, tag, networks, options.DialerOptions), + ctx: ctx, + logger: logger, + client: client, + uotClient: uotClient, + }, nil +} + +func (h *Outbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + err := h.client.Start() + if err != nil { + return err + } + h.logger.Info("NaiveProxy started, version: ", h.client.Engine().Version()) + return nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialEarly(ctx, destination) + case N.NetworkUDP: + if h.uotClient == nil { + return nil, E.New("UDP is not supported unless UDP over TCP is enabled") + } + h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) + return h.uotClient.DialContext(ctx, network, destination) + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if h.uotClient == nil { + return nil, E.New("UDP is not supported unless UDP over TCP is enabled") + } + return h.uotClient.ListenPacket(ctx, destination) +} + +func (h *Outbound) InterfaceUpdated() { + h.client.Engine().CloseAllConnections() +} + +func (h *Outbound) Close() error { + return h.client.Close() +} + +func (h *Outbound) Client() *cronet.NaiveClient { + return h.client +} + +type naiveDialer struct { + *cronet.NaiveClient +} + +func (d *naiveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d.NaiveClient.DialEarly(ctx, destination) +} diff --git a/protocol/naive/quic/inbound_init.go b/protocol/naive/quic/inbound_init.go new file mode 100644 index 00000000..1f868267 --- /dev/null +++ b/protocol/naive/quic/inbound_init.go @@ -0,0 +1,128 @@ +package quic + +import ( + "context" + "io" + "net/http" + "time" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/congestion" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/naive" + "github.com/sagernet/sing-quic" + "github.com/sagernet/sing-quic/congestion_bbr1" + "github.com/sagernet/sing-quic/congestion_bbr2" + congestion_meta1 "github.com/sagernet/sing-quic/congestion_meta1" + congestion_meta2 "github.com/sagernet/sing-quic/congestion_meta2" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/ntp" +) + +func init() { + naive.ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) { + err := qtls.ConfigureHTTP3(tlsConfig) + if err != nil { + return nil, err + } + + udpConn, err := listener.ListenUDP() + if err != nil { + return nil, err + } + + var congestionControl func(conn *quic.Conn) congestion.CongestionControl + timeFunc := ntp.TimeFuncFromContext(ctx) + if timeFunc == nil { + timeFunc = time.Now + } + switch options.QUICCongestionControl { + case "", "bbr": + congestionControl = func(conn *quic.Conn) congestion.CongestionControl { + return congestion_meta2.NewBbrSender( + congestion_meta2.DefaultClock{TimeFunc: timeFunc}, + congestion.ByteCount(conn.Config().InitialPacketSize), + congestion.ByteCount(congestion_meta1.InitialCongestionWindow), + ) + } + case "bbr_standard": + congestionControl = func(conn *quic.Conn) congestion.CongestionControl { + return congestion_bbr1.NewBbrSender( + congestion_bbr1.DefaultClock{TimeFunc: timeFunc}, + congestion.ByteCount(conn.Config().InitialPacketSize), + congestion_bbr1.InitialCongestionWindowPackets, + congestion_bbr1.MaxCongestionWindowPackets, + ) + } + case "bbr2": + congestionControl = func(conn *quic.Conn) congestion.CongestionControl { + return congestion_bbr2.NewBBR2Sender( + congestion_bbr2.DefaultClock{TimeFunc: timeFunc}, + congestion.ByteCount(conn.Config().InitialPacketSize), + 0, + false, + ) + } + case "bbr2_variant": + congestionControl = func(conn *quic.Conn) congestion.CongestionControl { + return congestion_bbr2.NewBBR2Sender( + congestion_bbr2.DefaultClock{TimeFunc: timeFunc}, + congestion.ByteCount(conn.Config().InitialPacketSize), + 32*congestion.ByteCount(conn.Config().InitialPacketSize), + true, + ) + } + case "cubic": + congestionControl = func(conn *quic.Conn) congestion.CongestionControl { + return congestion_meta1.NewCubicSender( + congestion_meta1.DefaultClock{TimeFunc: timeFunc}, + congestion.ByteCount(conn.Config().InitialPacketSize), + false, + ) + } + case "reno": + congestionControl = func(conn *quic.Conn) congestion.CongestionControl { + return congestion_meta1.NewCubicSender( + congestion_meta1.DefaultClock{TimeFunc: timeFunc}, + congestion.ByteCount(conn.Config().InitialPacketSize), + true, + ) + } + default: + return nil, E.New("unknown quic congestion control: ", options.QUICCongestionControl) + } + + quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{ + MaxIncomingStreams: 1 << 60, + Allow0RTT: true, + }) + if err != nil { + udpConn.Close() + return nil, err + } + + h3Server := &http3.Server{ + Handler: handler, + ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context { + conn.SetCongestionControl(congestionControl(conn)) + return log.ContextWithNewID(ctx) + }, + } + + go func() { + sErr := h3Server.ServeListener(quicListener) + udpConn.Close() + if sErr != nil && !E.IsClosedOrCanceled(sErr) { + logger.Error("http3 server closed: ", sErr) + } + }() + + return quicListener, nil + } + naive.WrapError = qtls.WrapError +} diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go new file mode 100644 index 00000000..e04db8c4 --- /dev/null +++ b/protocol/redirect/redirect.go @@ -0,0 +1,68 @@ +package redirect + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/redir" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterRedirect(registry *inbound.Registry) { + inbound.Register[option.RedirectInboundOptions](registry, C.TypeRedirect, NewRedirect) +} + +type Redirect struct { + inbound.Adapter + router adapter.Router + logger log.ContextLogger + listener *listener.Listener +} + +func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) (adapter.Inbound, error) { + redirect := &Redirect{ + Adapter: inbound.NewAdapter(C.TypeRedirect, tag), + router: router, + logger: logger, + } + redirect.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: redirect, + }) + return redirect, nil +} + +func (h *Redirect) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return h.listener.Start() +} + +func (h *Redirect) Close() error { + return h.listener.Close() +} + +func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + destination, err := redir.GetOriginalDestination(conn) + if err != nil { + conn.Close() + h.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err) + return + } + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.Destination = M.SocksaddrFromNetIP(destination) + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/redirect/tproxy.go b/protocol/redirect/tproxy.go new file mode 100644 index 00000000..f0b82bb1 --- /dev/null +++ b/protocol/redirect/tproxy.go @@ -0,0 +1,150 @@ +package redirect + +import ( + "context" + "net" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/redir" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/udpnat2" +) + +func RegisterTProxy(registry *inbound.Registry) { + inbound.Register[option.TProxyInboundOptions](registry, C.TypeTProxy, NewTProxy) +} + +type TProxy struct { + inbound.Adapter + ctx context.Context + router adapter.Router + logger log.ContextLogger + listener *listener.Listener + udpNat *udpnat.Service +} + +func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) (adapter.Inbound, error) { + tproxy := &TProxy{ + Adapter: inbound.NewAdapter(C.TypeTProxy, tag), + ctx: ctx, + router: router, + logger: logger, + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false) + tproxy.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: tproxy, + OOBPacketHandler: tproxy, + TProxy: true, + }) + return tproxy, nil +} + +func (t *TProxy) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return t.listener.Start() +} + +func (t *TProxy) Close() error { + return t.listener.Close() +} + +func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = t.Tag() + metadata.InboundType = t.Type() + metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + t.logger.InfoContext(ctx, "inbound packet connection from ", source) + t.logger.InfoContext(ctx, "inbound packet connection to ", destination) + var metadata adapter.InboundContext + metadata.Inbound = t.Tag() + metadata.InboundType = t.Type() + metadata.Source = source + metadata.Destination = destination + metadata.OriginDestination = t.listener.UDPAddr() + t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { + destination, err := redir.GetOriginalDestinationFromOOB(oob) + if err != nil { + t.logger.Warn("process packet from ", source, ": get tproxy destination: ", err) + return + } + t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil) +} + +func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { + ctx := log.ContextWithNewID(t.ctx) + writer := &tproxyPacketWriter{ + ctx: ctx, + listener: t.listener, + source: source.AddrPort(), + destination: destination, + } + return true, ctx, writer, func(it error) { + common.Close(common.PtrOrNil(writer.conn)) + } +} + +type tproxyPacketWriter struct { + ctx context.Context + listener *listener.Listener + source netip.AddrPort + destination M.Socksaddr + conn *net.UDPConn +} + +func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + defer buffer.Release() + if w.listener.ListenOptions().NetNs == "" { + conn := w.conn + if w.destination == destination && conn != nil { + _, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source) + if err != nil { + w.conn = nil + } + return err + } + } + var listenConfig net.ListenConfig + listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) + listenConfig.Control = control.Append(listenConfig.Control, redir.TProxyWriteBack()) + packetConn, err := w.listener.ListenPacket(listenConfig, w.ctx, "udp", destination.String()) + if err != nil { + return err + } + udpConn := packetConn.(*net.UDPConn) + if w.listener.ListenOptions().NetNs == "" && w.destination == destination { + w.conn = udpConn + } else { + defer udpConn.Close() + } + return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), w.source)) +} diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go new file mode 100644 index 00000000..52e2c524 --- /dev/null +++ b/protocol/shadowsocks/inbound.go @@ -0,0 +1,188 @@ +package shadowsocks + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocks, NewInbound) +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { + if len(options.Users) > 0 && len(options.Destinations) > 0 { + return nil, E.New("users and destinations options must not be combined") + } else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) { + return nil, E.New("users and destinations options are not supported in managed servers") + } + if len(options.Users) > 0 || options.Managed { + return newMultiInbound(ctx, router, logger, tag, options) + } else if len(options.Destinations) > 0 { + return newRelayInbound(ctx, router, logger, tag, options) + } else { + return newInbound(ctx, router, logger, tag, options) + } +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service shadowsocks.Service +} + +func newInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + switch { + case options.Method == shadowsocks.MethodNone: + inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) + case common.Contains(shadowaead.List, options.Method): + inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) + case common.Contains(shadowaead_2022.List, options.Method): + inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) + default: + err = E.New("unsupported method: ", options.Method) + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) + return inbound, err +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +//nolint:staticcheck +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } +} + +//nolint:staticcheck +func (h *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } +} + +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + return h.router.RouteConnection(ctx, conn, metadata) +} + +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + ctx = log.ContextWithNewID(ctx) + h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +var _ N.PacketConn = (*stubPacketConn)(nil) + +type stubPacketConn struct { + N.PacketWriter +} + +func (c *stubPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { + panic("stub!") +} + +func (c *stubPacketConn) Close() error { + return nil +} + +func (c *stubPacketConn) LocalAddr() net.Addr { + panic("stub!") +} + +func (c *stubPacketConn) SetDeadline(t time.Time) error { + panic("stub!") +} + +func (c *stubPacketConn) SetReadDeadline(t time.Time) error { + panic("stub!") +} + +func (c *stubPacketConn) SetWriteDeadline(t time.Time) error { + panic("stub!") +} + +func (h *Inbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) +} + +// Deprecated: remove +func NewError(logger logger.ContextLogger, ctx context.Context, err error) { + common.Close(err) + if E.IsClosedOrCanceled(err) { + logger.DebugContext(ctx, "connection closed: ", err) + return + } + logger.ErrorContext(ctx, err) +} diff --git a/protocol/shadowsocks/inbound_multi.go b/protocol/shadowsocks/inbound_multi.go new file mode 100644 index 00000000..7ff92646 --- /dev/null +++ b/protocol/shadowsocks/inbound_multi.go @@ -0,0 +1,212 @@ +package shadowsocks + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + 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" +) + +var ( + _ adapter.TCPInjectableInbound = (*MultiInbound)(nil) + _ adapter.ManagedSSMServer = (*MultiInbound)(nil) +) + +type MultiInbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service shadowsocks.MultiService[int] + users []option.ShadowsocksUser + tracker adapter.SSMTracker +} + +func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) { + inbound := &MultiInbound{ + Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + var service shadowsocks.MultiService[int] + if common.Contains(shadowaead_2022.List, options.Method) { + service, err = shadowaead_2022.NewMultiServiceWithPassword[int]( + options.Method, + options.Password, + int64(udpTimeout.Seconds()), + adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + ntp.TimeFuncFromContext(ctx), + ) + } else if common.Contains(shadowaead.List, options.Method) { + service, err = shadowaead.NewMultiService[int]( + options.Method, + int64(udpTimeout.Seconds()), + adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + ) + } else { + return nil, E.New("unsupported method: " + options.Method) + } + if err != nil { + return nil, err + } + if len(options.Users) > 0 { + err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int { + return index + }), common.Map(options.Users, func(user option.ShadowsocksUser) string { + return user.Password + })) + if err != nil { + return nil, err + } + } + inbound.service = service + inbound.users = options.Users + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) + return inbound, err +} + +func (h *MultiInbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return h.listener.Start() +} + +func (h *MultiInbound) Close() error { + return h.listener.Close() +} + +func (h *MultiInbound) SetTracker(tracker adapter.SSMTracker) { + h.tracker = tracker +} + +func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string) error { + err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int { + return index + }), uPSKs) + if err != nil { + return err + } + h.users = common.Map(users, func(user string) option.ShadowsocksUser { + return option.ShadowsocksUser{ + Name: user, + } + }) + return nil +} + +//nolint:staticcheck +func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } +} + +//nolint:staticcheck +func (h *MultiInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } +} + +func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + if h.tracker != nil { + conn = h.tracker.TrackConnection(conn, metadata) + } + return h.router.RouteConnection(ctx, conn, metadata) +} + +func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + ctx = log.ContextWithNewID(ctx) + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source) + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + if h.tracker != nil { + conn = h.tracker.TrackPacketConnection(conn, metadata) + } + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +//nolint:staticcheck +func (h *MultiInbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) +} diff --git a/protocol/shadowsocks/inbound_relay.go b/protocol/shadowsocks/inbound_relay.go new file mode 100644 index 00000000..d7d7bcff --- /dev/null +++ b/protocol/shadowsocks/inbound_relay.go @@ -0,0 +1,166 @@ +package shadowsocks + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + 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" +) + +var _ adapter.TCPInjectableInbound = (*RelayInbound)(nil) + +type RelayInbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service *shadowaead_2022.RelayService[int] + destinations []option.ShadowsocksDestination +} + +func newRelayInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*RelayInbound, error) { + inbound := &RelayInbound{ + Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + destinations: options.Destinations, + } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + service, err := shadowaead_2022.NewRelayServiceWithPassword[int]( + options.Method, + options.Password, + int64(udpTimeout.Seconds()), + adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + ) + if err != nil { + return nil, err + } + err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Destinations, func(index int, user option.ShadowsocksDestination) int { + return index + }), common.Map(options.Destinations, func(user option.ShadowsocksDestination) string { + return user.Password + }), common.Map(options.Destinations, option.ShadowsocksDestination.Build)) + if err != nil { + return nil, err + } + inbound.service = service + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) + return inbound, err +} + +func (h *RelayInbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return h.listener.Start() +} + +func (h *RelayInbound) Close() error { + return h.listener.Close() +} + +//nolint:staticcheck +func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } +} + +//nolint:staticcheck +func (h *RelayInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } +} + +func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + destinationIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + destination := h.destinations[destinationIndex].Name + if destination == "" { + destination = F.ToString(destinationIndex) + } else { + metadata.User = destination + } + h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + return h.router.RouteConnection(ctx, conn, metadata) +} + +func (h *RelayInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + destinationIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + destination := h.destinations[destinationIndex].Name + if destination == "" { + destination = F.ToString(destinationIndex) + } else { + metadata.User = destination + } + ctx = log.ContextWithNewID(ctx) + h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection from ", metadata.Source) + h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +//nolint:staticcheck +func (h *RelayInbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) +} diff --git a/protocol/shadowsocks/outbound.go b/protocol/shadowsocks/outbound.go new file mode 100644 index 00000000..9b9d9252 --- /dev/null +++ b/protocol/shadowsocks/outbound.go @@ -0,0 +1,178 @@ +package shadowsocks + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/mux" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/sip003" + "github.com/sagernet/sing-shadowsocks2" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.ShadowsocksOutboundOptions](registry, C.TypeShadowsocks, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + dialer N.Dialer + method shadowsocks.Method + serverAddr M.Socksaddr + plugin sip003.Plugin + uotClient *uot.Client + multiplexDialer *mux.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (adapter.Outbound, error) { + method, err := shadowsocks.CreateMethod(ctx, options.Method, shadowsocks.MethodOptions{ + Password: options.Password, + }) + if err != nil { + return nil, err + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowsocks, tag, options.Network.Build(), options.DialerOptions), + logger: logger, + dialer: outboundDialer, + method: method, + serverAddr: options.ServerOptions.Build(), + } + if options.Plugin != "" { + outbound.plugin, err = sip003.CreatePlugin(ctx, options.Plugin, options.PluginOptions, router, outbound.dialer, outbound.serverAddr) + if err != nil { + return nil, err + } + } + uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) + if !uotOptions.Enabled { + outbound.multiplexDialer, err = mux.NewClientWithOptions((*shadowsocksDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + } + if uotOptions.Enabled { + outbound.uotClient = &uot.Client{ + Dialer: (*shadowsocksDialer)(outbound), + Version: uotOptions.Version, + } + } + return outbound, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + if h.multiplexDialer == nil { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + if h.uotClient != nil { + h.logger.InfoContext(ctx, "outbound UoT connect packet connection to ", destination) + return h.uotClient.DialContext(ctx, network, destination) + } else { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + } + return (*shadowsocksDialer)(h).DialContext(ctx, network, destination) + } else { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + } + return h.multiplexDialer.DialContext(ctx, network, destination) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + if h.multiplexDialer == nil { + if h.uotClient != nil { + h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) + return h.uotClient.ListenPacket(ctx, destination) + } else { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return (*shadowsocksDialer)(h).ListenPacket(ctx, destination) + } else { + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + return h.multiplexDialer.ListenPacket(ctx, destination) + } +} + +func (h *Outbound) InterfaceUpdated() { + if h.multiplexDialer != nil { + h.multiplexDialer.Reset() + } +} + +func (h *Outbound) Close() error { + return common.Close(common.PtrOrNil(h.multiplexDialer)) +} + +var _ N.Dialer = (*shadowsocksDialer)(nil) + +type shadowsocksDialer Outbound + +func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + switch N.NetworkName(network) { + case N.NetworkTCP: + var outConn net.Conn + var err error + if h.plugin != nil { + outConn, err = h.plugin.DialContext(ctx) + } else { + outConn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + } + if err != nil { + return nil, err + } + return h.method.DialEarlyConn(outConn, destination), nil + case N.NetworkUDP: + outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr) + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(h.method.DialPacketConn(outConn), destination), nil + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (h *shadowsocksDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr) + if err != nil { + return nil, err + } + return h.method.DialPacketConn(outConn), nil +} diff --git a/protocol/shadowtls/inbound.go b/protocol/shadowtls/inbound.go new file mode 100644 index 00000000..17afa268 --- /dev/null +++ b/protocol/shadowtls/inbound.go @@ -0,0 +1,141 @@ +package shadowtls + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowtls" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.ShadowTLSInboundOptions](registry, C.TypeShadowTLS, NewInbound) +} + +type Inbound struct { + inbound.Adapter + router adapter.Router + logger logger.ContextLogger + listener *listener.Listener + service *shadowtls.Service +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeShadowTLS, tag), + router: router, + logger: logger, + } + + if options.Version == 0 { + options.Version = 1 + } + + var handshakeForServerName map[string]shadowtls.HandshakeConfig + if options.Version > 1 { + handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) + if options.HandshakeForServerName != nil { + for _, entry := range options.HandshakeForServerName.Entries() { + handshakeDialer, err := dialer.New(ctx, entry.Value.DialerOptions, entry.Value.ServerIsDomain()) + if err != nil { + return nil, err + } + handshakeForServerName[entry.Key] = shadowtls.HandshakeConfig{ + Server: entry.Value.ServerOptions.Build(), + Dialer: handshakeDialer, + } + } + } + } + serverIsDomain := options.Handshake.ServerIsDomain() + if options.WildcardSNI != option.ShadowTLSWildcardSNIOff { + serverIsDomain = true + } + handshakeDialer, err := dialer.New(ctx, options.Handshake.DialerOptions, serverIsDomain) + if err != nil { + return nil, err + } + service, err := shadowtls.NewService(shadowtls.ServiceConfig{ + Version: options.Version, + Password: options.Password, + Users: common.Map(options.Users, func(it option.ShadowTLSUser) shadowtls.User { + return (shadowtls.User)(it) + }), + Handshake: shadowtls.HandshakeConfig{ + Server: options.Handshake.ServerOptions.Build(), + Dialer: handshakeDialer, + }, + HandshakeForServerName: handshakeForServerName, + StrictMode: options.StrictMode, + WildcardSNI: shadowtls.WildcardSNI(options.WildcardSNI), + Handler: (*inboundHandler)(inbound), + Logger: logger, + }) + if err != nil { + return nil, err + } + inbound.service = service + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, metadata.Source, metadata.Destination, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } +} + +type inboundHandler Inbound + +func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.Source = source + metadata.Destination = destination + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/shadowtls/outbound.go b/protocol/shadowtls/outbound.go new file mode 100644 index 00000000..41a4a601 --- /dev/null +++ b/protocol/shadowtls/outbound.go @@ -0,0 +1,104 @@ +package shadowtls + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowtls" + "github.com/sagernet/sing/common" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.ShadowTLSOutboundOptions](registry, C.TypeShadowTLS, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + client *shadowtls.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (adapter.Outbound, error) { + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowTLS, tag, []string{N.NetworkTCP}, options.DialerOptions), + } + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + + if options.Version == 0 { + options.Version = 1 + } + + if options.Version == 1 { + options.TLS.MinVersion = "1.2" + options.TLS.MaxVersion = "1.2" + } + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + + var tlsHandshakeFunc shadowtls.TLSHandshakeFunc + switch options.Version { + case 1, 2: + tlsHandshakeFunc = func(ctx context.Context, conn net.Conn, _ shadowtls.TLSSessionIDGeneratorFunc) error { + return common.Error(tls.ClientHandshake(ctx, conn, tlsConfig)) + } + case 3: + if idConfig, loaded := tlsConfig.(tls.WithSessionIDGenerator); loaded { + tlsHandshakeFunc = func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { + idConfig.SetSessionIDGenerator(sessionIDGenerator) + return common.Error(tls.ClientHandshake(ctx, conn, tlsConfig)) + } + } else { + stdTLSConfig, err := tlsConfig.STDConfig() + if err != nil { + return nil, err + } + tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig) + } + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + client, err := shadowtls.NewClient(shadowtls.ClientConfig{ + Version: options.Version, + Password: options.Password, + Server: options.ServerOptions.Build(), + Dialer: outboundDialer, + TLSHandshake: tlsHandshakeFunc, + Logger: logger, + }) + if err != nil { + return nil, err + } + outbound.client = client + return outbound, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + switch N.NetworkName(network) { + case N.NetworkTCP: + return h.client.DialContext(ctx) + default: + return nil, os.ErrInvalid + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go new file mode 100644 index 00000000..68e0ef58 --- /dev/null +++ b/protocol/socks/inbound.go @@ -0,0 +1,119 @@ +package socks + +import ( + std_bufio "bufio" + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.SocksInboundOptions](registry, C.TypeSOCKS, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + authenticator *auth.Authenticator + udpTimeout time.Duration +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) { + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeSOCKS, tag), + router: uot.NewRouter(router, logger), + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + udpTimeout: udpTimeout, + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } +} + +func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "inbound packet connection") + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/socks/outbound.go b/protocol/socks/outbound.go new file mode 100644 index 00000000..344c7988 --- /dev/null +++ b/protocol/socks/outbound.go @@ -0,0 +1,117 @@ +package socks + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/service" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.SOCKSOutboundOptions](registry, C.TypeSOCKS, NewOutbound) +} + +var _ adapter.Outbound = (*Outbound)(nil) + +type Outbound struct { + outbound.Adapter + dnsRouter adapter.DNSRouter + logger logger.ContextLogger + client *socks.Client + resolve bool + uotClient *uot.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SOCKSOutboundOptions) (adapter.Outbound, error) { + var version socks.Version + var err error + if options.Version != "" { + version, err = socks.ParseVersion(options.Version) + } else { + version = socks.Version5 + } + if err != nil { + return nil, err + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSOCKS, tag, options.Network.Build(), options.DialerOptions), + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + logger: logger, + client: socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password), + resolve: version == socks.Version4, + } + uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) + if uotOptions.Enabled { + outbound.uotClient = &uot.Client{ + Dialer: outbound.client, + Version: uotOptions.Version, + } + } + return outbound, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + if h.uotClient != nil { + h.logger.InfoContext(ctx, "outbound UoT connect packet connection to ", destination) + return h.uotClient.DialContext(ctx, network, destination) + } + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } + if h.resolve && destination.IsDomain() { + destinationAddresses, err := h.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, h.client, network, destination, destinationAddresses) + } + return h.client.DialContext(ctx, network, destination) +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + if h.uotClient != nil { + h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) + return h.uotClient.ListenPacket(ctx, destination) + } + if h.resolve && destination.IsDomain() { + destinationAddresses, err := h.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + packetConn, _, err := N.ListenSerial(ctx, h.client, destination, destinationAddresses) + if err != nil { + return nil, err + } + return packetConn, nil + } + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return h.client.ListenPacket(ctx, destination) +} diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go new file mode 100644 index 00000000..b2f96807 --- /dev/null +++ b/protocol/ssh/outbound.go @@ -0,0 +1,219 @@ +package ssh + +import ( + "bytes" + "context" + "encoding/base64" + "math/rand" + "net" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/crypto/ssh" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.SSHOutboundOptions](registry, C.TypeSSH, NewOutbound) +} + +var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + +type Outbound struct { + outbound.Adapter + ctx context.Context + logger logger.ContextLogger + dialer N.Dialer + serverAddr M.Socksaddr + user string + hostKey []ssh.PublicKey + hostKeyAlgorithms []string + clientVersion string + authMethod []ssh.AuthMethod + clientAccess sync.Mutex + clientConn net.Conn + client *ssh.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (adapter.Outbound, error) { + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSSH, tag, []string{N.NetworkTCP}, options.DialerOptions), + ctx: ctx, + logger: logger, + dialer: outboundDialer, + serverAddr: options.ServerOptions.Build(), + user: options.User, + hostKeyAlgorithms: options.HostKeyAlgorithms, + clientVersion: options.ClientVersion, + } + if outbound.serverAddr.Port == 0 { + outbound.serverAddr.Port = 22 + } + if outbound.user == "" { + outbound.user = "root" + } + if outbound.clientVersion == "" { + outbound.clientVersion = randomVersion() + } + if options.Password != "" { + outbound.authMethod = append(outbound.authMethod, ssh.Password(options.Password)) + } + if len(options.PrivateKey) > 0 || options.PrivateKeyPath != "" { + var privateKey []byte + if len(options.PrivateKey) > 0 { + privateKey = []byte(strings.Join(options.PrivateKey, "\n")) + } else { + var err error + privateKey, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)) + if err != nil { + return nil, E.Cause(err, "read private key") + } + } + var signer ssh.Signer + var err error + if options.PrivateKeyPassphrase == "" { + signer, err = ssh.ParsePrivateKey(privateKey) + } else { + signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(options.PrivateKeyPassphrase)) + } + if err != nil { + return nil, E.Cause(err, "parse private key") + } + outbound.authMethod = append(outbound.authMethod, ssh.PublicKeys(signer)) + } + if len(options.HostKey) > 0 { + for _, hostKey := range options.HostKey { + key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) + if err != nil { + return nil, E.New("parse host key ", key) + } + outbound.hostKey = append(outbound.hostKey, key) + } + } + return outbound, nil +} + +func randomVersion() string { + version := "SSH-2.0-OpenSSH_" + if rand.Intn(2) == 0 { + version += "7." + strconv.Itoa(rand.Intn(10)) + } else { + version += "8." + strconv.Itoa(rand.Intn(9)) + } + return version +} + +func (s *Outbound) connect() (*ssh.Client, error) { + if s.client != nil { + return s.client, nil + } + + s.clientAccess.Lock() + defer s.clientAccess.Unlock() + + if s.client != nil { + return s.client, nil + } + + conn, err := s.dialer.DialContext(s.ctx, N.NetworkTCP, s.serverAddr) + if err != nil { + return nil, err + } + config := &ssh.ClientConfig{ + User: s.user, + Auth: s.authMethod, + ClientVersion: s.clientVersion, + HostKeyAlgorithms: s.hostKeyAlgorithms, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + if len(s.hostKey) == 0 { + return nil + } + serverKey := key.Marshal() + for _, hostKey := range s.hostKey { + if bytes.Equal(serverKey, hostKey.Marshal()) { + return nil + } + } + return E.New("host key mismatch, server send ", key.Type(), " ", base64.StdEncoding.EncodeToString(serverKey)) + }, + } + clientConn, chans, reqs, err := ssh.NewClientConn(conn, s.serverAddr.Addr.String(), config) + if err != nil { + conn.Close() + return nil, E.Cause(err, "connect to ssh server") + } + + client := ssh.NewClient(clientConn, chans, reqs) + + s.clientConn = conn + s.client = client + + go func() { + client.Wait() + conn.Close() + s.clientAccess.Lock() + s.client = nil + s.clientConn = nil + s.clientAccess.Unlock() + }() + + return client, nil +} + +func (s *Outbound) InterfaceUpdated() { + common.Close(s.clientConn) +} + +func (s *Outbound) Close() error { + return common.Close(s.clientConn) +} + +func (s *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + client, err := s.connect() + if err != nil { + return nil, err + } + conn, err := client.Dial(network, destination.String()) + if err != nil { + return nil, err + } + return &chanConnWrapper{Conn: conn}, nil +} + +func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +type chanConnWrapper struct { + net.Conn +} + +func (c *chanConnWrapper) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *chanConnWrapper) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *chanConnWrapper) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} diff --git a/protocol/tailscale/certificate_provider.go b/protocol/tailscale/certificate_provider.go new file mode 100644 index 00000000..5ac18a30 --- /dev/null +++ b/protocol/tailscale/certificate_provider.go @@ -0,0 +1,98 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "crypto/tls" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/common/dialer" + 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" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + "github.com/sagernet/tailscale/client/local" +) + +func RegisterCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.TailscaleCertificateProviderOptions](registry, C.TypeTailscale, NewCertificateProvider) +} + +var _ adapter.CertificateProviderService = (*CertificateProvider)(nil) + +type CertificateProvider struct { + certificate.Adapter + endpointTag string + endpoint *Endpoint + dialer N.Dialer + localClient *local.Client +} + +func NewCertificateProvider(ctx context.Context, _ log.ContextLogger, tag string, options option.TailscaleCertificateProviderOptions) (adapter.CertificateProviderService, error) { + if options.Endpoint == "" { + return nil, E.New("missing tailscale endpoint tag") + } + endpointManager := service.FromContext[adapter.EndpointManager](ctx) + if endpointManager == nil { + return nil, E.New("missing endpoint manager in context") + } + rawEndpoint, loaded := endpointManager.Get(options.Endpoint) + if !loaded { + return nil, E.New("endpoint not found: ", options.Endpoint) + } + endpoint, isTailscale := rawEndpoint.(*Endpoint) + if !isTailscale { + return nil, E.New("endpoint is not Tailscale: ", options.Endpoint) + } + providerDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{}, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "create tailscale certificate provider dialer") + } + return &CertificateProvider{ + Adapter: certificate.NewAdapter(C.TypeTailscale, tag), + endpointTag: options.Endpoint, + endpoint: endpoint, + dialer: providerDialer, + }, nil +} + +func (p *CertificateProvider) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + localClient, err := p.endpoint.Server().LocalClient() + if err != nil { + return E.Cause(err, "initialize tailscale local client for endpoint ", p.endpointTag) + } + originalDial := localClient.Dial + localClient.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) { + if originalDial != nil && addr == "local-tailscaled.sock:80" { + return originalDial(ctx, network, addr) + } + return p.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + p.localClient = localClient + return nil +} + +func (p *CertificateProvider) Close() error { + return nil +} + +func (p *CertificateProvider) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + localClient := p.localClient + if localClient == nil { + return nil, E.New("Tailscale is not ready yet") + } + return localClient.GetCertificate(clientHello) +} diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go new file mode 100644 index 00000000..3a92a66b --- /dev/null +++ b/protocol/tailscale/dns_transport.go @@ -0,0 +1,311 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "net" + "net/http" + "net/netip" + "net/url" + "os" + "strings" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + nDNS "github.com/sagernet/tailscale/net/dns" + "github.com/sagernet/tailscale/types/dnstype" + "github.com/sagernet/tailscale/wgengine/router" + "github.com/sagernet/tailscale/wgengine/wgcfg" + + mDNS "github.com/miekg/dns" + "go4.org/netipx" + "golang.org/x/net/http2" +) + +func RegistryTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, NewDNSTransport) +} + +type DNSTransport struct { + dns.TransportAdapter + ctx context.Context + logger logger.ContextLogger + endpointTag string + acceptDefaultResolvers bool + dnsRouter adapter.DNSRouter + endpointManager adapter.EndpointManager + endpoint *Endpoint + routePrefixes []netip.Prefix + routes map[string][]adapter.DNSTransport + hosts map[string][]netip.Addr + defaultResolvers []adapter.DNSTransport +} + +func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) { + if options.Endpoint == "" { + return nil, E.New("missing tailscale endpoint tag") + } + return &DNSTransport{ + TransportAdapter: dns.NewTransportAdapter(C.DNSTypeTailscale, tag, nil), + ctx: ctx, + logger: logger, + endpointTag: options.Endpoint, + acceptDefaultResolvers: options.AcceptDefaultResolvers, + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + endpointManager: service.FromContext[adapter.EndpointManager](ctx), + }, nil +} + +func (t *DNSTransport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateInitialize { + return nil + } + rawOutbound, loaded := t.endpointManager.Get(t.endpointTag) + if !loaded { + return E.New("endpoint not found: ", t.endpointTag) + } + ep, isTailscale := rawOutbound.(*Endpoint) + if !isTailscale { + return E.New("endpoint is not Tailscale: ", t.endpointTag) + } + if ep.onReconfigHook != nil { + return E.New("only one Tailscale DNS server is allowed for single endpoint") + } + ep.onReconfigHook = t.onReconfig + t.endpoint = ep + return nil +} + +func (t *DNSTransport) Reset() { +} + +func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) { + err := t.updateDNSServers(routerCfg, dnsCfg) + if err != nil { + t.logger.Error(E.Cause(err, "update DNS servers")) + } +} + +func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *nDNS.Config) error { + t.routePrefixes = buildRoutePrefixes(routeConfig) + directDialerOnce := sync.OnceValue(func() N.Dialer { + directDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{})) + return &DNSDialer{transport: t, fallbackDialer: directDialer} + }) + routes := make(map[string][]adapter.DNSTransport) + for domain, resolvers := range dnsConfig.Routes { + var myResolvers []adapter.DNSTransport + for _, resolver := range resolvers { + myResolver, err := t.createResolver(directDialerOnce, resolver) + if err != nil { + return err + } + myResolvers = append(myResolvers, myResolver) + } + routes[domain.WithTrailingDot()] = myResolvers + } + hosts := make(map[string][]netip.Addr) + for domain, addresses := range dnsConfig.Hosts { + hosts[domain.WithTrailingDot()] = addresses + } + var defaultResolvers []adapter.DNSTransport + for _, resolver := range dnsConfig.DefaultResolvers { + myResolver, err := t.createResolver(directDialerOnce, resolver) + if err != nil { + return err + } + defaultResolvers = append(defaultResolvers, myResolver) + } + t.routes = routes + t.hosts = hosts + t.defaultResolvers = defaultResolvers + if len(defaultResolvers) > 0 { + t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ", + strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " ")) + } else { + t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts") + } + return nil +} + +func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dnstype.Resolver) (adapter.DNSTransport, error) { + serverURL, parseURLErr := url.Parse(resolver.Addr) + var myDialer N.Dialer + if parseURLErr == nil && serverURL.Scheme == "http" { + myDialer = t.endpoint + } else { + myDialer = directDialer() + } + if len(resolver.BootstrapResolution) > 0 { + bootstrapTransport := transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, M.SocksaddrFrom(resolver.BootstrapResolution[0], 53)) + myDialer = dialer.NewResolveDialer(t.ctx, myDialer, false, "", adapter.DNSQueryOptions{Transport: bootstrapTransport}, 0) + } + if serverAddr := M.ParseSocksaddr(resolver.Addr); serverAddr.IsValid() { + if serverAddr.Port == 0 { + serverAddr.Port = 53 + } + return transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, serverAddr), nil + } else if parseURLErr != nil { + return nil, E.Cause(parseURLErr, "parse resolver address") + } else { + switch serverURL.Scheme { + case "https": + serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) + if serverAddr.Port == 0 { + serverAddr.Port = 443 + } + tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.AddrString(), option.OutboundTLSOptions{ + ALPN: []string{http2.NextProtoTLS, "http/1.1"}, + })) + return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, tlsConfig), nil + case "http": + serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) + if serverAddr.Port == 0 { + serverAddr.Port = 80 + } + return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, nil), nil + // case "tls": + default: + return nil, E.New("unknown resolver scheme: ", serverURL.Scheme) + } + } +} + +func buildRoutePrefixes(routeConfig *router.Config) []netip.Prefix { + var builder netipx.IPSetBuilder + for _, localAddr := range routeConfig.LocalAddrs { + builder.AddPrefix(localAddr) + } + for _, route := range routeConfig.Routes { + builder.AddPrefix(route) + } + for _, route := range routeConfig.LocalRoutes { + builder.AddPrefix(route) + } + for _, route := range routeConfig.SubnetRoutes { + builder.AddPrefix(route) + } + ipSet, err := builder.IPSet() + if err != nil { + return nil + } + return ipSet.Prefixes() +} + +func (t *DNSTransport) Close() error { + return nil +} + +func (t *DNSTransport) Raw() bool { + return true +} + +func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if len(message.Question) != 1 { + return nil, os.ErrInvalid + } + question := message.Question[0] + addresses, hostsLoaded := t.hosts[question.Name] + if hostsLoaded { + switch question.Qtype { + case mDNS.TypeA: + addresses4 := common.Filter(addresses, func(addr netip.Addr) bool { + return addr.Is4() + }) + if len(addresses4) > 0 { + return dns.FixedResponse(message.Id, question, addresses4, C.DefaultDNSTTL), nil + } + case mDNS.TypeAAAA: + addresses6 := common.Filter(addresses, func(addr netip.Addr) bool { + return addr.Is6() + }) + if len(addresses6) > 0 { + return dns.FixedResponse(message.Id, question, addresses6, C.DefaultDNSTTL), nil + } + } + } + for domainSuffix, transports := range t.routes { + if strings.HasSuffix(question.Name, domainSuffix) { + if len(transports) == 0 { + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Rcode: mDNS.RcodeNameError, + Response: true, + }, + Question: []mDNS.Question{question}, + }, nil + } + var lastErr error + for _, dnsTransport := range transports { + response, err := dnsTransport.Exchange(ctx, message) + if err != nil { + lastErr = err + continue + } + return response, nil + } + return nil, lastErr + } + } + if t.acceptDefaultResolvers { + if len(t.defaultResolvers) > 0 { + var lastErr error + for _, resolver := range t.defaultResolvers { + response, err := resolver.Exchange(ctx, message) + if err != nil { + lastErr = err + continue + } + return response, nil + } + return nil, lastErr + } else { + return nil, E.New("missing default resolvers") + } + } + return nil, dns.RcodeNameError +} + +type DNSDialer struct { + transport *DNSTransport + fallbackDialer N.Dialer +} + +func (d *DNSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if destination.IsDomain() { + panic("invalid request here") + } + for _, prefix := range d.transport.routePrefixes { + if prefix.Contains(destination.Addr) { + return d.transport.endpoint.DialContext(ctx, network, destination) + } + } + return d.fallbackDialer.DialContext(ctx, network, destination) +} + +func (d *DNSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if destination.IsDomain() { + panic("invalid request here") + } + for _, prefix := range d.transport.routePrefixes { + if prefix.Contains(destination.Addr) { + return d.transport.endpoint.ListenPacket(ctx, destination) + } + } + return d.fallbackDialer.ListenPacket(ctx, destination) +} diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go new file mode 100644 index 00000000..33b76930 --- /dev/null +++ b/protocol/tailscale/endpoint.go @@ -0,0 +1,860 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/netip" + "net/url" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "sync/atomic" + "syscall" + "time" + + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" + _ "github.com/sagernet/tailscale/feature/relayserver" + "github.com/sagernet/tailscale/ipn" + tsDNS "github.com/sagernet/tailscale/net/dns" + "github.com/sagernet/tailscale/net/netmon" + "github.com/sagernet/tailscale/net/netns" + "github.com/sagernet/tailscale/net/tsaddr" + tsTUN "github.com/sagernet/tailscale/net/tstun" + "github.com/sagernet/tailscale/tsnet" + "github.com/sagernet/tailscale/types/ipproto" + "github.com/sagernet/tailscale/types/nettype" + "github.com/sagernet/tailscale/version" + "github.com/sagernet/tailscale/wgengine" + "github.com/sagernet/tailscale/wgengine/filter" + "github.com/sagernet/tailscale/wgengine/router" + "github.com/sagernet/tailscale/wgengine/wgcfg" + + "go4.org/netipx" +) + +var ( + _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) + _ adapter.DirectRouteOutbound = (*Endpoint)(nil) + _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) +) + +func init() { + version.SetVersion("sing-box " + C.Version) +} + +func RegisterEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, NewEndpoint) +} + +type Endpoint struct { + endpoint.Adapter + ctx context.Context + router adapter.Router + logger logger.ContextLogger + dnsRouter adapter.DNSRouter + network adapter.NetworkManager + platformInterface adapter.PlatformInterface + server *tsnet.Server + stack *stack.Stack + icmpForwarder *tun.ICMPForwarder + filter *atomic.Pointer[filter.Filter] + onReconfigHook wgengine.ReconfigListener + + cfg *wgcfg.Config + dnsCfg *tsDNS.Config + routeDomains common.TypedValue[map[string]bool] + routePrefixes atomic.Pointer[netipx.IPSet] + + acceptRoutes bool + exitNode string + exitNodeAllowLANAccess bool + advertiseRoutes []netip.Prefix + advertiseExitNode bool + advertiseTags []string + relayServerPort *uint16 + relayServerStaticEndpoints []netip.AddrPort + + udpTimeout time.Duration + + systemInterface bool + systemInterfaceName string + systemInterfaceMTU uint32 + systemTun tun.Tun + systemDialer *dialer.DefaultDialer + fallbackTCPCloser func() +} + +func (t *Endpoint) registerNetstackHandlers() { + netstack := t.server.ExportNetstack() + if netstack == nil { + return + } + previousTCP := netstack.GetTCPHandlerForFlow + netstack.GetTCPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { + if previousTCP != nil { + handler, intercept = previousTCP(src, dst) + if handler != nil || !intercept { + return handler, intercept + } + } + return func(conn net.Conn) { + ctx := log.ContextWithNewID(t.ctx) + source := M.SocksaddrFrom(src.Addr(), src.Port()) + destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) + t.NewConnectionEx(ctx, conn, source, destination, nil) + }, true + } + + previousUDP := netstack.GetUDPHandlerForFlow + netstack.GetUDPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) { + if previousUDP != nil { + handler, intercept = previousUDP(src, dst) + if handler != nil || !intercept { + return handler, intercept + } + } + return func(conn nettype.ConnPacketConn) { + ctx := log.ContextWithNewID(t.ctx) + source := M.SocksaddrFrom(src.Addr(), src.Port()) + destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) + packetConn := bufio.NewUnbindPacketConnWithAddr(conn, destination) + t.NewPacketConnectionEx(ctx, packetConn, source, destination, nil) + }, true + } +} + +func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) { + stateDirectory := options.StateDirectory + if stateDirectory == "" { + stateDirectory = "tailscale" + } + hostname := options.Hostname + if hostname == "" { + osHostname, _ := os.Hostname() + osHostname = strings.TrimSpace(osHostname) + hostname = osHostname + } + if hostname == "" { + hostname = "sing-box" + } + stateDirectory = filemanager.BasePath(ctx, os.ExpandEnv(stateDirectory)) + stateDirectory, _ = filepath.Abs(stateDirectory) + for _, advertiseRoute := range options.AdvertiseRoutes { + if advertiseRoute.Addr().IsUnspecified() && advertiseRoute.Bits() == 0 { + return nil, E.New("`advertise_routes` cannot be default, use `advertise_exit_node` instead.") + } + } + if options.AdvertiseExitNode && options.ExitNode != "" { + return nil, E.New("cannot advertise an exit node and use an exit node at the same time.") + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + var remoteIsDomain bool + if options.ControlURL != "" { + controlURL, err := url.Parse(options.ControlURL) + if err != nil { + return nil, E.Cause(err, "parse control URL") + } + remoteIsDomain = M.ParseSocksaddr(controlURL.Hostname()).IsDomain() + } else { + // controlplane.tailscale.com + remoteIsDomain = true + } + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: remoteIsDomain, + ResolverOnDetour: true, + NewDialer: true, + }) + if err != nil { + return nil, err + } + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + server := &tsnet.Server{ + Dir: stateDirectory, + Hostname: hostname, + Logf: func(format string, args ...any) { + logger.Trace(fmt.Sprintf(format, args...)) + }, + UserLogf: func(format string, args ...any) { + logger.Debug(fmt.Sprintf(format, args...)) + }, + Ephemeral: options.Ephemeral, + AuthKey: options.AuthKey, + ControlURL: options.ControlURL, + AdvertiseTags: options.AdvertiseTags, + Dialer: &endpointDialer{Dialer: outboundDialer, logger: logger}, + LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) { + return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions()) + }, + DNS: &dnsConfigurtor{}, + HTTPClient: &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: ntp.TimeFuncFromContext(ctx), + }, + }, + }, + } + return &Endpoint{ + Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), + ctx: ctx, + router: router, + logger: logger, + dnsRouter: dnsRouter, + network: service.FromContext[adapter.NetworkManager](ctx), + platformInterface: service.FromContext[adapter.PlatformInterface](ctx), + server: server, + acceptRoutes: options.AcceptRoutes, + exitNode: options.ExitNode, + exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess, + advertiseRoutes: options.AdvertiseRoutes, + advertiseExitNode: options.AdvertiseExitNode, + advertiseTags: options.AdvertiseTags, + relayServerPort: options.RelayServerPort, + relayServerStaticEndpoints: options.RelayServerStaticEndpoints, + udpTimeout: udpTimeout, + systemInterface: options.SystemInterface, + systemInterfaceName: options.SystemInterfaceName, + systemInterfaceMTU: options.SystemInterfaceMTU, + }, nil +} + +func (t *Endpoint) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + return t.start() + case adapter.StartStatePostStart: + return t.postStart() + } + return nil +} + +func (t *Endpoint) start() error { + if t.platformInterface != nil { + err := t.network.UpdateInterfaces() + if err != nil { + return err + } + netmon.RegisterInterfaceGetter(func() ([]netmon.Interface, error) { + return common.Map(t.network.InterfaceFinder().Interfaces(), func(it control.Interface) netmon.Interface { + return netmon.Interface{ + Interface: &net.Interface{ + Index: it.Index, + MTU: it.MTU, + Name: it.Name, + HardwareAddr: it.HardwareAddr, + Flags: it.Flags, + }, + AltAddrs: common.Map(it.Addresses, func(it netip.Prefix) net.Addr { + return &net.IPNet{ + IP: it.Addr().AsSlice(), + Mask: net.CIDRMask(it.Bits(), it.Addr().BitLen()), + } + }), + } + }), nil + }) + } + if t.systemInterface { + mtu := t.systemInterfaceMTU + if mtu == 0 { + mtu = uint32(tsTUN.DefaultTUNMTU()) + } + tunName := t.systemInterfaceName + if tunName == "" { + tunName = tun.CalculateInterfaceName("tailscale") + } + tunOptions := tun.Options{ + Name: tunName, + MTU: mtu, + GSO: true, + InterfaceScope: true, + InterfaceMonitor: t.network.InterfaceMonitor(), + InterfaceFinder: t.network.InterfaceFinder(), + Logger: t.logger, + EXP_ExternalConfiguration: true, + } + systemTun, err := tun.New(tunOptions) + if err != nil { + return err + } + err = systemTun.Start() + if err != nil { + _ = systemTun.Close() + return err + } + wgTunDevice, err := newTunDeviceAdapter(systemTun, int(mtu), t.logger) + if err != nil { + _ = systemTun.Close() + return err + } + systemDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ + BindInterface: tunName, + }) + if err != nil { + _ = systemTun.Close() + return err + } + t.systemTun = systemTun + t.systemDialer = systemDialer + t.server.TunDevice = wgTunDevice + } + if mark := t.network.AutoRedirectOutputMark(); mark > 0 { + controlFunc := t.network.AutoRedirectOutputMarkFunc() + if bindFunc := t.network.AutoDetectInterfaceFunc(); bindFunc != nil { + controlFunc = control.Append(controlFunc, bindFunc) + } + netns.SetControlFunc(controlFunc) + } else if runtime.GOOS == "android" && t.platformInterface != nil { + netns.SetControlFunc(func(network, address string, c syscall.RawConn) error { + return control.Raw(c, func(fd uintptr) error { + return t.platformInterface.AutoDetectInterfaceControl(int(fd)) + }) + }) + } + return nil +} + +func (t *Endpoint) postStart() error { + err := t.server.Start() + if err != nil { + if t.systemTun != nil { + _ = t.systemTun.Close() + } + return err + } + if t.fallbackTCPCloser == nil { + t.fallbackTCPCloser = t.server.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { + return func(conn net.Conn) { + ctx := log.ContextWithNewID(t.ctx) + source := M.SocksaddrFrom(src.Addr(), src.Port()) + destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) + t.NewConnectionEx(ctx, conn, source, destination, nil) + }, true + }) + } + t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig) + + ipStack := t.server.ExportNetstack().ExportIPStack() + gErr := ipStack.SetSpoofing(tun.DefaultNIC, true) + if gErr != nil { + return gonet.TranslateNetstackError(gErr) + } + gErr = ipStack.SetPromiscuousMode(tun.DefaultNIC, true) + if gErr != nil { + return gonet.TranslateNetstackError(gErr) + } + icmpForwarder := tun.NewICMPForwarder(t.ctx, ipStack, t, t.udpTimeout) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + t.stack = ipStack + t.icmpForwarder = icmpForwarder + t.registerNetstackHandlers() + + localBackend := t.server.ExportLocalBackend() + perfs := &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + RouteAll: t.acceptRoutes, + AdvertiseRoutes: t.advertiseRoutes, + }, + RouteAllSet: true, + ExitNodeIPSet: true, + AdvertiseRoutesSet: true, + RelayServerPortSet: true, + RelayServerStaticEndpointsSet: true, + } + if t.advertiseExitNode { + perfs.AdvertiseRoutes = append(perfs.AdvertiseRoutes, tsaddr.ExitRoutes()...) + } + if t.relayServerPort != nil { + perfs.RelayServerPort = t.relayServerPort + } + if len(t.relayServerStaticEndpoints) > 0 { + perfs.RelayServerStaticEndpoints = t.relayServerStaticEndpoints + } + _, err = localBackend.EditPrefs(perfs) + if err != nil { + return E.Cause(err, "update prefs") + } + t.filter = localBackend.ExportFilter() + go t.watchState() + return nil +} + +func (t *Endpoint) watchState() { + localBackend := t.server.ExportLocalBackend() + localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) { + if roNotify.State != nil && *roNotify.State != ipn.NeedsLogin && *roNotify.State != ipn.NoState { + return false + } + authURL := localBackend.StatusWithoutPeers().AuthURL + if authURL != "" { + t.logger.Info("Waiting for authentication: ", authURL) + if t.platformInterface != nil { + err := t.platformInterface.SendNotification(&adapter.Notification{ + Identifier: "tailscale-authentication", + TypeName: "Tailscale Authentication Notifications", + TypeID: 10, + Title: "Tailscale Authentication", + Body: F.ToString("Tailscale outbound[", t.Tag(), "] is waiting for authentication."), + OpenURL: authURL, + }) + if err != nil { + t.logger.Error("send authentication notification: ", err) + } + } + return false + } + return true + }) + if t.exitNode != "" { + localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) { + if roNotify.State == nil || *roNotify.State != ipn.Running { + return true + } + status, err := common.Must1(t.server.LocalClient()).Status(t.ctx) + if err != nil { + t.logger.Error("set exit node: ", err) + return + } + perfs := &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + ExitNodeAllowLANAccess: t.exitNodeAllowLANAccess, + }, + ExitNodeIPSet: true, + ExitNodeAllowLANAccessSet: true, + } + err = perfs.SetExitNodeIP(t.exitNode, status) + if err != nil { + t.logger.Error("set exit node: ", err) + return true + } + _, err = localBackend.EditPrefs(perfs) + if err != nil { + t.logger.Error("set exit node: ", err) + return true + } + return false + }) + } +} + +func (t *Endpoint) Close() error { + err := common.Close(common.PtrOrNil(t.server)) + netmon.RegisterInterfaceGetter(nil) + netns.SetControlFunc(nil) + if t.fallbackTCPCloser != nil { + t.fallbackTCPCloser() + t.fallbackTCPCloser = nil + } + if t.systemTun != nil { + t.systemTun.Close() + t.systemTun = nil + } + return err +} + +func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case N.NetworkTCP: + t.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + t.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + if destination.IsDomain() { + destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, t, network, destination, destinationAddresses) + } + if t.systemDialer != nil { + return t.systemDialer.DialContext(ctx, network, destination) + } + addr4, addr6 := t.server.TailscaleIPs() + remoteAddr := tcpip.FullAddress{ + NIC: 1, + Port: destination.Port, + Addr: addressFromAddr(destination.Addr), + } + var localAddr tcpip.FullAddress + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !addr4.IsValid() { + return nil, E.New("missing Tailscale IPv4 address") + } + networkProtocol = header.IPv4ProtocolNumber + localAddr = tcpip.FullAddress{ + NIC: 1, + Addr: addressFromAddr(addr4), + } + } else { + if !addr6.IsValid() { + return nil, E.New("missing Tailscale IPv6 address") + } + networkProtocol = header.IPv6ProtocolNumber + localAddr = tcpip.FullAddress{ + NIC: 1, + Addr: addressFromAddr(addr6), + } + } + switch N.NetworkName(network) { + case N.NetworkTCP: + tcpConn, err := gonet.DialTCPWithBind(ctx, t.stack, localAddr, remoteAddr, networkProtocol) + if err != nil { + return nil, err + } + return tcpConn, nil + case N.NetworkUDP: + udpConn, err := gonet.DialUDP(t.stack, &localAddr, &remoteAddr, networkProtocol) + if err != nil { + return nil, err + } + return udpConn, nil + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (t *Endpoint) listenPacketWithAddress(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if t.systemDialer != nil { + return t.systemDialer.ListenPacket(ctx, destination) + } + addr4, addr6 := t.server.TailscaleIPs() + bind := tcpip.FullAddress{ + NIC: 1, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !addr4.IsValid() { + return nil, E.New("missing Tailscale IPv4 address") + } + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = addressFromAddr(addr4) + } else { + if !addr6.IsValid() { + return nil, E.New("missing Tailscale IPv6 address") + } + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = addressFromAddr(addr6) + } + udpConn, err := gonet.DialUDP(t.stack, &bind, nil, networkProtocol) + if err != nil { + return nil, err + } + return udpConn, nil +} + +func (t *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { + t.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if destination.IsDomain() { + destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, netip.Addr{}, err + } + var errors []error + for _, address := range destinationAddresses { + packetConn, packetErr := t.listenPacketWithAddress(ctx, M.SocksaddrFrom(address, destination.Port)) + if packetErr == nil { + return packetConn, address, nil + } + errors = append(errors, packetErr) + } + return nil, netip.Addr{}, E.Errors(errors...) + } + packetConn, err := t.listenPacketWithAddress(ctx, destination) + if err != nil { + return nil, netip.Addr{}, err + } + if destination.IsIP() { + return packetConn, destination.Addr, nil + } + return packetConn, netip.Addr{}, nil +} + +func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + packetConn, destinationAddress, err := t.ListenPacketWithDestination(ctx, destination) + if err != nil { + return nil, err + } + if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) { + return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil + } + return packetConn, nil +} + +func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + tsFilter := t.filter.Load() + if tsFilter != nil { + var ipProto ipproto.Proto + switch N.NetworkName(network) { + case N.NetworkTCP: + ipProto = ipproto.TCP + case N.NetworkUDP: + ipProto = ipproto.UDP + case N.NetworkICMP: + if !destination.IsIPv6() { + ipProto = ipproto.ICMPv4 + } else { + ipProto = ipproto.ICMPv6 + } + } + response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto) + switch response { + case filter.Drop: + return nil, syscall.ECONNREFUSED + case filter.DropSilently: + return nil, tun.ErrDrop + } + } + var ipVersion uint8 + if !destination.IsIPv6() { + ipVersion = 4 + } else { + ipVersion = 6 + } + routeDestination, err := t.router.PreMatch(adapter.InboundContext{ + Inbound: t.Tag(), + InboundType: t.Type(), + IPVersion: ipVersion, + Network: network, + Source: source, + Destination: destination, + }, routeContext, timeout, false) + if err != nil { + switch { + case rule.IsBypassed(err): + err = nil + case rule.IsRejected(err): + t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) + default: + if network == N.NetworkICMP { + t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) + } + } + } + return routeDestination, err +} + +func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = t.Tag() + metadata.InboundType = t.Type() + metadata.Source = source + addr4, addr6 := t.server.TailscaleIPs() + switch destination.Addr { + case addr4: + destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + case addr6: + destination.Addr = netip.IPv6Loopback() + } + metadata.Destination = destination + t.logger.InfoContext(ctx, "inbound connection from ", source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = t.Tag() + metadata.InboundType = t.Type() + metadata.Source = source + addr4, addr6 := t.server.TailscaleIPs() + switch destination.Addr { + case addr4: + metadata.OriginDestination = destination + destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) + case addr6: + metadata.OriginDestination = destination + destination.Addr = netip.IPv6Loopback() + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) + } + metadata.Destination = destination + t.logger.InfoContext(ctx, "inbound packet connection from ", source) + t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + ctx := log.ContextWithNewID(t.ctx) + var destination tun.DirectRouteDestination + var err error + if t.systemDialer != nil { + destination, err = ping.ConnectDestination( + ctx, t.logger, + t.systemDialer.DialerForICMPDestination(metadata.Destination.Addr).Control, + metadata.Destination.Addr, routeContext, timeout, + ) + } else { + inet4Address, inet6Address := t.server.TailscaleIPs() + if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() { + return nil, E.New("Tailscale is not ready yet") + } + destination, err = ping.ConnectGVisor( + ctx, t.logger, + metadata.Source.Addr, metadata.Destination.Addr, + routeContext, + t.stack, + inet4Address, inet6Address, + timeout, + ) + } + if err != nil { + return nil, err + } + t.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) + return destination, nil +} + +func (t *Endpoint) PreferredDomain(domain string) bool { + routeDomains := t.routeDomains.Load() + if routeDomains == nil { + return false + } + return routeDomains[strings.ToLower(domain)] +} + +func (t *Endpoint) PreferredAddress(address netip.Addr) bool { + routePrefixes := t.routePrefixes.Load() + if routePrefixes == nil { + return false + } + return routePrefixes.Contains(address) +} + +func (t *Endpoint) Server() *tsnet.Server { + return t.server +} + +func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *tsDNS.Config) { + if cfg == nil || dnsCfg == nil { + return + } + if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) { + return + } + var inet4Address, inet6Address netip.Addr + for _, address := range cfg.Addresses { + if address.Addr().Is4() { + inet4Address = address.Addr() + } else if address.Addr().Is6() { + inet6Address = address.Addr() + } + } + t.icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) + t.cfg = cfg + t.dnsCfg = dnsCfg + + routeDomains := make(map[string]bool) + for fqdn := range dnsCfg.Routes { + routeDomains[fqdn.WithoutTrailingDot()] = true + } + for _, fqdn := range dnsCfg.SearchDomains { + routeDomains[fqdn.WithoutTrailingDot()] = true + } + t.routeDomains.Store(routeDomains) + + var builder netipx.IPSetBuilder + for _, peer := range cfg.Peers { + for _, allowedIP := range peer.AllowedIPs { + builder.AddPrefix(allowedIP) + } + } + t.routePrefixes.Store(common.Must1(builder.IPSet())) + + if t.onReconfigHook != nil { + t.onReconfigHook(cfg, routerCfg, dnsCfg) + } +} + +func addressFromAddr(destination netip.Addr) tcpip.Address { + if destination.Is6() { + return tcpip.AddrFrom16(destination.As16()) + } else { + return tcpip.AddrFrom4(destination.As4()) + } +} + +type endpointDialer struct { + N.Dialer + logger logger.ContextLogger +} + +func (d *endpointDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkTCP: + d.logger.InfoContext(ctx, "output connection to ", destination) + case N.NetworkUDP: + d.logger.InfoContext(ctx, "output packet connection to ", destination) + } + return d.Dialer.DialContext(ctx, network, destination) +} + +func (d *endpointDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + d.logger.InfoContext(ctx, "output packet connection") + return d.Dialer.ListenPacket(ctx, destination) +} + +type dnsConfigurtor struct { + baseConfig tsDNS.OSConfig +} + +func (c *dnsConfigurtor) SetDNS(cfg tsDNS.OSConfig) error { + c.baseConfig = cfg + return nil +} + +func (c *dnsConfigurtor) SupportsSplitDNS() bool { + return true +} + +func (c *dnsConfigurtor) GetBaseConfig() (tsDNS.OSConfig, error) { + return c.baseConfig, nil +} + +func (c *dnsConfigurtor) Close() error { + return nil +} diff --git a/protocol/tailscale/hostinfo_tvos.go b/protocol/tailscale/hostinfo_tvos.go new file mode 100644 index 00000000..d8e391bb --- /dev/null +++ b/protocol/tailscale/hostinfo_tvos.go @@ -0,0 +1,16 @@ +//go:build with_gvisor && tvos + +package tailscale + +import ( + _ "unsafe" + + "github.com/sagernet/tailscale/types/lazy" +) + +//go:linkname isAppleTV github.com/sagernet/tailscale/version.isAppleTV +var isAppleTV lazy.SyncValue[bool] + +func init() { + isAppleTV.Set(true) +} diff --git a/protocol/tailscale/ping.go b/protocol/tailscale/ping.go new file mode 100644 index 00000000..8bb0476b --- /dev/null +++ b/protocol/tailscale/ping.go @@ -0,0 +1,55 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/tailscale/ipn/ipnstate" + "github.com/sagernet/tailscale/tailcfg" +) + +func (t *Endpoint) StartTailscalePing(ctx context.Context, peerIP string, fn func(*adapter.TailscalePingResult)) error { + ip, err := netip.ParseAddr(peerIP) + if err != nil { + return err + } + localClient, err := t.server.LocalClient() + if err != nil { + return err + } + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + result, pingErr := localClient.Ping(ctx, ip, tailcfg.PingDisco) + if ctx.Err() != nil { + return ctx.Err() + } + if pingErr != nil { + fn(&adapter.TailscalePingResult{ + Error: pingErr.Error(), + }) + } else { + fn(convertPingResult(result)) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +func convertPingResult(result *ipnstate.PingResult) *adapter.TailscalePingResult { + return &adapter.TailscalePingResult{ + LatencyMs: result.LatencySeconds * 1000, + IsDirect: result.Endpoint != "", + Endpoint: result.Endpoint, + DERPRegionID: int32(result.DERPRegionID), + DERPRegionCode: result.DERPRegionCode, + Error: result.Err, + } +} diff --git a/protocol/tailscale/status.go b/protocol/tailscale/status.go new file mode 100644 index 00000000..a4d14ee1 --- /dev/null +++ b/protocol/tailscale/status.go @@ -0,0 +1,105 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "slices" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/tailscale/ipn" + "github.com/sagernet/tailscale/ipn/ipnstate" +) + +var _ adapter.TailscaleEndpoint = (*Endpoint)(nil) + +func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapter.TailscaleEndpointStatus)) error { + localBackend := t.server.ExportLocalBackend() + sendStatus := func() { + status := localBackend.Status() + fn(convertTailscaleStatus(status)) + } + sendStatus() + localBackend.WatchNotifications(ctx, ipn.NotifyInitialState|ipn.NotifyInitialNetMap|ipn.NotifyRateLimit, nil, func(roNotify *ipn.Notify) (keepGoing bool) { + select { + case <-ctx.Done(): + return false + default: + } + if roNotify.State != nil || roNotify.NetMap != nil || roNotify.BrowseToURL != nil { + sendStatus() + } + return true + }) + return ctx.Err() +} + +func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointStatus { + result := &adapter.TailscaleEndpointStatus{ + BackendState: status.BackendState, + AuthURL: status.AuthURL, + } + if status.CurrentTailnet != nil { + result.NetworkName = status.CurrentTailnet.Name + result.MagicDNSSuffix = status.CurrentTailnet.MagicDNSSuffix + } + if status.Self != nil { + result.Self = convertTailscalePeer(status.Self) + } + groupIndex := make(map[int64]*adapter.TailscaleUserGroup) + for _, peerKey := range status.Peers() { + peer := status.Peer[peerKey] + userID := int64(peer.UserID) + group, loaded := groupIndex[userID] + if !loaded { + group = &adapter.TailscaleUserGroup{ + UserID: userID, + } + if profile, hasProfile := status.User[peer.UserID]; hasProfile { + group.LoginName = profile.LoginName + group.DisplayName = profile.DisplayName + group.ProfilePicURL = profile.ProfilePicURL + } + groupIndex[userID] = group + result.UserGroups = append(result.UserGroups, group) + } + group.Peers = append(group.Peers, convertTailscalePeer(peer)) + } + for _, group := range result.UserGroups { + slices.SortStableFunc(group.Peers, func(a, b *adapter.TailscalePeer) int { + if a.Online != b.Online { + if a.Online { + return -1 + } + return 1 + } + return 0 + }) + } + return result +} + +func convertTailscalePeer(peer *ipnstate.PeerStatus) *adapter.TailscalePeer { + ips := make([]string, len(peer.TailscaleIPs)) + for i, ip := range peer.TailscaleIPs { + ips[i] = ip.String() + } + var keyExpiry int64 + if peer.KeyExpiry != nil { + keyExpiry = peer.KeyExpiry.Unix() + } + return &adapter.TailscalePeer{ + HostName: peer.HostName, + DNSName: peer.DNSName, + OS: peer.OS, + TailscaleIPs: ips, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + UserID: int64(peer.UserID), + KeyExpiry: keyExpiry, + } +} diff --git a/protocol/tailscale/tun_device_unix.go b/protocol/tailscale/tun_device_unix.go new file mode 100644 index 00000000..a8d237ab --- /dev/null +++ b/protocol/tailscale/tun_device_unix.go @@ -0,0 +1,156 @@ +//go:build with_gvisor && !windows + +package tailscale + +import ( + "encoding/hex" + "errors" + "io" + "os" + "sync" + "sync/atomic" + + singTun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/logger" + wgTun "github.com/sagernet/wireguard-go/tun" +) + +type tunDeviceAdapter struct { + tun singTun.Tun + linuxTUN singTun.LinuxTUN + events chan wgTun.Event + mtu int + logger logger.ContextLogger + debugTun bool + readCount atomic.Uint32 + writeCount atomic.Uint32 + closeOnce sync.Once +} + +func newTunDeviceAdapter(tun singTun.Tun, mtu int, logger logger.ContextLogger) (wgTun.Device, error) { + if tun == nil { + return nil, os.ErrInvalid + } + if mtu == 0 { + mtu = 1500 + } + adapter := &tunDeviceAdapter{ + tun: tun, + events: make(chan wgTun.Event, 1), + mtu: mtu, + logger: logger, + debugTun: os.Getenv("SINGBOX_TS_TUN_DEBUG") != "", + } + if linuxTUN, ok := tun.(singTun.LinuxTUN); ok { + adapter.linuxTUN = linuxTUN + } + adapter.events <- wgTun.EventUp + return adapter, nil +} + +func (a *tunDeviceAdapter) File() *os.File { + return nil +} + +func (a *tunDeviceAdapter) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { + if a.linuxTUN != nil { + n, err := a.linuxTUN.BatchRead(bufs, offset-singTun.PacketOffset, sizes) + if err == nil { + for i := 0; i < n; i++ { + a.debugPacket("read", bufs[i][offset:offset+sizes[i]]) + } + } + return n, err + } + if offset < singTun.PacketOffset { + return 0, io.ErrShortBuffer + } + readBuf := bufs[0][offset-singTun.PacketOffset:] + n, err := a.tun.Read(readBuf) + if err == nil { + if n < singTun.PacketOffset { + return 0, io.ErrUnexpectedEOF + } + sizes[0] = n - singTun.PacketOffset + a.debugPacket("read", readBuf[singTun.PacketOffset:n]) + return 1, nil + } + if errors.Is(err, singTun.ErrTooManySegments) { + err = wgTun.ErrTooManySegments + } + return 0, err +} + +func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err error) { + if a.linuxTUN != nil { + for i := range bufs { + a.debugPacket("write", bufs[i][offset:]) + } + return a.linuxTUN.BatchWrite(bufs, offset) + } + for _, packet := range bufs { + a.debugPacket("write", packet[offset:]) + if singTun.PacketOffset > 0 { + common.ClearArray(packet[offset-singTun.PacketOffset : offset]) + singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:])) + } + _, err = a.tun.Write(packet[offset-singTun.PacketOffset:]) + if err != nil { + return 0, err + } + } + // WireGuard will not read count. + return 0, nil +} + +func (a *tunDeviceAdapter) MTU() (int, error) { + return a.mtu, nil +} + +func (a *tunDeviceAdapter) Name() (string, error) { + return a.tun.Name() +} + +func (a *tunDeviceAdapter) Events() <-chan wgTun.Event { + return a.events +} + +func (a *tunDeviceAdapter) Close() error { + var err error + a.closeOnce.Do(func() { + close(a.events) + err = a.tun.Close() + }) + return err +} + +func (a *tunDeviceAdapter) BatchSize() int { + if a.linuxTUN != nil { + return a.linuxTUN.BatchSize() + } + return 1 +} + +func (a *tunDeviceAdapter) debugPacket(direction string, packet []byte) { + if !a.debugTun || a.logger == nil { + return + } + var counter *atomic.Uint32 + switch direction { + case "read": + counter = &a.readCount + case "write": + counter = &a.writeCount + default: + return + } + if counter.Add(1) > 8 { + return + } + sample := packet + if len(sample) > 64 { + sample = sample[:64] + } + a.logger.Trace("tailscale tun ", direction, " len=", len(packet), " head=", hex.EncodeToString(sample)) +} diff --git a/protocol/tailscale/tun_device_windows.go b/protocol/tailscale/tun_device_windows.go new file mode 100644 index 00000000..8c9e87ce --- /dev/null +++ b/protocol/tailscale/tun_device_windows.go @@ -0,0 +1,117 @@ +//go:build with_gvisor && windows + +package tailscale + +import ( + "errors" + "os" + "sync" + "sync/atomic" + + singTun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/logger" + wgTun "github.com/sagernet/wireguard-go/tun" +) + +type tunDeviceAdapter struct { + tun singTun.WinTun + nativeTun *singTun.NativeTun + events chan wgTun.Event + mtu atomic.Int64 + closeOnce sync.Once +} + +func newTunDeviceAdapter(tun singTun.Tun, mtu int, _ logger.ContextLogger) (wgTun.Device, error) { + winTun, ok := tun.(singTun.WinTun) + if !ok { + return nil, errors.New("not a windows tun device") + } + nativeTun, ok := winTun.(*singTun.NativeTun) + if !ok { + return nil, errors.New("unsupported windows tun device") + } + if mtu == 0 { + mtu = 1500 + } + adapter := &tunDeviceAdapter{ + tun: winTun, + nativeTun: nativeTun, + events: make(chan wgTun.Event, 1), + } + adapter.mtu.Store(int64(mtu)) + adapter.events <- wgTun.EventUp + return adapter, nil +} + +func (a *tunDeviceAdapter) File() *os.File { + return nil +} + +func (a *tunDeviceAdapter) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { + packet, release, err := a.tun.ReadPacket() + if err != nil { + return 0, err + } + defer release() + sizes[0] = copy(bufs[0][offset-singTun.PacketOffset:], packet) + return 1, nil +} + +func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err error) { + for _, packet := range bufs { + if singTun.PacketOffset > 0 { + singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:])) + } + _, err = a.tun.Write(packet[offset-singTun.PacketOffset:]) + if err != nil { + return 0, err + } + } + return 0, nil +} + +func (a *tunDeviceAdapter) MTU() (int, error) { + return int(a.mtu.Load()), nil +} + +func (a *tunDeviceAdapter) ForceMTU(mtu int) { + if mtu <= 0 { + return + } + update := int(a.mtu.Load()) != mtu + a.mtu.Store(int64(mtu)) + if update { + select { + case a.events <- wgTun.EventMTUUpdate: + default: + } + } +} + +func (a *tunDeviceAdapter) LUID() uint64 { + if a.nativeTun == nil { + return 0 + } + return a.nativeTun.LUID() +} + +func (a *tunDeviceAdapter) Name() (string, error) { + return a.tun.Name() +} + +func (a *tunDeviceAdapter) Events() <-chan wgTun.Event { + return a.events +} + +func (a *tunDeviceAdapter) Close() error { + var err error + a.closeOnce.Do(func() { + close(a.events) + err = a.tun.Close() + }) + return err +} + +func (a *tunDeviceAdapter) BatchSize() int { + return 1 +} diff --git a/protocol/tor/outbound.go b/protocol/tor/outbound.go new file mode 100644 index 00000000..9a0e2d65 --- /dev/null +++ b/protocol/tor/outbound.go @@ -0,0 +1,212 @@ +package tor + +import ( + "context" + "net" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + 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/rw" + "github.com/sagernet/sing/protocol/socks" + + "github.com/cretz/bine/control" + "github.com/cretz/bine/tor" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.TorOutboundOptions](registry, C.TypeTor, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + ctx context.Context + logger logger.ContextLogger + proxy *ProxyListener + startConf *tor.StartConf + options map[string]string + events chan control.Event + instance *tor.Tor + socksClient *socks.Client +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (adapter.Outbound, error) { + var startConf tor.StartConf + startConf.DataDir = os.ExpandEnv(options.DataDirectory) + startConf.TempDataDirBase = os.TempDir() + startConf.ExtraArgs = options.ExtraArgs + if options.DataDirectory != "" { + dataDirAbs, _ := filepath.Abs(startConf.DataDir) + if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.IsFile(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { + options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) + } + if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.IsFile(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { + options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) + } + } + if options.ExecutablePath != "" { + startConf.ExePath = options.ExecutablePath + startConf.ProcessCreator = nil + startConf.UseEmbeddedControlConn = false + } + if startConf.DataDir != "" { + torrcFile := filepath.Join(startConf.DataDir, "torrc") + err := rw.MkdirParent(torrcFile) + if err != nil { + return nil, err + } + if !rw.IsFile(torrcFile) { + err := os.WriteFile(torrcFile, []byte(""), 0o600) + if err != nil { + return nil, err + } + } + startConf.TorrcFile = torrcFile + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, false) + if err != nil { + return nil, err + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, tag, []string{N.NetworkTCP}, options.DialerOptions), + ctx: ctx, + logger: logger, + proxy: NewProxyListener(ctx, logger, outboundDialer), + startConf: &startConf, + options: options.Options, + }, nil +} + +func (t *Outbound) Start() error { + err := t.start() + if err != nil { + t.Close() + } + return err +} + +var torLogEvents = []control.EventCode{ + control.EventCodeLogDebug, + control.EventCodeLogErr, + control.EventCodeLogInfo, + control.EventCodeLogNotice, + control.EventCodeLogWarn, +} + +func (t *Outbound) start() error { + torInstance, err := tor.Start(t.ctx, t.startConf) + if err != nil { + return E.New(strings.ToLower(err.Error())) + } + t.instance = torInstance + t.events = make(chan control.Event, 8) + err = torInstance.Control.AddEventListener(t.events, torLogEvents...) + if err != nil { + return err + } + go t.recvLoop() + err = t.proxy.Start() + if err != nil { + return err + } + proxyPort := "127.0.0.1:" + F.ToString(t.proxy.Port()) + proxyUsername := t.proxy.Username() + proxyPassword := t.proxy.Password() + t.logger.Trace("created upstream proxy at ", proxyPort) + t.logger.Trace("upstream proxy username ", proxyUsername) + t.logger.Trace("upstream proxy password ", proxyPassword) + confOptions := []*control.KeyVal{ + control.NewKeyVal("Socks5Proxy", proxyPort), + control.NewKeyVal("Socks5ProxyUsername", proxyUsername), + control.NewKeyVal("Socks5ProxyPassword", proxyPassword), + } + err = torInstance.Control.ResetConf(confOptions...) + if err != nil { + return err + } + if len(t.options) > 0 { + for key, value := range t.options { + switch key { + case "Socks5Proxy", + "Socks5ProxyUsername", + "Socks5ProxyPassword": + continue + } + err = torInstance.Control.SetConf(control.NewKeyVal(key, value)) + if err != nil { + return E.Cause(err, "set ", key, "=", value) + } + } + } + err = torInstance.EnableNetwork(t.ctx, true) + if err != nil { + return err + } + info, err := torInstance.Control.GetInfo("net/listeners/socks") + if err != nil { + return err + } + if len(info) != 1 || info[0].Key != "net/listeners/socks" { + return E.New("get socks proxy address") + } + t.logger.Trace("obtained tor socks5 address ", info[0].Val) + // TODO: set password for tor socks5 server if supported + t.socksClient = socks.NewClient(N.SystemDialer, M.ParseSocksaddr(info[0].Val), socks.Version5, "", "") + return nil +} + +func (t *Outbound) recvLoop() { + for rawEvent := range t.events { + switch event := rawEvent.(type) { + case *control.LogEvent: + event.Raw = strings.ToLower(event.Raw) + switch event.Severity { + case control.EventCodeLogDebug, control.EventCodeLogInfo: + t.logger.Trace(event.Raw) + case control.EventCodeLogNotice: + if strings.Contains(event.Raw, "disablenetwork") || strings.Contains(event.Raw, "socks listener") { + t.logger.Trace(event.Raw) + continue + } + t.logger.Info(event.Raw) + case control.EventCodeLogWarn: + t.logger.Warn(event.Raw) + case control.EventCodeLogErr: + t.logger.Error(event.Raw) + } + } + } +} + +func (t *Outbound) Close() error { + err := common.Close( + common.PtrOrNil(t.proxy), + common.PtrOrNil(t.instance), + ) + if t.events != nil { + close(t.events) + t.events = nil + } + return err +} + +func (t *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + t.logger.InfoContext(ctx, "outbound connection to ", destination) + return t.socksClient.DialContext(ctx, network, destination) +} + +func (t *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} diff --git a/protocol/tor/proxy.go b/protocol/tor/proxy.go new file mode 100644 index 00000000..378e74fc --- /dev/null +++ b/protocol/tor/proxy.go @@ -0,0 +1,121 @@ +package tor + +import ( + std_bufio "bufio" + "context" + "crypto/rand" + "encoding/hex" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + 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/protocol/socks" + "github.com/sagernet/sing/service" +) + +type ProxyListener struct { + ctx context.Context + logger log.ContextLogger + dialer N.Dialer + connection adapter.ConnectionManager + tcpListener *net.TCPListener + username string + password string + authenticator *auth.Authenticator +} + +func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Dialer) *ProxyListener { + var usernameB [64]byte + var passwordB [64]byte + rand.Read(usernameB[:]) + rand.Read(passwordB[:]) + username := hex.EncodeToString(usernameB[:]) + password := hex.EncodeToString(passwordB[:]) + return &ProxyListener{ + ctx: ctx, + logger: logger, + dialer: dialer, + connection: service.FromContext[adapter.ConnectionManager](ctx), + authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}), + username: username, + password: password, + } +} + +func (l *ProxyListener) Start() error { + tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + }) + if err != nil { + return err + } + l.tcpListener = tcpListener + go l.acceptLoop() + return nil +} + +func (l *ProxyListener) Port() uint16 { + if l.tcpListener == nil { + panic("start listener first") + } + return M.SocksaddrFromNet(l.tcpListener.Addr()).Port +} + +func (l *ProxyListener) Username() string { + return l.username +} + +func (l *ProxyListener) Password() string { + return l.password +} + +func (l *ProxyListener) Close() error { + return common.Close(l.tcpListener) +} + +func (l *ProxyListener) acceptLoop() { + for { + tcpConn, err := l.tcpListener.AcceptTCP() + if err != nil { + return + } + ctx := log.ContextWithNewID(l.ctx) + go func() { + hErr := l.accept(ctx, tcpConn) + if hErr != nil { + if E.IsClosedOrCanceled(hErr) { + l.logger.DebugContext(ctx, E.Cause(hErr, "proxy connection closed")) + return + } + l.logger.ErrorContext(ctx, E.Cause(hErr, "proxy")) + } + }() + } +} + +func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { + return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, nil, 0, M.SocksaddrFromNet(conn.RemoteAddr()), nil) +} + +func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + metadata.Network = N.NetworkTCP + l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination) + l.connection.NewConnection(ctx, l.dialer, conn, metadata, onClose) +} + +func (l *ProxyListener) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + metadata.Network = N.NetworkUDP + l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination) + l.connection.NewPacketConnection(ctx, l.dialer, conn, metadata, onClose) +} diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go new file mode 100644 index 00000000..6e11c088 --- /dev/null +++ b/protocol/trojan/inbound.go @@ -0,0 +1,262 @@ +package trojan + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/trojan" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.TrojanInboundOptions](registry, C.TypeTrojan, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + service *trojan.Service[int] + users []option.TrojanUser + tlsConfig tls.ServerConfig + fallbackAddr M.Socksaddr + fallbackAddrTLSNextProto map[string]M.Socksaddr + transport adapter.V2RayServerTransport +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeTrojan, tag), + router: router, + logger: logger, + users: options.Users, + } + if options.TLS != nil { + tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ + Context: ctx, + Logger: logger, + Options: common.PtrValueOrDefault(options.TLS), + KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && + !common.PtrValueOrDefault(options.Multiplex).Enabled, + }) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + var fallbackHandler N.TCPConnectionHandlerEx + if options.Fallback != nil && options.Fallback.Server != "" || len(options.FallbackForALPN) > 0 { + if options.Fallback != nil && options.Fallback.Server != "" { + inbound.fallbackAddr = options.Fallback.Build() + if !inbound.fallbackAddr.IsValid() { + return nil, E.New("invalid fallback address: ", inbound.fallbackAddr) + } + } + if len(options.FallbackForALPN) > 0 { + if inbound.tlsConfig == nil { + return nil, E.New("fallback for ALPN is not supported without TLS") + } + fallbackAddrNextProto := make(map[string]M.Socksaddr) + for nextProto, destination := range options.FallbackForALPN { + fallbackAddr := destination.Build() + if !fallbackAddr.IsValid() { + return nil, E.New("invalid fallback address for ALPN ", nextProto, ": ", fallbackAddr) + } + fallbackAddrNextProto[nextProto] = fallbackAddr + } + inbound.fallbackAddrTLSNextProto = fallbackAddrNextProto + } + fallbackHandler = adapter.NewUpstreamContextHandlerEx(inbound.fallbackConnection, nil) + } + service := trojan.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger) + err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int { + return index + }), common.Map(options.Users, func(it option.TrojanUser) string { + return it.Password + })) + if err != nil { + return nil, err + } + if options.Transport != nil { + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) + if err != nil { + return nil, E.Cause(err, "create server transport: ", options.Transport.Type) + } + } + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + inbound.service = service + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + if h.transport == nil { + return h.listener.Start() + } + if common.Contains(h.transport.Network(), N.NetworkTCP) { + tcpListener, err := h.listener.ListenTCP() + if err != nil { + return err + } + go func() { + sErr := h.transport.Serve(tcpListener) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + if common.Contains(h.transport.Network(), N.NetworkUDP) { + udpConn, err := h.listener.ListenUDP() + if err != nil { + return err + } + go func() { + sErr := h.transport.ServePacket(udpConn) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + return nil +} + +func (h *Inbound) Close() error { + return common.Close( + h.listener, + h.tlsConfig, + h.transport, + ) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if h.tlsConfig != nil && h.transport == nil { + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return + } + conn = tlsConn + } + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + var fallbackAddr M.Socksaddr + if len(h.fallbackAddrTLSNextProto) > 0 { + if tlsConn, loaded := common.Cast[tls.Conn](conn); loaded { + connectionState := tlsConn.ConnectionState() + if connectionState.NegotiatedProtocol != "" { + if fallbackAddr, loaded = h.fallbackAddrTLSNextProto[connectionState.NegotiatedProtocol]; !loaded { + h.logger.DebugContext(ctx, "process connection from ", metadata.Source, ": fallback disabled for ALPN: ", connectionState.NegotiatedProtocol) + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + } + } + } + if !fallbackAddr.IsValid() { + if !h.fallbackAddr.IsValid() { + h.logger.DebugContext(ctx, "process connection from ", metadata.Source, ": fallback disabled by default") + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + fallbackAddr = h.fallbackAddr + } + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.Destination = fallbackAddr + h.logger.InfoContext(ctx, "fallback connection to ", fallbackAddr) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) + +type inboundTransportHandler Inbound + +func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/trojan/outbound.go b/protocol/trojan/outbound.go new file mode 100644 index 00000000..26c7c81f --- /dev/null +++ b/protocol/trojan/outbound.go @@ -0,0 +1,158 @@ +package trojan + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/trojan" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.TrojanOutboundOptions](registry, C.TypeTrojan, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + dialer N.Dialer + serverAddr M.Socksaddr + key [56]byte + multiplexDialer *mux.Client + tlsConfig tls.Config + tlsDialer tls.Dialer + transport adapter.V2RayClientTransport +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (adapter.Outbound, error) { + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTrojan, tag, options.Network.Build(), options.DialerOptions), + logger: logger, + dialer: outboundDialer, + serverAddr: options.ServerOptions.Build(), + key: trojan.Key(options.Password), + } + if options.TLS != nil { + outbound.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{ + Context: ctx, + Logger: logger, + ServerAddress: options.Server, + Options: common.PtrValueOrDefault(options.TLS), + KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && + !common.PtrValueOrDefault(options.Multiplex).Enabled, + }) + if err != nil { + return nil, err + } + outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig) + } + if options.Transport != nil { + outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) + if err != nil { + return nil, E.Cause(err, "create client transport: ", options.Transport.Type) + } + } + outbound.multiplexDialer, err = mux.NewClientWithOptions((*trojanDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + return outbound, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if h.multiplexDialer == nil { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + return (*trojanDialer)(h).DialContext(ctx, network, destination) + } else { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + } + return h.multiplexDialer.DialContext(ctx, network, destination) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if h.multiplexDialer == nil { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return (*trojanDialer)(h).ListenPacket(ctx, destination) + } else { + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + return h.multiplexDialer.ListenPacket(ctx, destination) + } +} + +func (h *Outbound) InterfaceUpdated() { + if h.transport != nil { + h.transport.Close() + } + if h.multiplexDialer != nil { + h.multiplexDialer.Reset() + } +} + +func (h *Outbound) Close() error { + return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) +} + +type trojanDialer Outbound + +func (h *trojanDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + var conn net.Conn + var err error + if h.transport != nil { + conn, err = h.transport.DialContext(ctx) + } else if h.tlsDialer != nil { + conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) + } else { + conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + } + if err != nil { + common.Close(conn) + return nil, err + } + switch N.NetworkName(network) { + case N.NetworkTCP: + return trojan.NewClientConn(conn, h.key, destination), nil + case N.NetworkUDP: + return bufio.NewBindPacketConn(trojan.NewClientPacketConn(conn, h.key), destination), nil + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (h *trojanDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + conn, err := h.DialContext(ctx, N.NetworkUDP, destination) + if err != nil { + return nil, err + } + return conn.(net.PacketConn), nil +} diff --git a/protocol/tuic/inbound.go b/protocol/tuic/inbound.go new file mode 100644 index 00000000..600c7f93 --- /dev/null +++ b/protocol/tuic/inbound.go @@ -0,0 +1,170 @@ +package tuic + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic/tuic" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/gofrs/uuid/v5" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, NewInbound) +} + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + server *tuic.Service[int] + userNameList []string +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { + options.UDPFragmentDefault = true + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeTUIC, tag), + router: uot.NewRouter(router, logger), + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), + tlsConfig: tlsConfig, + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + service, err := tuic.NewService[int](tuic.ServiceOptions{ + Context: ctx, + Logger: logger, + TLSConfig: tlsConfig, + CongestionControl: options.CongestionControl, + AuthTimeout: time.Duration(options.AuthTimeout), + ZeroRTTHandshake: options.ZeroRTTHandshake, + Heartbeat: time.Duration(options.Heartbeat), + UDPTimeout: udpTimeout, + Handler: inbound, + }) + if err != nil { + return nil, err + } + var userList []int + var userNameList []string + var userUUIDList [][16]byte + var userPasswordList []string + for index, user := range options.Users { + if user.UUID == "" { + return nil, E.New("missing uuid for user ", index) + } + userUUID, err := uuid.FromString(user.UUID) + if err != nil { + return nil, E.Cause(err, "invalid uuid for user ", index) + } + userList = append(userList, index) + userNameList = append(userNameList, user.Name) + userUUIDList = append(userUUIDList, userUUID) + userPasswordList = append(userPasswordList, user.Password) + } + service.UpdateUsers(userList, userUUIDList, userPasswordList) + inbound.server = service + inbound.userNameList = userNameList + return inbound, nil +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + userID, _ := auth.UserFromContext[int](ctx) + if userName := h.userNameList[userID]; userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + userID, _ := auth.UserFromContext[int](ctx) + if userName := h.userNameList[userID]; userName != "" { + metadata.User = userName + h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } + } + packetConn, err := h.listener.ListenUDP() + if err != nil { + return err + } + return h.server.Start(packetConn) +} + +func (h *Inbound) Close() error { + return common.Close( + h.listener, + h.tlsConfig, + common.PtrOrNil(h.server), + ) +} diff --git a/protocol/tuic/outbound.go b/protocol/tuic/outbound.go new file mode 100644 index 00000000..94d3cb77 --- /dev/null +++ b/protocol/tuic/outbound.go @@ -0,0 +1,141 @@ +package tuic + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic/tuic" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" + + "github.com/gofrs/uuid/v5" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, NewOutbound) +} + +var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + client *tuic.Client + udpStream bool +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { + options.UDPFragmentDefault = true + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + userUUID, err := uuid.FromString(options.UUID) + if err != nil { + return nil, E.Cause(err, "invalid uuid") + } + var tuicUDPStream bool + if options.UDPOverStream && options.UDPRelayMode != "" { + return nil, E.New("udp_over_stream is conflict with udp_relay_mode") + } + switch options.UDPRelayMode { + case "native": + case "quic": + tuicUDPStream = true + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + client, err := tuic.NewClient(tuic.ClientOptions{ + Context: ctx, + Dialer: outboundDialer, + ServerAddress: options.ServerOptions.Build(), + TLSConfig: tlsConfig, + UUID: userUUID, + Password: options.Password, + CongestionControl: options.CongestionControl, + UDPStream: tuicUDPStream, + ZeroRTTHandshake: options.ZeroRTTHandshake, + Heartbeat: time.Duration(options.Heartbeat), + }) + if err != nil { + return nil, err + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTUIC, tag, options.Network.Build(), options.DialerOptions), + logger: logger, + client: client, + udpStream: options.UDPOverStream, + }, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialConn(ctx, destination) + case N.NetworkUDP: + if h.udpStream { + h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) + streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) + if err != nil { + return nil, err + } + return uot.NewLazyConn(streamConn, uot.Request{ + IsConnect: true, + Destination: destination, + }), nil + } else { + conn, err := h.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(conn, destination), nil + } + default: + return nil, E.New("unsupported network: ", network) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if h.udpStream { + h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) + streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) + if err != nil { + return nil, err + } + return uot.NewLazyConn(streamConn, uot.Request{ + IsConnect: false, + Destination: destination, + }), nil + } else { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return h.client.ListenPacket(ctx) + } +} + +func (h *Outbound) InterfaceUpdated() { + _ = h.client.CloseWithError(E.New("network changed")) +} + +func (h *Outbound) Close() error { + return h.client.CloseWithError(os.ErrClosed) +} diff --git a/protocol/tun/hook.go b/protocol/tun/hook.go new file mode 100644 index 00000000..1afa643f --- /dev/null +++ b/protocol/tun/hook.go @@ -0,0 +1,3 @@ +package tun + +var HookBeforeCreatePlatformInterface func() diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go new file mode 100644 index 00000000..4b113f4a --- /dev/null +++ b/protocol/tun/inbound.go @@ -0,0 +1,559 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + "runtime" + "strconv" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ranges" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + "go4.org/netipx" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.TunInboundOptions](registry, C.TypeTun, NewInbound) +} + +type Inbound struct { + tag string + ctx context.Context + router adapter.Router + networkManager adapter.NetworkManager + logger log.ContextLogger + tunOptions tun.Options + udpTimeout time.Duration + stack string + tunIf tun.Tun + tunStack tun.Stack + platformInterface adapter.PlatformInterface + platformOptions option.TunPlatformOptions + autoRedirect tun.AutoRedirect + routeRuleSet []adapter.RuleSet + routeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeExcludeRuleSet []adapter.RuleSet + routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeAddressSet []*netipx.IPSet + routeExcludeAddressSet []*netipx.IPSet +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions) (adapter.Inbound, error) { + //nolint:staticcheck + if len(options.Inet4Address) > 0 || len(options.Inet6Address) > 0 || + len(options.Inet4RouteAddress) > 0 || len(options.Inet6RouteAddress) > 0 || + len(options.Inet4RouteExcludeAddress) > 0 || len(options.Inet6RouteExcludeAddress) > 0 { + return nil, E.New("legacy tun address fields are deprecated in sing-box 1.10.0 and removed in sing-box 1.12.0") + } + //nolint:staticcheck + if options.GSO { + return nil, E.New("GSO option in tun is deprecated in sing-box 1.11.0 and removed in sing-box 1.12.0") + } + //nolint:staticcheck + if options.InboundOptions != (option.InboundOptions{}) { + return nil, E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions") + } + + address := options.Address + inet4Address := common.Filter(address, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6Address := common.Filter(address, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + routeAddress := options.RouteAddress + inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + routeExcludeAddress := options.RouteExcludeAddress + inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + tunMTU := options.MTU + enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152 + if tunMTU == 0 { + if platformInterface != nil && platformInterface.UnderNetworkExtension() { + // In Network Extension, when MTU exceeds 4064 (4096-UTUN_IF_HEADROOM_SIZE), the performance of tun will drop significantly, which may be a system bug. + tunMTU = 4064 + } else if C.IsAndroid { + // Some Android devices report ENOBUFS when using MTU 65535 + tunMTU = 9000 + } else { + tunMTU = 65535 + } + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + var err error + includeUID := uidToRange(options.IncludeUID) + if len(options.IncludeUIDRange) > 0 { + includeUID, err = parseRange(includeUID, options.IncludeUIDRange) + if err != nil { + return nil, E.Cause(err, "parse include_uid_range") + } + } + excludeUID := uidToRange(options.ExcludeUID) + if len(options.ExcludeUIDRange) > 0 { + excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) + if err != nil { + return nil, E.Cause(err, "parse exclude_uid_range") + } + } + + tableIndex := options.IPRoute2TableIndex + if tableIndex == 0 { + tableIndex = tun.DefaultIPRoute2TableIndex + } + ruleIndex := options.IPRoute2RuleIndex + if ruleIndex == 0 { + ruleIndex = tun.DefaultIPRoute2RuleIndex + } + autoRedirectFallbackRuleIndex := options.AutoRedirectFallbackRuleIndex + if autoRedirectFallbackRuleIndex == 0 { + autoRedirectFallbackRuleIndex = tun.DefaultIPRoute2AutoRedirectFallbackRuleIndex + } + inputMark := uint32(options.AutoRedirectInputMark) + if inputMark == 0 { + inputMark = tun.DefaultAutoRedirectInputMark + } + outputMark := uint32(options.AutoRedirectOutputMark) + if outputMark == 0 { + outputMark = tun.DefaultAutoRedirectOutputMark + } + resetMark := uint32(options.AutoRedirectResetMark) + if resetMark == 0 { + resetMark = tun.DefaultAutoRedirectResetMark + } + nfQueue := options.AutoRedirectNFQueue + if nfQueue == 0 { + nfQueue = tun.DefaultAutoRedirectNFQueue + } + var includeMACAddress []net.HardwareAddr + for i, macString := range options.IncludeMACAddress { + mac, macErr := net.ParseMAC(macString) + if macErr != nil { + return nil, E.Cause(macErr, "parse include_mac_address[", i, "]") + } + includeMACAddress = append(includeMACAddress, mac) + } + var excludeMACAddress []net.HardwareAddr + for i, macString := range options.ExcludeMACAddress { + mac, macErr := net.ParseMAC(macString) + if macErr != nil { + return nil, E.Cause(macErr, "parse exclude_mac_address[", i, "]") + } + excludeMACAddress = append(excludeMACAddress, mac) + } + networkManager := service.FromContext[adapter.NetworkManager](ctx) + multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000)) + inbound := &Inbound{ + tag: tag, + ctx: ctx, + router: router, + networkManager: networkManager, + logger: logger, + tunOptions: tun.Options{ + Name: options.InterfaceName, + MTU: tunMTU, + GSO: enableGSO, + Inet4Address: inet4Address, + Inet6Address: inet6Address, + AutoRoute: options.AutoRoute, + IPRoute2TableIndex: tableIndex, + IPRoute2RuleIndex: ruleIndex, + IPRoute2AutoRedirectFallbackRuleIndex: autoRedirectFallbackRuleIndex, + AutoRedirectInputMark: inputMark, + AutoRedirectOutputMark: outputMark, + AutoRedirectResetMark: resetMark, + AutoRedirectNFQueue: nfQueue, + ExcludeMPTCP: options.ExcludeMPTCP, + Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4), + Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6), + StrictRoute: options.StrictRoute, + IncludeInterface: options.IncludeInterface, + ExcludeInterface: options.ExcludeInterface, + Inet4RouteAddress: inet4RouteAddress, + Inet6RouteAddress: inet6RouteAddress, + Inet4RouteExcludeAddress: inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: inet6RouteExcludeAddress, + IncludeUID: includeUID, + ExcludeUID: excludeUID, + IncludeAndroidUser: options.IncludeAndroidUser, + IncludePackage: options.IncludePackage, + ExcludePackage: options.ExcludePackage, + IncludeMACAddress: includeMACAddress, + ExcludeMACAddress: excludeMACAddress, + InterfaceMonitor: networkManager.InterfaceMonitor(), + EXP_MultiPendingPackets: multiPendingPackets, + }, + udpTimeout: udpTimeout, + stack: options.Stack, + platformInterface: platformInterface, + platformOptions: common.PtrValueOrDefault(options.Platform), + } + for _, routeAddressSet := range options.RouteAddressSet { + ruleSet, loaded := router.RuleSet(routeAddressSet) + if !loaded { + return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) + } + inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) + } + for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { + ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) + if !loaded { + return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) + } + inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) + } + if options.AutoRedirect { + if !options.AutoRoute { + return nil, E.New("`auto_route` is required by `auto_redirect`") + } + disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) + inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ + TunOptions: &inbound.tunOptions, + Context: ctx, + Handler: (*autoRedirectHandler)(inbound), + Logger: logger, + NetworkMonitor: networkManager.NetworkMonitor(), + InterfaceFinder: networkManager.InterfaceFinder(), + TableName: "sing-box", + DisableNFTables: dErr == nil && disableNFTables, + RouteAddressSet: &inbound.routeAddressSet, + RouteExcludeAddressSet: &inbound.routeExcludeAddressSet, + }) + if err != nil { + return nil, E.Cause(err, "initialize auto-redirect") + } + if !C.IsAndroid { + inbound.tunOptions.AutoRedirectMarkMode = true + err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) + if err != nil { + return nil, err + } + } + } + return inbound, nil +} + +func uidToRange(uidList badoption.Listable[uint32]) []ranges.Range[uint32] { + return common.Map(uidList, func(uid uint32) ranges.Range[uint32] { + return ranges.NewSingle(uid) + }) +} + +func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.Range[uint32], error) { + for _, uidRange := range rangeList { + if !strings.Contains(uidRange, ":") { + return nil, E.New("missing ':' in range: ", uidRange) + } + subIndex := strings.Index(uidRange, ":") + if subIndex == 0 { + return nil, E.New("missing range start: ", uidRange) + } else if subIndex == len(uidRange)-1 { + return nil, E.New("missing range end: ", uidRange) + } + var start, end uint64 + var err error + start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) + if err != nil { + return nil, E.Cause(err, "parse range start") + } + end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32) + if err != nil { + return nil, E.Cause(err, "parse range end") + } + uidRanges = append(uidRanges, ranges.New(uint32(start), uint32(end))) + } + return uidRanges, nil +} + +func (t *Inbound) Type() string { + return C.TypeTun +} + +func (t *Inbound) Tag() string { + return t.tag +} + +func (t *Inbound) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + if C.IsAndroid && t.platformInterface == nil { + t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) + } + if t.tunOptions.Name == "" { + t.tunOptions.Name = tun.CalculateInterfaceName("") + } + if t.platformInterface == nil { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeRuleSet := range t.routeRuleSet { + ipSets := routeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) + } + routeRuleSet.IncRef() + t.routeAddressSet = append(t.routeAddressSet, ipSets...) + if t.autoRedirect != nil { + t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + } + } + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + ipSets := routeExcludeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) + } + routeExcludeRuleSet.IncRef() + t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) + if t.autoRedirect != nil { + t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + } + } + } + var ( + tunInterface tun.Tun + err error + ) + monitor := taskmonitor.New(t.logger, C.StartTimeout) + tunOptions := t.tunOptions + if t.autoRedirect == nil && !(runtime.GOOS == "android" && t.platformInterface != nil) { + for _, ipSet := range t.routeAddressSet { + for _, prefix := range ipSet.Prefixes() { + if prefix.Addr().Is4() { + tunOptions.Inet4RouteAddress = append(tunOptions.Inet4RouteAddress, prefix) + } else { + tunOptions.Inet6RouteAddress = append(tunOptions.Inet6RouteAddress, prefix) + } + } + } + for _, ipSet := range t.routeExcludeAddressSet { + for _, prefix := range ipSet.Prefixes() { + if prefix.Addr().Is4() { + tunOptions.Inet4RouteExcludeAddress = append(tunOptions.Inet4RouteExcludeAddress, prefix) + } else { + tunOptions.Inet6RouteExcludeAddress = append(tunOptions.Inet6RouteExcludeAddress, prefix) + } + } + } + } + monitor.Start("open interface") + if t.platformInterface != nil && t.platformInterface.UsePlatformInterface() { + tunInterface, err = t.platformInterface.OpenInterface(&tunOptions, t.platformOptions) + } else { + if HookBeforeCreatePlatformInterface != nil { + HookBeforeCreatePlatformInterface() + } + tunInterface, err = tun.New(tunOptions) + } + monitor.Finish() + t.tunOptions.Name = tunOptions.Name + if err != nil { + return E.Cause(err, "configure tun interface") + } + t.logger.Trace("creating stack") + t.tunIf = tunInterface + var ( + forwarderBindInterface bool + includeAllNetworks bool + ) + if t.platformInterface != nil { + forwarderBindInterface = true + includeAllNetworks = t.platformInterface.NetworkExtensionIncludeAllNetworks() + } + tunStack, err := tun.NewStack(t.stack, tun.StackOptions{ + Context: t.ctx, + Tun: tunInterface, + TunOptions: t.tunOptions, + UDPTimeout: t.udpTimeout, + Handler: t, + Logger: t.logger, + ForwarderBindInterface: forwarderBindInterface, + InterfaceFinder: t.networkManager.InterfaceFinder(), + IncludeAllNetworks: includeAllNetworks, + }) + if err != nil { + return err + } + t.tunStack = tunStack + t.logger.Info("started at ", t.tunOptions.Name) + case adapter.StartStatePostStart: + monitor := taskmonitor.New(t.logger, C.StartTimeout) + monitor.Start("starting tun stack") + err := t.tunStack.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "starting tun stack") + } + monitor.Start("starting tun interface") + err = t.tunIf.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "starting TUN interface") + } + if t.autoRedirect != nil { + monitor.Start("initialize auto-redirect") + err := t.autoRedirect.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "auto-redirect") + } + } + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil + } + return nil +} + +func (t *Inbound) updateRouteAddressSet(it adapter.RuleSet) { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + t.autoRedirect.UpdateRouteAddressSet() + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil +} + +func (t *Inbound) Close() error { + return common.Close( + t.tunStack, + t.tunIf, + t.autoRedirect, + ) +} + +func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + var ipVersion uint8 + if !destination.IsIPv6() { + ipVersion = 4 + } else { + ipVersion = 6 + } + routeDestination, err := t.router.PreMatch(adapter.InboundContext{ + Inbound: t.tag, + InboundType: C.TypeTun, + IPVersion: ipVersion, + Network: network, + Source: source, + Destination: destination, + }, routeContext, timeout, false) + if err != nil { + switch { + case rule.IsBypassed(err): + err = nil + case rule.IsRejected(err): + t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) + default: + if network == N.NetworkICMP { + t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) + } + } + } + return routeDestination, err +} + +func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = t.tag + metadata.InboundType = C.TypeTun + metadata.Source = source + metadata.Destination = destination + + t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = t.tag + metadata.InboundType = C.TypeTun + metadata.Source = source + metadata.Destination = destination + + t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +type autoRedirectHandler Inbound + +func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + var ipVersion uint8 + if !destination.IsIPv6() { + ipVersion = 4 + } else { + ipVersion = 6 + } + routeDestination, err := t.router.PreMatch(adapter.InboundContext{ + Inbound: t.tag, + InboundType: C.TypeTun, + IPVersion: ipVersion, + Network: network, + Source: source, + Destination: destination, + }, routeContext, timeout, true) + if err != nil { + switch { + case rule.IsBypassed(err): + t.logger.Trace("bypass ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) + case rule.IsRejected(err): + t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) + default: + if network == N.NetworkICMP { + t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) + } + } + } + return routeDestination, err +} + +func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = t.tag + metadata.InboundType = C.TypeTun + metadata.Source = source + metadata.Destination = destination + + t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (t *autoRedirectHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + panic("unexcepted") +} diff --git a/protocol/vless/inbound.go b/protocol/vless/inbound.go new file mode 100644 index 00000000..75cd4124 --- /dev/null +++ b/protocol/vless/inbound.go @@ -0,0 +1,222 @@ +package vless + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-vmess/packetaddr" + "github.com/sagernet/sing-vmess/vless" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/bufio" + 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" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.VLESSInboundOptions](registry, C.TypeVLESS, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + users []option.VLESSUser + service *vless.Service[int] + tlsConfig tls.ServerConfig + transport adapter.V2RayServerTransport +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeVLESS, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + users: options.Users, + } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + service := vless.NewService[int](logger, adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx)) + service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int { + return index + }), common.Map(inbound.users, func(it option.VLESSUser) string { + return it.UUID + }), common.Map(inbound.users, func(it option.VLESSUser) string { + return it.Flow + })) + inbound.service = service + if options.TLS != nil { + inbound.tlsConfig, err = tls.NewServerWithOptions(tls.ServerOptions{ + Context: ctx, + Logger: logger, + Options: common.PtrValueOrDefault(options.TLS), + KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && + !common.PtrValueOrDefault(options.Multiplex).Enabled && + common.All(options.Users, func(it option.VLESSUser) bool { + return it.Flow == "" + }), + }) + if err != nil { + return nil, err + } + } + if options.Transport != nil { + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) + if err != nil { + return nil, E.Cause(err, "create server transport: ", options.Transport.Type) + } + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } + } + if h.transport == nil { + return h.listener.Start() + } + if common.Contains(h.transport.Network(), N.NetworkTCP) { + tcpListener, err := h.listener.ListenTCP() + if err != nil { + return err + } + go func() { + sErr := h.transport.Serve(tcpListener) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + if common.Contains(h.transport.Network(), N.NetworkUDP) { + udpConn, err := h.listener.ListenUDP() + if err != nil { + return err + } + go func() { + sErr := h.transport.ServePacket(udpConn) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + return nil +} + +func (h *Inbound) Close() error { + return common.Close( + h.service, + h.listener, + h.tlsConfig, + h.transport, + ) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if h.tlsConfig != nil && h.transport == nil { + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return + } + conn = tlsConn + } + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { + metadata.Destination = M.Socksaddr{} + conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), metadata.Destination) + h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) + +type inboundTransportHandler Inbound + +func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/vless/outbound.go b/protocol/vless/outbound.go new file mode 100644 index 00000000..d8132cf9 --- /dev/null +++ b/protocol/vless/outbound.go @@ -0,0 +1,218 @@ +package vless + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-vmess/packetaddr" + "github.com/sagernet/sing-vmess/vless" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.VLESSOutboundOptions](registry, C.TypeVLESS, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + dialer N.Dialer + client *vless.Client + serverAddr M.Socksaddr + multiplexDialer *mux.Client + tlsConfig tls.Config + tlsDialer tls.Dialer + transport adapter.V2RayClientTransport + packetAddr bool + xudp bool +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) { + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVLESS, tag, options.Network.Build(), options.DialerOptions), + logger: logger, + dialer: outboundDialer, + serverAddr: options.ServerOptions.Build(), + } + if options.TLS != nil { + outbound.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{ + Context: ctx, + Logger: logger, + ServerAddress: options.Server, + Options: common.PtrValueOrDefault(options.TLS), + KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && + !common.PtrValueOrDefault(options.Multiplex).Enabled && + options.Flow == "", + }) + if err != nil { + return nil, err + } + outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig) + } + if options.Transport != nil { + outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) + if err != nil { + return nil, E.Cause(err, "create client transport: ", options.Transport.Type) + } + } + if options.PacketEncoding == nil { + outbound.xudp = true + } else { + switch *options.PacketEncoding { + case "": + case "packetaddr": + outbound.packetAddr = true + case "xudp": + outbound.xudp = true + default: + return nil, E.New("unknown packet encoding: ", options.PacketEncoding) + } + } + outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger) + if err != nil { + return nil, err + } + outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + return outbound, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if h.multiplexDialer == nil { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + return (*vlessDialer)(h).DialContext(ctx, network, destination) + } else { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + } + return h.multiplexDialer.DialContext(ctx, network, destination) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if h.multiplexDialer == nil { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return (*vlessDialer)(h).ListenPacket(ctx, destination) + } else { + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + return h.multiplexDialer.ListenPacket(ctx, destination) + } +} + +func (h *Outbound) InterfaceUpdated() { + if h.transport != nil { + h.transport.Close() + } + if h.multiplexDialer != nil { + h.multiplexDialer.Reset() + } +} + +func (h *Outbound) Close() error { + return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) +} + +type vlessDialer Outbound + +func (h *vlessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + var conn net.Conn + var err error + if h.transport != nil { + conn, err = h.transport.DialContext(ctx) + } else if h.tlsDialer != nil { + conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) + } else { + conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + } + if err != nil { + return nil, err + } + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialEarlyConn(conn, destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if h.xudp { + return h.client.DialEarlyXUDPPacketConn(conn, destination) + } else if h.packetAddr { + if destination.IsDomain() { + return nil, E.New("packetaddr: domain destination is not supported") + } + packetConn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(packetaddr.NewConn(packetConn, destination), destination), nil + } else { + return h.client.DialEarlyPacketConn(conn, destination) + } + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + var conn net.Conn + var err error + if h.transport != nil { + conn, err = h.transport.DialContext(ctx) + } else if h.tlsDialer != nil { + conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) + } else { + conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + } + if err != nil { + common.Close(conn) + return nil, err + } + if h.xudp { + return h.client.DialEarlyXUDPPacketConn(conn, destination) + } else if h.packetAddr { + if destination.IsDomain() { + return nil, E.New("packetaddr: domain destination is not supported") + } + conn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) + if err != nil { + return nil, err + } + return packetaddr.NewConn(conn, destination), nil + } else { + return h.client.DialEarlyPacketConn(conn, destination) + } +} diff --git a/protocol/vmess/inbound.go b/protocol/vmess/inbound.go new file mode 100644 index 00000000..4e9c763c --- /dev/null +++ b/protocol/vmess/inbound.go @@ -0,0 +1,228 @@ +package vmess + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing-vmess/packetaddr" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/bufio" + 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" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.VMessInboundOptions](registry, C.TypeVMess, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service *vmess.Service[int] + users []option.VMessUser + tlsConfig tls.ServerConfig + transport adapter.V2RayServerTransport +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeVMess, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + users: options.Users, + } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + var serviceOptions []vmess.ServiceOption + if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil { + serviceOptions = append(serviceOptions, vmess.ServiceWithTimeFunc(timeFunc)) + } + if options.Transport != nil && options.Transport.Type != "" { + serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection()) + } + service := vmess.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...) + inbound.service = service + err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int { + return index + }), common.Map(options.Users, func(it option.VMessUser) string { + return it.UUID + }), common.Map(options.Users, func(it option.VMessUser) int { + return it.AlterId + })) + if err != nil { + return nil, err + } + if options.TLS != nil { + inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + } + if options.Transport != nil { + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) + if err != nil { + return nil, E.Cause(err, "create server transport: ", options.Transport.Type) + } + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + err := h.service.Start() + if err != nil { + return err + } + if h.tlsConfig != nil { + err = h.tlsConfig.Start() + if err != nil { + return err + } + } + if h.transport == nil { + return h.listener.Start() + } + if common.Contains(h.transport.Network(), N.NetworkTCP) { + tcpListener, err := h.listener.ListenTCP() + if err != nil { + return err + } + go func() { + sErr := h.transport.Serve(tcpListener) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + if common.Contains(h.transport.Network(), N.NetworkUDP) { + udpConn, err := h.listener.ListenUDP() + if err != nil { + return err + } + go func() { + sErr := h.transport.ServePacket(udpConn) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + return nil +} + +func (h *Inbound) Close() error { + return common.Close( + h.service, + h.listener, + h.tlsConfig, + h.transport, + ) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if h.tlsConfig != nil && h.transport == nil { + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return + } + conn = tlsConn + } + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { + metadata.Destination = M.Socksaddr{} + conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), metadata.Destination) + h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) + +type inboundTransportHandler Inbound + +func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/vmess/outbound.go b/protocol/vmess/outbound.go new file mode 100644 index 00000000..703f06b1 --- /dev/null +++ b/protocol/vmess/outbound.go @@ -0,0 +1,206 @@ +package vmess + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing-vmess/packetaddr" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.VMessOutboundOptions](registry, C.TypeVMess, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + dialer N.Dialer + client *vmess.Client + serverAddr M.Socksaddr + multiplexDialer *mux.Client + tlsConfig tls.Config + tlsDialer tls.Dialer + transport adapter.V2RayClientTransport + packetAddr bool + xudp bool +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (adapter.Outbound, error) { + outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + if err != nil { + return nil, err + } + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVMess, tag, options.Network.Build(), options.DialerOptions), + logger: logger, + dialer: outboundDialer, + serverAddr: options.ServerOptions.Build(), + } + if options.TLS != nil { + outbound.tlsConfig, err = tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + if outbound.tlsConfig != nil { + outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig) + } + } + if options.Transport != nil { + outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) + if err != nil { + return nil, E.Cause(err, "create client transport: ", options.Transport.Type) + } + } + outbound.multiplexDialer, err = mux.NewClientWithOptions((*vmessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + switch options.PacketEncoding { + case "": + case "packetaddr": + outbound.packetAddr = true + case "xudp": + outbound.xudp = true + default: + return nil, E.New("unknown packet encoding: ", options.PacketEncoding) + } + var clientOptions []vmess.ClientOption + if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil { + clientOptions = append(clientOptions, vmess.ClientWithTimeFunc(timeFunc)) + } + if options.GlobalPadding { + clientOptions = append(clientOptions, vmess.ClientWithGlobalPadding()) + } + if options.AuthenticatedLength { + clientOptions = append(clientOptions, vmess.ClientWithAuthenticatedLength()) + } + security := options.Security + if security == "" { + security = "auto" + } + if security == "auto" && outbound.tlsConfig != nil { + security = "zero" + } + client, err := vmess.NewClient(options.UUID, security, options.AlterId, clientOptions...) + if err != nil { + return nil, err + } + outbound.client = client + return outbound, nil +} + +func (h *Outbound) InterfaceUpdated() { + if h.transport != nil { + h.transport.Close() + } + if h.multiplexDialer != nil { + h.multiplexDialer.Reset() + } +} + +func (h *Outbound) Close() error { + return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if h.multiplexDialer == nil { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + return (*vmessDialer)(h).DialContext(ctx, network, destination) + } else { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + } + return h.multiplexDialer.DialContext(ctx, network, destination) + } +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if h.multiplexDialer == nil { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return (*vmessDialer)(h).ListenPacket(ctx, destination) + } else { + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + return h.multiplexDialer.ListenPacket(ctx, destination) + } +} + +type vmessDialer Outbound + +func (h *vmessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + var conn net.Conn + var err error + if h.transport != nil { + conn, err = h.transport.DialContext(ctx) + } else if h.tlsDialer != nil { + conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) + } else { + conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + } + if err != nil { + common.Close(conn) + return nil, err + } + switch N.NetworkName(network) { + case N.NetworkTCP: + return h.client.DialEarlyConn(conn, destination), nil + case N.NetworkUDP: + return h.client.DialEarlyPacketConn(conn, destination), nil + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + var conn net.Conn + var err error + if h.transport != nil { + conn, err = h.transport.DialContext(ctx) + } else if h.tlsDialer != nil { + conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) + } else { + conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + } + if err != nil { + return nil, err + } + if h.packetAddr { + if destination.IsDomain() { + return nil, E.New("packetaddr: domain destination is not supported") + } + return packetaddr.NewConn(h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}), destination), nil + } else if h.xudp { + return h.client.DialEarlyXUDPPacketConn(conn, destination), nil + } else { + return h.client.DialEarlyPacketConn(conn, destination), nil + } +} diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go new file mode 100644 index 00000000..9fdc4814 --- /dev/null +++ b/protocol/wireguard/endpoint.go @@ -0,0 +1,265 @@ +package wireguard + +import ( + "context" + "net" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-box/transport/wireguard" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +var ( + _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) + _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) +) + +func RegisterEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint) +} + +type Endpoint struct { + endpoint.Adapter + ctx context.Context + router adapter.Router + dnsRouter adapter.DNSRouter + logger logger.ContextLogger + localAddresses []netip.Prefix + endpoint *wireguard.Endpoint +} + +func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) { + ep := &Endpoint{ + Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), + ctx: ctx, + router: router, + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + logger: logger, + localAddresses: options.Address, + } + if options.Detour != "" && options.ListenPort != 0 { + return nil, E.New("`listen_port` is conflict with `detour`") + } + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: common.Any(options.Peers, func(it option.WireGuardPeer) bool { + return !M.ParseAddr(it.Address).IsValid() + }), + ResolverOnDetour: true, + }) + if err != nil { + return nil, err + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{ + Context: ctx, + Logger: logger, + System: options.System, + Handler: ep, + UDPTimeout: udpTimeout, + Dialer: outboundDialer, + CreateDialer: func(interfaceName string) N.Dialer { + return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{ + BindInterface: interfaceName, + })) + }, + Name: options.Name, + MTU: options.MTU, + Address: options.Address, + PrivateKey: options.PrivateKey, + ListenPort: options.ListenPort, + ResolvePeer: func(domain string) (netip.Addr, error) { + endpointAddresses, lookupErr := ep.dnsRouter.Lookup(ctx, domain, outboundDialer.(dialer.ResolveDialer).QueryOptions()) + if lookupErr != nil { + return netip.Addr{}, lookupErr + } + return endpointAddresses[0], nil + }, + Peers: common.Map(options.Peers, func(it option.WireGuardPeer) wireguard.PeerOptions { + return wireguard.PeerOptions{ + Endpoint: M.ParseSocksaddrHostPort(it.Address, it.Port), + PublicKey: it.PublicKey, + PreSharedKey: it.PreSharedKey, + AllowedIPs: it.AllowedIPs, + PersistentKeepaliveInterval: it.PersistentKeepaliveInterval, + Reserved: it.Reserved, + } + }), + Workers: options.Workers, + }) + if err != nil { + return nil, err + } + ep.endpoint = wgEndpoint + return ep, nil +} + +func (w *Endpoint) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + return w.endpoint.Start(false) + case adapter.StartStatePostStart: + return w.endpoint.Start(true) + } + return nil +} + +func (w *Endpoint) Close() error { + return w.endpoint.Close() +} + +func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + var ipVersion uint8 + if !destination.IsIPv6() { + ipVersion = 4 + } else { + ipVersion = 6 + } + routeDestination, err := w.router.PreMatch(adapter.InboundContext{ + Inbound: w.Tag(), + InboundType: w.Type(), + IPVersion: ipVersion, + Network: network, + Source: source, + Destination: destination, + }, routeContext, timeout, false) + if err != nil { + switch { + case rule.IsBypassed(err): + err = nil + case rule.IsRejected(err): + w.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) + default: + if network == N.NetworkICMP { + w.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) + } + } + } + return routeDestination, err +} + +func (w *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = w.Tag() + metadata.InboundType = w.Type() + metadata.Source = source + for _, localPrefix := range w.localAddresses { + if localPrefix.Contains(destination.Addr) { + metadata.OriginDestination = destination + if destination.Addr.Is4() { + destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + } else { + destination.Addr = netip.IPv6Loopback() + } + break + } + } + metadata.Destination = destination + w.logger.InfoContext(ctx, "inbound connection from ", source) + w.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + w.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (w *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = w.Tag() + metadata.InboundType = w.Type() + metadata.Source = source + metadata.Destination = destination + for _, localPrefix := range w.localAddresses { + if localPrefix.Contains(destination.Addr) { + metadata.OriginDestination = destination + if destination.Addr.Is4() { + metadata.Destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + } else { + metadata.Destination.Addr = netip.IPv6Loopback() + } + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) + } + } + w.logger.InfoContext(ctx, "inbound packet connection from ", source) + w.logger.InfoContext(ctx, "inbound packet connection to ", destination) + w.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (w *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case N.NetworkTCP: + w.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + w.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + if destination.IsDomain() { + destinationAddresses, err := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, w.endpoint, network, destination, destinationAddresses) + } else if !destination.Addr.IsValid() { + return nil, E.New("invalid destination: ", destination) + } + return w.endpoint.DialContext(ctx, network, destination) +} + +func (w *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { + w.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if destination.IsDomain() { + destinationAddresses, err := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) + if err != nil { + return nil, netip.Addr{}, err + } + return N.ListenSerial(ctx, w.endpoint, destination, destinationAddresses) + } + packetConn, err := w.endpoint.ListenPacket(ctx, destination) + if err != nil { + return nil, netip.Addr{}, err + } + if destination.IsIP() { + return packetConn, destination.Addr, nil + } + return packetConn, netip.Addr{}, nil +} + +func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + packetConn, destinationAddress, err := w.ListenPacketWithDestination(ctx, destination) + if err != nil { + return nil, err + } + if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) { + return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil + } + return packetConn, nil +} + +func (w *Endpoint) PreferredDomain(domain string) bool { + return false +} + +func (w *Endpoint) PreferredAddress(address netip.Addr) bool { + return w.endpoint.Lookup(address) != nil +} + +func (w *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + return w.endpoint.NewDirectRouteConnection(metadata, routeContext, timeout) +} diff --git a/release/DEFAULT_BUILD_TAGS b/release/DEFAULT_BUILD_TAGS new file mode 100644 index 00000000..e06bc120 --- /dev/null +++ b/release/DEFAULT_BUILD_TAGS @@ -0,0 +1 @@ +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,badlinkname,tfogo_checklinkname0 \ No newline at end of file diff --git a/release/DEFAULT_BUILD_TAGS_OTHERS b/release/DEFAULT_BUILD_TAGS_OTHERS new file mode 100644 index 00000000..a28e900e --- /dev/null +++ b/release/DEFAULT_BUILD_TAGS_OTHERS @@ -0,0 +1 @@ +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,badlinkname,tfogo_checklinkname0 \ No newline at end of file diff --git a/release/DEFAULT_BUILD_TAGS_WINDOWS b/release/DEFAULT_BUILD_TAGS_WINDOWS new file mode 100644 index 00000000..af4fe416 --- /dev/null +++ b/release/DEFAULT_BUILD_TAGS_WINDOWS @@ -0,0 +1 @@ +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0 \ No newline at end of file diff --git a/release/LDFLAGS b/release/LDFLAGS new file mode 100644 index 00000000..8f613f97 --- /dev/null +++ b/release/LDFLAGS @@ -0,0 +1 @@ +-X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0 \ No newline at end of file diff --git a/release/completions/sing-box.bash b/release/completions/sing-box.bash new file mode 100644 index 00000000..22df7763 --- /dev/null +++ b/release/completions/sing-box.bash @@ -0,0 +1,1580 @@ +# bash completion for sing-box -*- shell-script -*- + +__sing-box_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__sing-box_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__sing-box_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__sing-box_contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__sing-box_handle_go_custom_completion() +{ + __sing-box_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" + + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + + local out requestComp lastParam lastChar comp directive args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly sing-box allows handling aliases + args=("${words[@]:1}") + # Disable ActiveHelp which is not supported for bash completion v1 + requestComp="SING_BOX_ACTIVE_HELP=0 ${words[0]} __completeNoDesc ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __sing-box_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" + + if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __sing-box_debug "${FUNCNAME[0]}: Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __sing-box_debug "${FUNCNAME[0]}: calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%:*} + if [ "${directive}" = "${out}" ]; then + # There is not directive specified + directive=0 + fi + __sing-box_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" + __sing-box_debug "${FUNCNAME[0]}: the completions are: ${out}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + # Error code. No completion. + __sing-box_debug "${FUNCNAME[0]}: received error from custom completion go code" + return + else + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __sing-box_debug "${FUNCNAME[0]}: activating no space" + compopt -o nospace + fi + fi + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __sing-box_debug "${FUNCNAME[0]}: activating no file completion" + compopt +o default + fi + fi + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local fullFilter filter filteringCmd + # Do not use quotes around the $out variable or else newline + # characters will be kept. + for filter in ${out}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __sing-box_debug "File filtering command: $filteringCmd" + $filteringCmd + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + # Use printf to strip any trailing newline + subdir=$(printf "%s" "${out}") + if [ -n "$subdir" ]; then + __sing-box_debug "Listing directories in $subdir" + __sing-box_handle_subdirs_in_dir_flag "$subdir" + else + __sing-box_debug "Listing directories in ." + _filedir -d + fi + else + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${out}" -- "$cur") + fi +} + +__sing-box_handle_reply() +{ + __sing-box_debug "${FUNCNAME[0]}" + local comp + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${allflags[*]}" -- "$cur") + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace + fi + + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%=*}" + __sing-box_index_of_word "${flag}" "${flags_with_completion[@]}" + COMPREPLY=() + if [[ ${index} -ge 0 ]]; then + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION:-}" ]; then + # zsh completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi + fi + + if [[ -z "${flag_parsing_disabled}" ]]; then + # If flag parsing is enabled, we have completed the flags and can return. + # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough + # to possibly call handle_go_custom_completion. + return 0; + fi + ;; + esac + + # check if we are handling a flag with special work handling + local index + __sing-box_index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi + + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions+=("${must_have_one_noun[@]}") + elif [[ -n "${has_completion_function}" ]]; then + # if a go completion function is provided, defer to that function + __sing-box_handle_go_custom_completion + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${completions[*]}" -- "$cur") + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${noun_aliases[*]}" -- "$cur") + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + if declare -F __sing-box_custom_func >/dev/null; then + # try command name qualified custom func + __sing-box_custom_func + else + # otherwise fall back to unqualified for compatibility + declare -F __custom_func >/dev/null && __custom_func + fi + fi + + # available in bash-completion >= 2, not always present on macOS + if declare -F __ltrim_colon_completions >/dev/null; then + __ltrim_colon_completions "$cur" + fi + + # If there is only 1 completion and it is a flag with an = it will be completed + # but we don't want a space after the = + if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then + compopt -o nospace + fi +} + +# The arguments should be in the form "ext1|ext2|extn" +__sing-box_handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__sing-box_handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return +} + +__sing-box_handle_flag() +{ + __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue="" + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __sing-box_debug "${FUNCNAME[0]}: looking for ${flagname}" + if __sing-box_contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __sing-box_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + # flaghash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + fi + + # skip the argument to a two word flag + if [[ ${words[c]} != *"="* ]] && __sing-box_contains_word "${words[c]}" "${two_word_flags[@]}"; then + __sing-box_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() + fi + fi + + c=$((c+1)) + +} + +__sing-box_handle_noun() +{ + __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __sing-box_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __sing-box_contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() + fi + + nouns+=("${words[c]}") + c=$((c+1)) +} + +__sing-box_handle_command() +{ + __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_sing-box_root_command" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + __sing-box_debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F "$next_command" >/dev/null && $next_command +} + +__sing-box_handle_word() +{ + if [[ $c -ge $cword ]]; then + __sing-box_handle_reply + return + fi + __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __sing-box_handle_flag + elif __sing-box_contains_word "${words[c]}" "${commands[@]}"; then + __sing-box_handle_command + elif [[ $c -eq 0 ]]; then + __sing-box_handle_command + elif __sing-box_contains_word "${words[c]}" "${command_aliases[@]}"; then + # aliashash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + words[c]=${aliashash[${words[c]}]} + __sing-box_handle_command + else + __sing-box_handle_noun + fi + else + __sing-box_handle_noun + fi + __sing-box_handle_word +} + +_sing-box_check() +{ + last_command="sing-box_check" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_format() +{ + last_command="sing-box_format" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--write") + flags+=("-w") + local_nonpersistent_flags+=("--write") + local_nonpersistent_flags+=("-w") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_ech-keypair() +{ + last_command="sing-box_generate_ech-keypair" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--pq-signature-schemes-enabled") + local_nonpersistent_flags+=("--pq-signature-schemes-enabled") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_rand() +{ + last_command="sing-box_generate_rand" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--base64") + local_nonpersistent_flags+=("--base64") + flags+=("--hex") + local_nonpersistent_flags+=("--hex") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_reality-keypair() +{ + last_command="sing-box_generate_reality-keypair" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_tls-keypair() +{ + last_command="sing-box_generate_tls-keypair" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--months=") + two_word_flags+=("--months") + two_word_flags+=("-m") + local_nonpersistent_flags+=("--months") + local_nonpersistent_flags+=("--months=") + local_nonpersistent_flags+=("-m") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_uuid() +{ + last_command="sing-box_generate_uuid" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_vapid-keypair() +{ + last_command="sing-box_generate_vapid-keypair" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate_wg-keypair() +{ + last_command="sing-box_generate_wg-keypair" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_generate() +{ + last_command="sing-box_generate" + + command_aliases=() + + commands=() + commands+=("ech-keypair") + commands+=("rand") + commands+=("reality-keypair") + commands+=("tls-keypair") + commands+=("uuid") + commands+=("vapid-keypair") + commands+=("wg-keypair") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geoip_export() +{ + last_command="sing-box_geoip_export" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output") + local_nonpersistent_flags+=("--output=") + local_nonpersistent_flags+=("-o") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geoip_list() +{ + last_command="sing-box_geoip_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geoip_lookup() +{ + last_command="sing-box_geoip_lookup" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geoip() +{ + last_command="sing-box_geoip" + + command_aliases=() + + commands=() + commands+=("export") + commands+=("list") + commands+=("lookup") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geosite_export() +{ + last_command="sing-box_geosite_export" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output") + local_nonpersistent_flags+=("--output=") + local_nonpersistent_flags+=("-o") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geosite_list() +{ + last_command="sing-box_geosite_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geosite_lookup() +{ + last_command="sing-box_geosite_lookup" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_geosite() +{ + last_command="sing-box_geosite" + + command_aliases=() + + commands=() + commands+=("export") + commands+=("list") + commands+=("lookup") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-f") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_merge() +{ + last_command="sing-box_merge" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_compile() +{ + last_command="sing-box_rule-set_compile" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output") + local_nonpersistent_flags+=("--output=") + local_nonpersistent_flags+=("-o") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_convert() +{ + last_command="sing-box_rule-set_convert" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output") + local_nonpersistent_flags+=("--output=") + local_nonpersistent_flags+=("-o") + flags+=("--type=") + two_word_flags+=("--type") + two_word_flags+=("-t") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + local_nonpersistent_flags+=("-t") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_decompile() +{ + last_command="sing-box_rule-set_decompile" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output") + local_nonpersistent_flags+=("--output=") + local_nonpersistent_flags+=("-o") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_format() +{ + last_command="sing-box_rule-set_format" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--write") + flags+=("-w") + local_nonpersistent_flags+=("--write") + local_nonpersistent_flags+=("-w") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_match() +{ + last_command="sing-box_rule-set_match" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--format=") + two_word_flags+=("--format") + two_word_flags+=("-f") + local_nonpersistent_flags+=("--format") + local_nonpersistent_flags+=("--format=") + local_nonpersistent_flags+=("-f") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_merge() +{ + last_command="sing-box_rule-set_merge" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set_upgrade() +{ + last_command="sing-box_rule-set_upgrade" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--write") + flags+=("-w") + local_nonpersistent_flags+=("--write") + local_nonpersistent_flags+=("-w") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_rule-set() +{ + last_command="sing-box_rule-set" + + command_aliases=() + + commands=() + commands+=("compile") + commands+=("convert") + commands+=("decompile") + commands+=("format") + commands+=("match") + commands+=("merge") + commands+=("upgrade") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_run() +{ + last_command="sing-box_run" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_tools_connect() +{ + last_command="sing-box_tools_connect" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--network=") + two_word_flags+=("--network") + two_word_flags+=("-n") + local_nonpersistent_flags+=("--network") + local_nonpersistent_flags+=("--network=") + local_nonpersistent_flags+=("-n") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--outbound=") + two_word_flags+=("--outbound") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_tools_fetch() +{ + last_command="sing-box_tools_fetch" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--outbound=") + two_word_flags+=("--outbound") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_tools_synctime() +{ + last_command="sing-box_tools_synctime" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--format=") + two_word_flags+=("--format") + two_word_flags+=("-f") + local_nonpersistent_flags+=("--format") + local_nonpersistent_flags+=("--format=") + local_nonpersistent_flags+=("-f") + flags+=("--server=") + two_word_flags+=("--server") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--server") + local_nonpersistent_flags+=("--server=") + local_nonpersistent_flags+=("-s") + flags+=("--write") + flags+=("-w") + local_nonpersistent_flags+=("--write") + local_nonpersistent_flags+=("-w") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + flags+=("--outbound=") + two_word_flags+=("--outbound") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_tools() +{ + last_command="sing-box_tools" + + command_aliases=() + + commands=() + commands+=("connect") + commands+=("fetch") + commands+=("synctime") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--outbound=") + two_word_flags+=("--outbound") + two_word_flags+=("-o") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_version() +{ + last_command="sing-box_version" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--name") + flags+=("-n") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("-n") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_sing-box_root_command() +{ + last_command="sing-box" + + command_aliases=() + + commands=() + commands+=("check") + commands+=("format") + commands+=("generate") + commands+=("geoip") + commands+=("geosite") + commands+=("merge") + commands+=("rule-set") + commands+=("run") + commands+=("tools") + commands+=("version") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +__start_sing-box() +{ + local cur prev words cword split + declare -A flaghash 2>/dev/null || : + declare -A aliashash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __sing-box_init_completion -n "=" || return + fi + + local c=0 + local flag_parsing_disabled= + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("sing-box") + local command_aliases=() + local must_have_one_flag=() + local must_have_one_noun=() + local has_completion_function="" + local last_command="" + local nouns=() + local noun_aliases=() + + __sing-box_handle_word +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_sing-box sing-box +else + complete -o default -o nospace -F __start_sing-box sing-box +fi + +# ex: ts=4 sw=4 et filetype=sh diff --git a/release/completions/sing-box.fish b/release/completions/sing-box.fish new file mode 100644 index 00000000..2aec1e1d --- /dev/null +++ b/release/completions/sing-box.fish @@ -0,0 +1,235 @@ +# fish completion for sing-box -*- shell-script -*- + +function __sing_box_debug + set -l file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end +end + +function __sing_box_perform_completion + __sing_box_debug "Starting __sing_box_perform_completion" + + # Extract all args except the last one + set -l args (commandline -opc) + # Extract the last arg and escape it in case it is a space + set -l lastArg (string escape -- (commandline -ct)) + + __sing_box_debug "args: $args" + __sing_box_debug "last arg: $lastArg" + + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "SING_BOX_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" + + __sing_box_debug "Calling $requestComp" + set -l results (eval $requestComp 2> /dev/null) + + # Some programs may output extra empty lines after the directive. + # Let's ignore them or else it will break completion. + # Ref: https://github.com/spf13/cobra/issues/1279 + for line in $results[-1..1] + if test (string trim -- $line) = "" + # Found an empty line, remove it + set results $results[1..-2] + else + # Found non-empty line, we have our proper output + break + end + end + + set -l comps $results[1..-2] + set -l directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") + + __sing_box_debug "Comps: $comps" + __sing_box_debug "DirectiveLine: $directiveLine" + __sing_box_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%s%s\n" "$flagPrefix" "$comp" + end + + printf "%s\n" "$directiveLine" +end + +# this function limits calls to __sing_box_perform_completion, by caching the result behind $__sing_box_perform_completion_once_result +function __sing_box_perform_completion_once + __sing_box_debug "Starting __sing_box_perform_completion_once" + + if test -n "$__sing_box_perform_completion_once_result" + __sing_box_debug "Seems like a valid result already exists, skipping __sing_box_perform_completion" + return 0 + end + + set --global __sing_box_perform_completion_once_result (__sing_box_perform_completion) + if test -z "$__sing_box_perform_completion_once_result" + __sing_box_debug "No completions, probably due to a failure" + return 1 + end + + __sing_box_debug "Performed completions and set __sing_box_perform_completion_once_result" + return 0 +end + +# this function is used to clear the $__sing_box_perform_completion_once_result variable after completions are run +function __sing_box_clear_perform_completion_once_result + __sing_box_debug "" + __sing_box_debug "========= clearing previously set __sing_box_perform_completion_once_result variable ==========" + set --erase __sing_box_perform_completion_once_result + __sing_box_debug "Successfully erased the variable __sing_box_perform_completion_once_result" +end + +function __sing_box_requires_order_preservation + __sing_box_debug "" + __sing_box_debug "========= checking if order preservation is required ==========" + + __sing_box_perform_completion_once + if test -z "$__sing_box_perform_completion_once_result" + __sing_box_debug "Error determining if order preservation is required" + return 1 + end + + set -l directive (string sub --start 2 $__sing_box_perform_completion_once_result[-1]) + __sing_box_debug "Directive is: $directive" + + set -l shellCompDirectiveKeepOrder 32 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) + __sing_box_debug "Keeporder is: $keeporder" + + if test $keeporder -ne 0 + __sing_box_debug "This does require order preservation" + return 0 + end + + __sing_box_debug "This doesn't require order preservation" + return 1 +end + + +# This function does two things: +# - Obtain the completions and store them in the global __sing_box_comp_results +# - Return false if file completion should be performed +function __sing_box_prepare_completions + __sing_box_debug "" + __sing_box_debug "========= starting completion logic ==========" + + # Start fresh + set --erase __sing_box_comp_results + + __sing_box_perform_completion_once + __sing_box_debug "Completion results: $__sing_box_perform_completion_once_result" + + if test -z "$__sing_box_perform_completion_once_result" + __sing_box_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + return 1 + end + + set -l directive (string sub --start 2 $__sing_box_perform_completion_once_result[-1]) + set --global __sing_box_comp_results $__sing_box_perform_completion_once_result[1..-2] + + __sing_box_debug "Completions are: $__sing_box_comp_results" + __sing_box_debug "Directive is: $directive" + + set -l shellCompDirectiveError 1 + set -l shellCompDirectiveNoSpace 2 + set -l shellCompDirectiveNoFileComp 4 + set -l shellCompDirectiveFilterFileExt 8 + set -l shellCompDirectiveFilterDirs 16 + + if test -z "$directive" + set directive 0 + end + + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) + if test $compErr -eq 1 + __sing_box_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + return 1 + end + + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) + if test $filefilter -eq 1; or test $dirfilter -eq 1 + __sing_box_debug "File extension filtering or directory filtering not supported" + # Do full file completion instead + return 1 + end + + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) + + __sing_box_debug "nospace: $nospace, nofiles: $nofiles" + + # If we want to prevent a space, or if file completion is NOT disabled, + # we need to count the number of valid completions. + # To do so, we will filter on prefix as the completions we have received + # may not already be filtered so as to allow fish to match on different + # criteria than the prefix. + if test $nospace -ne 0; or test $nofiles -eq 0 + set -l prefix (commandline -t | string escape --style=regex) + __sing_box_debug "prefix: $prefix" + + set -l completions (string match -r -- "^$prefix.*" $__sing_box_comp_results) + set --global __sing_box_comp_results $completions + __sing_box_debug "Filtered completions are: $__sing_box_comp_results" + + # Important not to quote the variable for count to work + set -l numComps (count $__sing_box_comp_results) + __sing_box_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # We must first split on \t to get rid of the descriptions to be + # able to check what the actual completion will be. + # We don't need descriptions anyway since there is only a single + # real completion which the shell will expand immediately. + set -l split (string split --max 1 \t $__sing_box_comp_results[1]) + + # Fish won't add a space if the completion ends with any + # of the following characters: @=/:., + set -l lastChar (string sub -s -1 -- $split) + if not string match -r -q "[@=/:.,]" -- "$lastChar" + # In other cases, to support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __sing_box_debug "Adding second completion to perform nospace directive" + set --global __sing_box_comp_results $split[1] $split[1]. + __sing_box_debug "Completions are now: $__sing_box_comp_results" + end + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + # To be consistent with bash and zsh, we only trigger file + # completion when there are no other completions + __sing_box_debug "Requesting file completion" + return 1 + end + end + + return 0 +end + +# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves +# so we can properly delete any completions provided by another script. +# Only do this if the program can be found, or else fish may print some errors; besides, +# the existing completions will only be loaded if the program can be found. +if type -q "sing-box" + # The space after the program name is essential to trigger completion for the program + # and not completion of the program name itself. + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. + complete --do-complete "sing-box " > /dev/null 2>&1 +end + +# Remove any pre-existing completions for the program since we will be handling all of them. +complete -c sing-box -e + +# this will get called after the two calls below and clear the $__sing_box_perform_completion_once_result global +complete -c sing-box -n '__sing_box_clear_perform_completion_once_result' +# The call to __sing_box_prepare_completions will setup __sing_box_comp_results +# which provides the program's completion choices. +# If this doesn't require order preservation, we don't use the -k flag +complete -c sing-box -n 'not __sing_box_requires_order_preservation && __sing_box_prepare_completions' -f -a '$__sing_box_comp_results' +# otherwise we use the -k flag +complete -k -c sing-box -n '__sing_box_requires_order_preservation && __sing_box_prepare_completions' -f -a '$__sing_box_comp_results' diff --git a/release/completions/sing-box.zsh b/release/completions/sing-box.zsh new file mode 100644 index 00000000..96200556 --- /dev/null +++ b/release/completions/sing-box.zsh @@ -0,0 +1,212 @@ +#compdef sing-box +compdef _sing-box sing-box + +# zsh completion for sing-box -*- shell-script -*- + +__sing-box_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_sing-box() +{ + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __sing-box_debug "\n========= starting completion logic ==========" + __sing-box_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") + __sing-box_debug "Truncated words[*]: ${words[*]}," + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __sing-box_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., sing-box -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} __complete ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __sing-box_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __sing-box_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __sing-box_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS='\n' read -r line; do + lastLine=${line} + done < <(printf "%s\n" "${out[@]}") + __sing-box_debug "last line: ${lastLine}" + + if [ "${lastLine[1]}" = : ]; then + directive=${lastLine[2,-1]} + # Remove the directive including the : and the newline + local suffix + (( suffix=${#lastLine}+2)) + out=${out[1,-$suffix]} + else + # There is no directive specified. Leave $out as is. + __sing-box_debug "No directive found. Setting do default" + directive=0 + fi + + __sing-box_debug "directive: ${directive}" + __sing-box_debug "completions: ${out}" + __sing-box_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __sing-box_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __sing-box_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __sing-box_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab="$(printf '\t')" + comp=${comp//$tab/:} + + __sing-box_debug "Adding completion: ${comp}" + completions+=${comp} + lastComp=$comp + fi + done < <(printf "%s\n" "${out[@]}") + + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __sing-box_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __sing-box_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __sing-box_debug "Activating keep order." + keepOrder="-V" + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local filteringCmd + filteringCmd='_files' + for filter in ${completions[@]}; do + if [ ${filter[1]} != '*' ]; then + # zsh requires a glob pattern to do file filtering + filter="\*.$filter" + fi + filteringCmd+=" -g $filter" + done + filteringCmd+=" ${flagPrefix}" + + __sing-box_debug "File filtering command: $filteringCmd" + _arguments '*:filename:'"$filteringCmd" + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + subdir="${completions[1]}" + if [ -n "$subdir" ]; then + __sing-box_debug "Listing directories in $subdir" + pushd "${subdir}" >/dev/null 2>&1 + else + __sing-box_debug "Listing directories in ." + fi + + local result + _arguments '*:dirname:_files -/'" ${flagPrefix}" + result=$? + if [ -n "$subdir" ]; then + popd >/dev/null 2>&1 + fi + return $result + else + __sing-box_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then + __sing-box_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __sing-box_debug "_describe did not find completions." + __sing-box_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __sing-box_debug "deactivating file completion" + + # We must return an error code here to let zsh know that there were no + # completions found by _describe; this is what will trigger other + # matching algorithms to attempt to find completions. + # For example zsh can match letters in the middle of words. + return 1 + else + # Perform file completion + __sing-box_debug "Activating file completion" + + # We must return the result of this command, so it must be the + # last command, or else we must store its result to return it. + _arguments '*:filename:_files'" ${flagPrefix}" + fi + fi + fi +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_sing-box" ]; then + _sing-box +fi diff --git a/release/config/config.json b/release/config/config.json new file mode 100644 index 00000000..e5a2a663 --- /dev/null +++ b/release/config/config.json @@ -0,0 +1,41 @@ +{ + "log": { + "level": "info" + }, + "dns": { + "servers": [ + { + "type": "tls", + "tag": "google", + "server": "8.8.8.8" + } + ] + }, + "inbounds": [ + { + "type": "shadowsocks", + "listen": "::", + "listen_port": 8080, + "network": "tcp", + "method": "2022-blake3-aes-128-gcm", + "password": "Gn1JUS14bLUHgv1cWDDp4A==", + "multiplex": { + "enabled": true, + "padding": true + } + } + ], + "outbounds": [ + { + "type": "direct" + } + ], + "route": { + "rules": [ + { + "port": 53, + "action": "hijack-dns" + } + ] + } +} diff --git a/release/config/openwrt.conf b/release/config/openwrt.conf new file mode 100644 index 00000000..1ce4c77d --- /dev/null +++ b/release/config/openwrt.conf @@ -0,0 +1,5 @@ +config sing-box 'main' + option enabled '1' + option conffile '/etc/sing-box/config.json' + option workdir '/usr/share/sing-box' + option log_stderr '1' diff --git a/release/config/openwrt.init b/release/config/openwrt.init new file mode 100644 index 00000000..9979fc1a --- /dev/null +++ b/release/config/openwrt.init @@ -0,0 +1,32 @@ +#!/bin/sh /etc/rc.common + +USE_PROCD=1 +START=99 +PROG="/usr/bin/sing-box" + +start_service() { + config_load "sing-box" + + local enabled config_file working_directory + local log_stderr + config_get_bool enabled "main" "enabled" "0" + [ "$enabled" -eq "1" ] || return 0 + + config_get config_file "main" "conffile" "/etc/sing-box/config.json" + config_get working_directory "main" "workdir" "/usr/share/sing-box" + config_get_bool log_stderr "main" "log_stderr" "1" + + procd_open_instance + procd_set_param command "$PROG" run -c "$config_file" -D "$working_directory" + procd_set_param file "$config_file" + procd_set_param stderr "$log_stderr" + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param respawn + + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "sing-box" +} diff --git a/release/config/openwrt.keep b/release/config/openwrt.keep new file mode 100644 index 00000000..b26dd531 --- /dev/null +++ b/release/config/openwrt.keep @@ -0,0 +1 @@ +/etc/sing-box/ diff --git a/release/config/openwrt.prerm b/release/config/openwrt.prerm new file mode 100644 index 00000000..12d06ec7 --- /dev/null +++ b/release/config/openwrt.prerm @@ -0,0 +1,4 @@ +#!/bin/sh +[ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 +. ${IPKG_INSTROOT}/lib/functions.sh +default_prerm $0 $@ diff --git a/release/config/sing-box-split-dns.xml b/release/config/sing-box-split-dns.xml new file mode 100644 index 00000000..4ee64c8d --- /dev/null +++ b/release/config/sing-box-split-dns.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/release/config/sing-box.confd b/release/config/sing-box.confd new file mode 100644 index 00000000..506caa32 --- /dev/null +++ b/release/config/sing-box.confd @@ -0,0 +1,6 @@ +# /etc/conf.d/sing-box: config file for /etc/init.d/sing-box + +# sing-box configuration path, could be file or directory +# SINGBOX_CONFIG=/etc/sing-box + +# SINGBOX_WORKDIR=/var/lib/sing-box diff --git a/release/config/sing-box.initd b/release/config/sing-box.initd new file mode 100644 index 00000000..1541518a --- /dev/null +++ b/release/config/sing-box.initd @@ -0,0 +1,44 @@ +#!/sbin/openrc-run + +name=$RC_SVCNAME +description="sing-box service" +supervisor="supervise-daemon" +command="/usr/bin/sing-box" +extra_commands="checkconfig" +extra_started_commands="reload" + +: ${SINGBOX_CONFIG:=${config:-"/etc/sing-box"}} + +if [ -d "$SINGBOX_CONFIG" ]; then + _config_opt="-C $SINGBOX_CONFIG" +elif [ -z "$SINGBOX_CONFIG" ]; then + _config_opt="" +else + _config_opt="-c $SINGBOX_CONFIG" +fi + +_workdir=${SINGBOX_WORKDIR:-${workdir:-"/var/lib/sing-box"}} + +command_args="run --disable-color + -D $_workdir + $_config_opt" + +depend() { + after net dns +} + +checkconfig() { + ebegin "Checking $RC_SVCNAME configuration" + sing-box check -D "$_workdir" $_config_opt + eend $? +} + +start_pre() { + checkconfig +} + +reload() { + ebegin "Reloading $RC_SVCNAME" + checkconfig && $supervisor "$RC_SVCNAME" --signal HUP + eend $? +} diff --git a/release/config/sing-box.postinst b/release/config/sing-box.postinst new file mode 100644 index 00000000..770dd22a --- /dev/null +++ b/release/config/sing-box.postinst @@ -0,0 +1,3 @@ +#!/bin/sh + +systemd-sysusers sing-box.conf diff --git a/release/config/sing-box.rules b/release/config/sing-box.rules new file mode 100644 index 00000000..668b2640 --- /dev/null +++ b/release/config/sing-box.rules @@ -0,0 +1,8 @@ +polkit.addRule(function(action, subject) { + if ((action.id == "org.freedesktop.resolve1.set-domains" || + action.id == "org.freedesktop.resolve1.set-default-route" || + action.id == "org.freedesktop.resolve1.set-dns-servers") && + subject.user == "sing-box") { + return polkit.Result.YES; + } +}); diff --git a/release/config/sing-box.service b/release/config/sing-box.service new file mode 100644 index 00000000..f003f844 --- /dev/null +++ b/release/config/sing-box.service @@ -0,0 +1,18 @@ +[Unit] +Description=sing-box service +Documentation=https://sing-box.sagernet.org +After=network.target nss-lookup.target network-online.target + +[Service] +User=sing-box +StateDirectory=sing-box +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=10s +LimitNOFILE=infinity + +[Install] +WantedBy=multi-user.target diff --git a/release/config/sing-box.sysusers b/release/config/sing-box.sysusers new file mode 100644 index 00000000..33e38100 --- /dev/null +++ b/release/config/sing-box.sysusers @@ -0,0 +1 @@ +u sing-box - "sing-box Service" diff --git a/release/config/sing-box@.service b/release/config/sing-box@.service new file mode 100644 index 00000000..726bbee7 --- /dev/null +++ b/release/config/sing-box@.service @@ -0,0 +1,18 @@ +[Unit] +Description=sing-box service +Documentation=https://sing-box.sagernet.org +After=network.target nss-lookup.target network-online.target + +[Service] +User=sing-box +StateDirectory=sing-box-%i +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=10s +LimitNOFILE=infinity + +[Install] +WantedBy=multi-user.target diff --git a/release/local/common.sh b/release/local/common.sh new file mode 100644 index 00000000..13a8415c --- /dev/null +++ b/release/local/common.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +BINARY_NAME="sing-box" + +INSTALL_BIN_PATH="/usr/local/bin" +INSTALL_CONFIG_PATH="/usr/local/etc/sing-box" +INSTALL_DATA_PATH="/var/lib/sing-box" +SYSTEMD_SERVICE_PATH="/etc/systemd/system" + +DEFAULT_BUILD_TAGS="$(cat "$PROJECT_DIR/release/DEFAULT_BUILD_TAGS_OTHERS")" + +setup_environment() { + if [ -d /usr/local/go ]; then + export PATH="$PATH:/usr/local/go/bin" + fi + + if ! command -v go &> /dev/null; then + echo "Error: Go is not installed or not in PATH" + echo "Run install_go.sh to install Go" + exit 1 + fi +} + +get_build_tags() { + local extra_tags="$1" + if [ -n "$extra_tags" ]; then + echo "${DEFAULT_BUILD_TAGS},${extra_tags}" + else + echo "${DEFAULT_BUILD_TAGS}" + fi +} + +get_version() { + cd "$PROJECT_DIR" + GOHOSTOS=$(go env GOHOSTOS) + GOHOSTARCH=$(go env GOHOSTARCH) + CGO_ENABLED=0 GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest +} + +get_ldflags() { + local version + version=$(get_version) + local shared_ldflags + shared_ldflags=$(cat "$PROJECT_DIR/release/LDFLAGS") + echo "-X 'github.com/sagernet/sing-box/constant.Version=${version}' ${shared_ldflags} -s -w -buildid=" +} + +build_sing_box() { + local tags="$1" + local ldflags + ldflags=$(get_ldflags) + + echo "Building sing-box with tags: $tags" + cd "$PROJECT_DIR" + export GOTOOLCHAIN=local + go install -v -trimpath -ldflags "$ldflags" -tags "$tags" ./cmd/sing-box +} + +install_binary() { + local gopath + gopath=$(go env GOPATH) + echo "Installing binary to $INSTALL_BIN_PATH/$BINARY_NAME" + sudo cp "${gopath}/bin/${BINARY_NAME}" "${INSTALL_BIN_PATH}/" +} + +setup_config() { + echo "Setting up configuration" + sudo mkdir -p "$INSTALL_CONFIG_PATH" + if [ ! -f "$INSTALL_CONFIG_PATH/config.json" ]; then + sudo cp "$PROJECT_DIR/release/config/config.json" "$INSTALL_CONFIG_PATH/config.json" + echo "Default config installed to $INSTALL_CONFIG_PATH/config.json" + else + echo "Config already exists at $INSTALL_CONFIG_PATH/config.json (not overwriting)" + fi +} + +setup_systemd() { + echo "Setting up systemd service" + sudo cp "$SCRIPT_DIR/sing-box.service" "$SYSTEMD_SERVICE_PATH/" + sudo systemctl daemon-reload +} + +stop_service() { + if systemctl is-active --quiet sing-box; then + echo "Stopping sing-box service" + sudo systemctl stop sing-box + fi +} + +start_service() { + echo "Starting sing-box service" + sudo systemctl start sing-box +} + +restart_service() { + echo "Restarting sing-box service" + sudo systemctl restart sing-box +} diff --git a/release/local/debug.sh b/release/local/debug.sh new file mode 100644 index 00000000..d8651999 --- /dev/null +++ b/release/local/debug.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +setup_environment + +echo "Updating sing-box from git repository..." +cd "$PROJECT_DIR" +git fetch +git reset FETCH_HEAD --hard +git clean -fdx + +BUILD_TAGS=$(get_build_tags "debug") + +build_sing_box "$BUILD_TAGS" + +stop_service +install_binary +start_service + +echo "" +echo "Following service logs (Ctrl+C to exit)..." +sudo journalctl -u sing-box --output cat -f diff --git a/release/local/enable.sh b/release/local/enable.sh new file mode 100644 index 00000000..19929921 --- /dev/null +++ b/release/local/enable.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +sudo systemctl enable sing-box +sudo systemctl start sing-box +sudo journalctl -u sing-box --output cat -f diff --git a/release/local/install.sh b/release/local/install.sh new file mode 100644 index 00000000..d5bf94fc --- /dev/null +++ b/release/local/install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +setup_environment + +BUILD_TAGS=$(get_build_tags) + +build_sing_box "$BUILD_TAGS" +install_binary +setup_config +setup_systemd + +echo "" +echo "Installation complete!" +echo "To enable and start the service, run: $SCRIPT_DIR/enable.sh" diff --git a/release/local/install_go.sh b/release/local/install_go.sh new file mode 100644 index 00000000..ea64fec4 --- /dev/null +++ b/release/local/install_go.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') +curl -Lo go.tar.gz "https://go.dev/dl/go$go_version.linux-amd64.tar.gz" +sudo rm -rf /usr/local/go +sudo tar -C /usr/local -xzf go.tar.gz +rm go.tar.gz diff --git a/release/local/reinstall.sh b/release/local/reinstall.sh new file mode 100644 index 00000000..1daaa181 --- /dev/null +++ b/release/local/reinstall.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +setup_environment + +BUILD_TAGS=$(get_build_tags) + +build_sing_box "$BUILD_TAGS" + +stop_service +install_binary +start_service + +echo "" +echo "Reinstallation complete!" diff --git a/release/local/sing-box.service b/release/local/sing-box.service new file mode 100644 index 00000000..9a152ade --- /dev/null +++ b/release/local/sing-box.service @@ -0,0 +1,16 @@ +[Unit] +Description=sing-box service +Documentation=https://sing-box.sagernet.org +After=network.target nss-lookup.target network-online.target + +[Service] +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +ExecStart=/usr/local/bin/sing-box -D /var/lib/sing-box -C /usr/local/etc/sing-box run +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=10s +LimitNOFILE=infinity + +[Install] +WantedBy=multi-user.target diff --git a/release/local/uninstall.sh b/release/local/uninstall.sh new file mode 100644 index 00000000..b9c89ab0 --- /dev/null +++ b/release/local/uninstall.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +echo "Uninstalling sing-box..." + +if systemctl is-active --quiet sing-box 2>/dev/null; then + echo "Stopping sing-box service..." + sudo systemctl stop sing-box +fi + +if systemctl is-enabled --quiet sing-box 2>/dev/null; then + echo "Disabling sing-box service..." + sudo systemctl disable sing-box +fi + +echo "Removing files..." +sudo rm -rf "$INSTALL_DATA_PATH" +sudo rm -rf "$INSTALL_BIN_PATH/$BINARY_NAME" +sudo rm -rf "$INSTALL_CONFIG_PATH" +sudo rm -rf "$SYSTEMD_SERVICE_PATH/sing-box.service" + +echo "Reloading systemd..." +sudo systemctl daemon-reload + +echo "" +echo "Uninstallation complete!" diff --git a/release/local/update.sh b/release/local/update.sh new file mode 100644 index 00000000..2331d270 --- /dev/null +++ b/release/local/update.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +echo "Updating sing-box from git repository..." +cd "$PROJECT_DIR" +git fetch +git reset FETCH_HEAD --hard +git clean -fdx + +echo "" +echo "Running reinstall..." +exec "$SCRIPT_DIR/reinstall.sh" \ No newline at end of file diff --git a/route/conn.go b/route/conn.go new file mode 100644 index 00000000..59afe539 --- /dev/null +++ b/route/conn.go @@ -0,0 +1,423 @@ +package route + +import ( + "context" + "io" + "net" + "net/netip" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tlsfragment" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/canceler" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" +) + +var _ adapter.ConnectionManager = (*ConnectionManager)(nil) + +type ConnectionManager struct { + logger logger.ContextLogger + access sync.Mutex + connections list.List[io.Closer] +} + +func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager { + return &ConnectionManager{ + logger: logger, + } +} + +func (m *ConnectionManager) Start(stage adapter.StartStage) error { + return nil +} + +func (m *ConnectionManager) Count() int { + return m.connections.Len() +} + +func (m *ConnectionManager) CloseAll() { + m.access.Lock() + var closers []io.Closer + for element := m.connections.Front(); element != nil; { + nextElement := element.Next() + closers = append(closers, element.Value) + m.connections.Remove(element) + element = nextElement + } + m.access.Unlock() + for _, closer := range closers { + common.Close(closer) + } +} + +func (m *ConnectionManager) Close() error { + m.CloseAll() + return nil +} + +func (m *ConnectionManager) TrackConn(conn net.Conn) net.Conn { + m.access.Lock() + element := m.connections.PushBack(conn) + m.access.Unlock() + return &trackedConn{ + Conn: conn, + manager: m, + element: element, + } +} + +func (m *ConnectionManager) TrackPacketConn(conn net.PacketConn) net.PacketConn { + m.access.Lock() + element := m.connections.PushBack(conn) + m.access.Unlock() + return &trackedPacketConn{ + PacketConn: conn, + manager: m, + element: element, + } +} + +func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = adapter.WithContext(ctx, &metadata) + var ( + remoteConn net.Conn + err error + ) + if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() { + remoteConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remoteConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) + } + if err != nil { + var remoteString string + if len(metadata.DestinationAddresses) > 0 { + remoteString = "[" + strings.Join(common.Map(metadata.DestinationAddresses, netip.Addr.String), ",") + "]" + } else { + remoteString = metadata.Destination.String() + } + var dialerString string + if outbound, isOutbound := this.(adapter.Outbound); isOutbound { + dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" + } + err = E.Cause(err, "open connection to ", remoteString, dialerString) + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, err) + return + } + err = N.ReportConnHandshakeSuccess(conn, remoteConn) + if err != nil { + err = E.Cause(err, "report handshake success") + remoteConn.Close() + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, err) + return + } + if metadata.TLSFragment || metadata.TLSRecordFragment { + remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay) + } + var done atomic.Bool + if m.kickWriteHandshake(ctx, conn, remoteConn, false, &done, onClose) { + return + } + if m.kickWriteHandshake(ctx, remoteConn, conn, true, &done, onClose) { + return + } + go m.connectionCopy(ctx, conn, remoteConn, false, &done, onClose) + go m.connectionCopy(ctx, remoteConn, conn, true, &done, onClose) +} + +func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = adapter.WithContext(ctx, &metadata) + var ( + remotePacketConn net.PacketConn + remoteConn net.Conn + destinationAddress netip.Addr + err error + ) + if metadata.UDPConnect { + parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer) + if len(metadata.DestinationAddresses) > 0 { + if isParallelDialer { + remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remoteConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) + } + } else if metadata.Destination.IsIP() { + if isParallelDialer { + remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) + } + } else { + remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) + } + if err != nil { + var remoteString string + if len(metadata.DestinationAddresses) > 0 { + remoteString = "[" + strings.Join(common.Map(metadata.DestinationAddresses, netip.Addr.String), ",") + "]" + } else { + remoteString = metadata.Destination.String() + } + var dialerString string + if outbound, isOutbound := this.(adapter.Outbound); isOutbound { + dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" + } + err = E.Cause(err, "open packet connection to ", remoteString, dialerString) + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, err) + return + } + remotePacketConn = bufio.NewUnbindPacketConn(remoteConn) + connRemoteAddr := M.AddrFromNet(remoteConn.RemoteAddr()) + if connRemoteAddr != metadata.Destination.Addr { + destinationAddress = connRemoteAddr + } + } else { + if len(metadata.DestinationAddresses) > 0 { + remotePacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else if packetDialer, withDestination := this.(dialer.PacketDialerWithDestination); withDestination { + remotePacketConn, destinationAddress, err = packetDialer.ListenPacketWithDestination(ctx, metadata.Destination) + } else { + remotePacketConn, err = this.ListenPacket(ctx, metadata.Destination) + } + if err != nil { + var dialerString string + if outbound, isOutbound := this.(adapter.Outbound); isOutbound { + dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" + } + err = E.Cause(err, "listen packet connection using ", dialerString) + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, err) + return + } + } + err = N.ReportPacketConnHandshakeSuccess(conn, remotePacketConn) + if err != nil { + conn.Close() + remotePacketConn.Close() + m.logger.ErrorContext(ctx, "report handshake success: ", err) + return + } + if destinationAddress.IsValid() { + var originDestination M.Socksaddr + if metadata.RouteOriginalDestination.IsValid() { + originDestination = metadata.RouteOriginalDestination + } else { + originDestination = metadata.Destination + } + if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { + natConn.UpdateDestination(destinationAddress) + } else { + destination := M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) + if metadata.Destination != destination { + if metadata.UDPDisableDomainUnmapping { + remotePacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(remotePacketConn), destination, originDestination) + } else { + remotePacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(remotePacketConn), destination, originDestination) + } + } else if metadata.RouteOriginalDestination.IsValid() && metadata.RouteOriginalDestination != metadata.Destination { + remotePacketConn = bufio.NewDestinationNATPacketConn(bufio.NewPacketConn(remotePacketConn), metadata.Destination, metadata.RouteOriginalDestination) + } + } + } else if metadata.RouteOriginalDestination.IsValid() && metadata.RouteOriginalDestination != metadata.Destination { + remotePacketConn = bufio.NewDestinationNATPacketConn(bufio.NewPacketConn(remotePacketConn), metadata.Destination, metadata.RouteOriginalDestination) + } + var udpTimeout time.Duration + if metadata.UDPTimeout > 0 { + udpTimeout = metadata.UDPTimeout + } else { + protocol := metadata.Protocol + if protocol == "" { + protocol = C.PortProtocols[metadata.Destination.Port] + } + if protocol != "" { + udpTimeout = C.ProtocolTimeouts[protocol] + } + } + if udpTimeout > 0 { + ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout) + } + destination := bufio.NewPacketConn(remotePacketConn) + var done atomic.Bool + go m.packetConnectionCopy(ctx, conn, destination, false, &done, onClose) + go m.packetConnectionCopy(ctx, destination, conn, true, &done, onClose) +} + +func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn, destination net.Conn, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { + _, err := bufio.CopyWithIncreateBuffer(destination, source, bufio.DefaultIncreaseBufferAfter, bufio.DefaultBatchSize) + if err != nil { + common.Close(source, destination) + } else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex { + err = duplexDst.CloseWrite() + if err != nil { + common.Close(source, destination) + } + } else { + destination.Close() + } + if done.Swap(true) { + if onClose != nil { + onClose(err) + } + common.Close(source, destination) + } + if !direction { + if err == nil { + m.logger.DebugContext(ctx, "connection upload finished") + } else if !E.IsClosedOrCanceled(err) { + m.logger.ErrorContext(ctx, "connection upload closed: ", err) + } else { + m.logger.TraceContext(ctx, "connection upload closed") + } + } else { + if err == nil { + m.logger.DebugContext(ctx, "connection download finished") + } else if !E.IsClosedOrCanceled(err) { + m.logger.ErrorContext(ctx, "connection download closed: ", err) + } else { + m.logger.TraceContext(ctx, "connection download closed") + } + } +} + +func (m *ConnectionManager) kickWriteHandshake(ctx context.Context, source net.Conn, destination net.Conn, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) bool { + if !N.NeedHandshakeForWrite(destination) { + return false + } + var ( + cachedBuffer *buf.Buffer + wrotePayload bool + ) + sourceReader, readCounters := N.UnwrapCountReader(source, nil) + destinationWriter, writeCounters := N.UnwrapCountWriter(destination, nil) + if cachedReader, ok := sourceReader.(N.CachedReader); ok { + cachedBuffer = cachedReader.ReadCached() + } + var err error + if cachedBuffer != nil { + wrotePayload = true + dataLen := cachedBuffer.Len() + _, err = destinationWriter.Write(cachedBuffer.Bytes()) + cachedBuffer.Release() + if err == nil { + for _, counter := range readCounters { + counter(int64(dataLen)) + } + for _, counter := range writeCounters { + counter(int64(dataLen)) + } + } + } else { + _ = destination.SetWriteDeadline(time.Now().Add(C.ReadPayloadTimeout)) + _, err = destinationWriter.Write(nil) + _ = destination.SetWriteDeadline(time.Time{}) + } + if err == nil { + return false + } + if !wrotePayload && (E.IsMulti(err, os.ErrInvalid, context.DeadlineExceeded, io.EOF) || E.IsTimeout(err)) { + return false + } + if !done.Swap(true) { + if onClose != nil { + onClose(err) + } + } + common.Close(source, destination) + if !direction { + m.logger.ErrorContext(ctx, "connection upload handshake: ", err) + } else { + m.logger.ErrorContext(ctx, "connection download handshake: ", err) + } + return true +} + +func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.PacketReader, destination N.PacketWriter, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { + _, err := bufio.CopyPacket(destination, source) + if !direction { + if err == nil { + m.logger.DebugContext(ctx, "packet upload finished") + } else if E.IsClosedOrCanceled(err) { + m.logger.TraceContext(ctx, "packet upload closed") + } else { + m.logger.DebugContext(ctx, "packet upload closed: ", err) + } + } else { + if err == nil { + m.logger.DebugContext(ctx, "packet download finished") + } else if E.IsClosedOrCanceled(err) { + m.logger.TraceContext(ctx, "packet download closed") + } else { + m.logger.DebugContext(ctx, "packet download closed: ", err) + } + } + if !done.Swap(true) { + if onClose != nil { + onClose(err) + } + } + common.Close(source, destination) +} + +type trackedConn struct { + net.Conn + manager *ConnectionManager + element *list.Element[io.Closer] +} + +func (c *trackedConn) Close() error { + c.manager.access.Lock() + c.manager.connections.Remove(c.element) + c.manager.access.Unlock() + return c.Conn.Close() +} + +func (c *trackedConn) Upstream() any { + return c.Conn +} + +func (c *trackedConn) ReaderReplaceable() bool { + return true +} + +func (c *trackedConn) WriterReplaceable() bool { + return true +} + +type trackedPacketConn struct { + net.PacketConn + manager *ConnectionManager + element *list.Element[io.Closer] +} + +func (c *trackedPacketConn) Close() error { + c.manager.access.Lock() + c.manager.connections.Remove(c.element) + c.manager.access.Unlock() + return c.PacketConn.Close() +} + +func (c *trackedPacketConn) Upstream() any { + return bufio.NewPacketConn(c.PacketConn) +} + +func (c *trackedPacketConn) ReaderReplaceable() bool { + return true +} + +func (c *trackedPacketConn) WriterReplaceable() bool { + return true +} diff --git a/route/dns.go b/route/dns.go new file mode 100644 index 00000000..dee4a756 --- /dev/null +++ b/route/dns.go @@ -0,0 +1,109 @@ +package route + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + dnsOutbound "github.com/sagernet/sing-box/protocol/dns" + R "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/udpnat2" + + mDNS "github.com/miekg/dns" +) + +func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Destination = M.Socksaddr{} + for { + conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) + err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata) + if err != nil { + if !E.IsClosedOrCanceled(err) { + return err + } else { + return nil + } + } + } +} + +func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + if natConn, isNatConn := conn.(udpnat.Conn); isNatConn { + metadata.Destination = M.Socksaddr{} + for _, packet := range packetBuffers { + buffer := packet.Buffer + destination := packet.Destination + N.PutPacketBuffer(packet) + go ExchangeDNSPacket(ctx, r.dns, r.logger, natConn, buffer, metadata, destination) + } + natConn.SetHandler(&dnsHijacker{ + router: r.dns, + logger: r.logger, + conn: conn, + ctx: ctx, + metadata: metadata, + onClose: onClose, + }) + return nil + } + err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil && !E.IsClosedOrCanceled(err) { + return E.Cause(err, "process DNS packet") + } + return nil +} + +func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) { + err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination) + if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) { + logger.ErrorContext(ctx, E.Cause(err, "process DNS packet")) + } +} + +func exchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) error { + var message mDNS.Msg + err := message.Unpack(buffer.Bytes()) + buffer.Release() + if err != nil { + return E.Cause(err, "unpack request") + } + response, err := router.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{}) + if err != nil { + return err + } + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) + if err != nil { + return err + } + err = conn.WritePacket(responseBuffer, destination) + return err +} + +type dnsHijacker struct { + router adapter.DNSRouter + logger logger.ContextLogger + conn N.PacketConn + ctx context.Context + metadata adapter.InboundContext + onClose N.CloseHandlerFunc +} + +func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) { + go ExchangeDNSPacket(h.ctx, h.router, h.logger, h.conn, buffer, h.metadata, destination) +} + +func (h *dnsHijacker) Close() error { + if h.onClose != nil { + h.onClose(nil) + } + return nil +} diff --git a/route/neighbor_resolver_darwin.go b/route/neighbor_resolver_darwin.go new file mode 100644 index 00000000..a8884ae6 --- /dev/null +++ b/route/neighbor_resolver_darwin.go @@ -0,0 +1,239 @@ +//go:build darwin + +package route + +import ( + "net" + "net/netip" + "os" + "sync" + "time" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +var defaultLeaseFiles = []string{ + "/var/db/dhcpd_leases", + "/tmp/dhcp.leases", +} + +type neighborResolver struct { + logger logger.ContextLogger + leaseFiles []string + access sync.RWMutex + neighborIPToMAC map[netip.Addr]net.HardwareAddr + leaseIPToMAC map[netip.Addr]net.HardwareAddr + ipToHostname map[netip.Addr]string + macToHostname map[string]string + watcher *fswatch.Watcher + done chan struct{} +} + +func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) { + if len(leaseFiles) == 0 { + for _, path := range defaultLeaseFiles { + info, err := os.Stat(path) + if err == nil && info.Size() > 0 { + leaseFiles = append(leaseFiles, path) + } + } + } + return &neighborResolver{ + logger: resolverLogger, + leaseFiles: leaseFiles, + neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr), + leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr), + ipToHostname: make(map[netip.Addr]string), + macToHostname: make(map[string]string), + done: make(chan struct{}), + }, nil +} + +func (r *neighborResolver) Start() error { + err := r.loadNeighborTable() + if err != nil { + r.logger.Warn(E.Cause(err, "load neighbor table")) + } + r.doReloadLeaseFiles() + go r.subscribeNeighborUpdates() + if len(r.leaseFiles) > 0 { + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: r.leaseFiles, + Logger: r.logger, + Callback: func(_ string) { + r.doReloadLeaseFiles() + }, + }) + if err != nil { + r.logger.Warn(E.Cause(err, "create lease file watcher")) + } else { + r.watcher = watcher + err = watcher.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start lease file watcher")) + } + } + } + return nil +} + +func (r *neighborResolver) Close() error { + close(r.done) + if r.watcher != nil { + return r.watcher.Close() + } + return nil +} + +func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) { + r.access.RLock() + defer r.access.RUnlock() + mac, found := r.neighborIPToMAC[address] + if found { + return mac, true + } + mac, found = r.leaseIPToMAC[address] + if found { + return mac, true + } + mac, found = extractMACFromEUI64(address) + if found { + return mac, true + } + return nil, false +} + +func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) { + r.access.RLock() + defer r.access.RUnlock() + hostname, found := r.ipToHostname[address] + if found { + return hostname, true + } + mac, macFound := r.neighborIPToMAC[address] + if !macFound { + mac, macFound = r.leaseIPToMAC[address] + } + if !macFound { + mac, macFound = extractMACFromEUI64(address) + } + if macFound { + hostname, found = r.macToHostname[mac.String()] + if found { + return hostname, true + } + } + return "", false +} + +func (r *neighborResolver) loadNeighborTable() error { + entries, err := ReadNeighborEntries() + if err != nil { + return err + } + r.access.Lock() + defer r.access.Unlock() + for _, entry := range entries { + r.neighborIPToMAC[entry.Address] = entry.MACAddress + } + return nil +} + +func (r *neighborResolver) subscribeNeighborUpdates() { + routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) + if err != nil { + r.logger.Warn(E.Cause(err, "subscribe neighbor updates")) + return + } + err = unix.SetNonblock(routeSocket, true) + if err != nil { + unix.Close(routeSocket) + r.logger.Warn(E.Cause(err, "set route socket nonblock")) + return + } + routeSocketFile := os.NewFile(uintptr(routeSocket), "route") + defer routeSocketFile.Close() + buffer := buf.NewPacket() + defer buffer.Release() + for { + select { + case <-r.done: + return + default: + } + err = setReadDeadline(routeSocketFile, 3*time.Second) + if err != nil { + r.logger.Warn(E.Cause(err, "set route socket read deadline")) + return + } + n, err := routeSocketFile.Read(buffer.FreeBytes()) + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-r.done: + return + default: + } + r.logger.Warn(E.Cause(err, "receive neighbor update")) + continue + } + messages, err := route.ParseRIB(route.RIBTypeRoute, buffer.FreeBytes()[:n]) + if err != nil { + continue + } + for _, message := range messages { + routeMessage, isRouteMessage := message.(*route.RouteMessage) + if !isRouteMessage { + continue + } + if routeMessage.Flags&unix.RTF_LLINFO == 0 { + continue + } + address, mac, isDelete, ok := ParseRouteNeighborMessage(routeMessage) + if !ok { + continue + } + r.access.Lock() + if isDelete { + delete(r.neighborIPToMAC, address) + } else { + r.neighborIPToMAC[address] = mac + } + r.access.Unlock() + } + } +} + +func (r *neighborResolver) doReloadLeaseFiles() { + leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles) + r.access.Lock() + r.leaseIPToMAC = leaseIPToMAC + r.ipToHostname = ipToHostname + r.macToHostname = macToHostname + r.access.Unlock() +} + +func setReadDeadline(file *os.File, timeout time.Duration) error { + rawConn, err := file.SyscallConn() + if err != nil { + return err + } + var controlErr error + err = rawConn.Control(func(fd uintptr) { + tv := unix.NsecToTimeval(int64(timeout)) + controlErr = unix.SetsockoptTimeval(int(fd), unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv) + }) + if err != nil { + return err + } + return controlErr +} diff --git a/route/neighbor_resolver_lease.go b/route/neighbor_resolver_lease.go new file mode 100644 index 00000000..e3f9c0b4 --- /dev/null +++ b/route/neighbor_resolver_lease.go @@ -0,0 +1,386 @@ +package route + +import ( + "bufio" + "encoding/hex" + "net" + "net/netip" + "os" + "strconv" + "strings" + "time" +) + +func parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + if strings.HasSuffix(path, "dhcpd_leases") { + parseBootpdLeases(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "kea-leases4.csv") { + parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "kea-leases6.csv") { + parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "dhcpd.leases") { + parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname) + return + } + parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname) +} + +func ReloadLeaseFiles(leaseFiles []string) (leaseIPToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + leaseIPToMAC = make(map[netip.Addr]net.HardwareAddr) + ipToHostname = make(map[netip.Addr]string) + macToHostname = make(map[string]string) + for _, path := range leaseFiles { + parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname) + } + return +} + +func parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + now := time.Now().Unix() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "duid ") { + continue + } + if strings.HasPrefix(line, "# ") { + parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname) + continue + } + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + expiry, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil { + continue + } + if expiry != 0 && expiry < now { + continue + } + if strings.Contains(fields[1], ":") { + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) + if !addrOK { + continue + } + address = address.Unmap() + ipToMAC[address] = mac + hostname := fields[3] + if hostname != "*" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + } else { + var mac net.HardwareAddr + if len(fields) >= 5 { + duid, duidErr := parseDUID(fields[4]) + if duidErr == nil { + mac, _ = extractMACFromDUID(duid) + } + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) + if !addrOK { + continue + } + address = address.Unmap() + if mac != nil { + ipToMAC[address] = mac + } + hostname := fields[3] + if hostname != "*" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } + } +} + +func parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + fields := strings.Fields(line) + if len(fields) < 5 { + return + } + validTime, err := strconv.ParseInt(fields[4], 10, 64) + if err != nil { + return + } + if validTime == 0 { + return + } + if validTime > 0 && validTime < time.Now().Unix() { + return + } + hostname := fields[3] + if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) { + hostname = "" + } + if len(fields) >= 8 && fields[2] == "ipv4" { + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + return + } + addressField := fields[7] + slashIndex := strings.IndexByte(addressField, '/') + if slashIndex >= 0 { + addressField = addressField[:slashIndex] + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) + if !addrOK { + return + } + address = address.Unmap() + ipToMAC[address] = mac + if hostname != "" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + return + } + var mac net.HardwareAddr + duidHex := fields[1] + duidBytes, hexErr := hex.DecodeString(duidHex) + if hexErr == nil { + mac, _ = extractMACFromDUID(duidBytes) + } + for i := 7; i < len(fields); i++ { + addressField := fields[i] + slashIndex := strings.IndexByte(addressField, '/') + if slashIndex >= 0 { + addressField = addressField[:slashIndex] + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) + if !addrOK { + continue + } + address = address.Unmap() + if mac != nil { + ipToMAC[address] = mac + } + if hostname != "" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } +} + +func parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + var currentIP netip.Addr + var currentMAC net.HardwareAddr + var currentHostname string + var currentActive bool + var inLease bool + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") { + ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {") + parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString)) + if addrOK { + currentIP = parsed.Unmap() + inLease = true + currentMAC = nil + currentHostname = "" + currentActive = false + } + continue + } + if line == "}" && inLease { + if currentActive && currentMAC != nil { + ipToMAC[currentIP] = currentMAC + if currentHostname != "" { + ipToHostname[currentIP] = currentHostname + macToHostname[currentMAC.String()] = currentHostname + } + } else { + delete(ipToMAC, currentIP) + delete(ipToHostname, currentIP) + } + inLease = false + continue + } + if !inLease { + continue + } + if strings.HasPrefix(line, "hardware ethernet ") { + macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";") + parsed, macErr := net.ParseMAC(macString) + if macErr == nil { + currentMAC = parsed + } + } else if strings.HasPrefix(line, "client-hostname ") { + hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";") + hostname = strings.Trim(hostname, "\"") + if hostname != "" { + currentHostname = hostname + } + } else if strings.HasPrefix(line, "binding state ") { + state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";") + currentActive = state == "active" + } + } +} + +func parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + firstLine := true + for scanner.Scan() { + if firstLine { + firstLine = false + continue + } + fields := strings.Split(scanner.Text(), ",") + if len(fields) < 10 { + continue + } + if fields[9] != "0" { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) + if !addrOK { + continue + } + address = address.Unmap() + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + continue + } + ipToMAC[address] = mac + hostname := "" + if len(fields) > 8 { + hostname = fields[8] + } + if hostname != "" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + } +} + +func parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + firstLine := true + for scanner.Scan() { + if firstLine { + firstLine = false + continue + } + fields := strings.Split(scanner.Text(), ",") + if len(fields) < 14 { + continue + } + if fields[13] != "0" { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) + if !addrOK { + continue + } + address = address.Unmap() + var mac net.HardwareAddr + if fields[12] != "" { + mac, _ = net.ParseMAC(fields[12]) + } + if mac == nil { + duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", "")) + if duidErr == nil { + mac, _ = extractMACFromDUID(duid) + } + } + hostname := "" + if len(fields) > 11 { + hostname = fields[11] + } + if mac != nil { + ipToMAC[address] = mac + } + if hostname != "" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } +} + +func parseBootpdLeases(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + now := time.Now().Unix() + scanner := bufio.NewScanner(file) + var currentName string + var currentIP netip.Addr + var currentMAC net.HardwareAddr + var currentLease int64 + var inBlock bool + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "{" { + inBlock = true + currentName = "" + currentIP = netip.Addr{} + currentMAC = nil + currentLease = 0 + continue + } + if line == "}" && inBlock { + if currentMAC != nil && currentIP.IsValid() { + if currentLease == 0 || currentLease >= now { + ipToMAC[currentIP] = currentMAC + if currentName != "" { + ipToHostname[currentIP] = currentName + macToHostname[currentMAC.String()] = currentName + } + } + } + inBlock = false + continue + } + if !inBlock { + continue + } + key, value, found := strings.Cut(line, "=") + if !found { + continue + } + switch key { + case "name": + currentName = value + case "ip_address": + parsed, addrOK := netip.AddrFromSlice(net.ParseIP(value)) + if addrOK { + currentIP = parsed.Unmap() + } + case "hw_address": + typeAndMAC, hasSep := strings.CutPrefix(value, "1,") + if hasSep { + mac, macErr := net.ParseMAC(typeAndMAC) + if macErr == nil { + currentMAC = mac + } + } + case "lease": + leaseHex := strings.TrimPrefix(value, "0x") + parsed, parseErr := strconv.ParseInt(leaseHex, 16, 64) + if parseErr == nil { + currentLease = parsed + } + } + } +} diff --git a/route/neighbor_resolver_linux.go b/route/neighbor_resolver_linux.go new file mode 100644 index 00000000..b7991b4c --- /dev/null +++ b/route/neighbor_resolver_linux.go @@ -0,0 +1,224 @@ +//go:build linux + +package route + +import ( + "net" + "net/netip" + "os" + "slices" + "sync" + "time" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "github.com/jsimonetti/rtnetlink" + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +var defaultLeaseFiles = []string{ + "/tmp/dhcp.leases", + "/var/lib/dhcp/dhcpd.leases", + "/var/lib/dhcpd/dhcpd.leases", + "/var/lib/kea/kea-leases4.csv", + "/var/lib/kea/kea-leases6.csv", +} + +type neighborResolver struct { + logger logger.ContextLogger + leaseFiles []string + access sync.RWMutex + neighborIPToMAC map[netip.Addr]net.HardwareAddr + leaseIPToMAC map[netip.Addr]net.HardwareAddr + ipToHostname map[netip.Addr]string + macToHostname map[string]string + watcher *fswatch.Watcher + done chan struct{} +} + +func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) { + if len(leaseFiles) == 0 { + for _, path := range defaultLeaseFiles { + info, err := os.Stat(path) + if err == nil && info.Size() > 0 { + leaseFiles = append(leaseFiles, path) + } + } + } + return &neighborResolver{ + logger: resolverLogger, + leaseFiles: leaseFiles, + neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr), + leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr), + ipToHostname: make(map[netip.Addr]string), + macToHostname: make(map[string]string), + done: make(chan struct{}), + }, nil +} + +func (r *neighborResolver) Start() error { + err := r.loadNeighborTable() + if err != nil { + r.logger.Warn(E.Cause(err, "load neighbor table")) + } + r.doReloadLeaseFiles() + go r.subscribeNeighborUpdates() + if len(r.leaseFiles) > 0 { + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: r.leaseFiles, + Logger: r.logger, + Callback: func(_ string) { + r.doReloadLeaseFiles() + }, + }) + if err != nil { + r.logger.Warn(E.Cause(err, "create lease file watcher")) + } else { + r.watcher = watcher + err = watcher.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start lease file watcher")) + } + } + } + return nil +} + +func (r *neighborResolver) Close() error { + close(r.done) + if r.watcher != nil { + return r.watcher.Close() + } + return nil +} + +func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) { + r.access.RLock() + defer r.access.RUnlock() + mac, found := r.neighborIPToMAC[address] + if found { + return mac, true + } + mac, found = r.leaseIPToMAC[address] + if found { + return mac, true + } + mac, found = extractMACFromEUI64(address) + if found { + return mac, true + } + return nil, false +} + +func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) { + r.access.RLock() + defer r.access.RUnlock() + hostname, found := r.ipToHostname[address] + if found { + return hostname, true + } + mac, macFound := r.neighborIPToMAC[address] + if !macFound { + mac, macFound = r.leaseIPToMAC[address] + } + if !macFound { + mac, macFound = extractMACFromEUI64(address) + } + if macFound { + hostname, found = r.macToHostname[mac.String()] + if found { + return hostname, true + } + } + return "", false +} + +func (r *neighborResolver) loadNeighborTable() error { + connection, err := rtnetlink.Dial(nil) + if err != nil { + return E.Cause(err, "dial rtnetlink") + } + defer connection.Close() + neighbors, err := connection.Neigh.List() + if err != nil { + return E.Cause(err, "list neighbors") + } + r.access.Lock() + defer r.access.Unlock() + for _, neigh := range neighbors { + if neigh.Attributes == nil { + continue + } + if neigh.Attributes.LLAddress == nil || len(neigh.Attributes.Address) == 0 { + continue + } + address, ok := netip.AddrFromSlice(neigh.Attributes.Address) + if !ok { + continue + } + r.neighborIPToMAC[address] = slices.Clone(neigh.Attributes.LLAddress) + } + return nil +} + +func (r *neighborResolver) subscribeNeighborUpdates() { + connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ + Groups: 1 << (unix.RTNLGRP_NEIGH - 1), + }) + if err != nil { + r.logger.Warn(E.Cause(err, "subscribe neighbor updates")) + return + } + defer connection.Close() + for { + select { + case <-r.done: + return + default: + } + err = connection.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + r.logger.Warn(E.Cause(err, "set netlink read deadline")) + return + } + messages, err := connection.Receive() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-r.done: + return + default: + } + r.logger.Warn(E.Cause(err, "receive neighbor update")) + continue + } + for _, message := range messages { + address, mac, isDelete, ok := ParseNeighborMessage(message) + if !ok { + continue + } + r.access.Lock() + if isDelete { + delete(r.neighborIPToMAC, address) + } else { + r.neighborIPToMAC[address] = mac + } + r.access.Unlock() + } + } +} + +func (r *neighborResolver) doReloadLeaseFiles() { + leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles) + r.access.Lock() + r.leaseIPToMAC = leaseIPToMAC + r.ipToHostname = ipToHostname + r.macToHostname = macToHostname + r.access.Unlock() +} diff --git a/route/neighbor_resolver_parse.go b/route/neighbor_resolver_parse.go new file mode 100644 index 00000000..1979b7ea --- /dev/null +++ b/route/neighbor_resolver_parse.go @@ -0,0 +1,50 @@ +package route + +import ( + "encoding/binary" + "encoding/hex" + "net" + "net/netip" + "slices" + "strings" +) + +func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) { + if len(duid) < 4 { + return nil, false + } + duidType := binary.BigEndian.Uint16(duid[0:2]) + hwType := binary.BigEndian.Uint16(duid[2:4]) + if hwType != 1 { + return nil, false + } + switch duidType { + case 1: + if len(duid) < 14 { + return nil, false + } + return net.HardwareAddr(slices.Clone(duid[8:14])), true + case 3: + if len(duid) < 10 { + return nil, false + } + return net.HardwareAddr(slices.Clone(duid[4:10])), true + } + return nil, false +} + +func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) { + if !address.Is6() { + return nil, false + } + b := address.As16() + if b[11] != 0xff || b[12] != 0xfe { + return nil, false + } + return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true +} + +func parseDUID(s string) ([]byte, error) { + cleaned := strings.ReplaceAll(s, ":", "") + return hex.DecodeString(cleaned) +} diff --git a/route/neighbor_resolver_platform.go b/route/neighbor_resolver_platform.go new file mode 100644 index 00000000..ddb9a995 --- /dev/null +++ b/route/neighbor_resolver_platform.go @@ -0,0 +1,84 @@ +package route + +import ( + "net" + "net/netip" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/logger" +) + +type platformNeighborResolver struct { + logger logger.ContextLogger + platform adapter.PlatformInterface + access sync.RWMutex + ipToMAC map[netip.Addr]net.HardwareAddr + ipToHostname map[netip.Addr]string + macToHostname map[string]string +} + +func newPlatformNeighborResolver(resolverLogger logger.ContextLogger, platform adapter.PlatformInterface) adapter.NeighborResolver { + return &platformNeighborResolver{ + logger: resolverLogger, + platform: platform, + ipToMAC: make(map[netip.Addr]net.HardwareAddr), + ipToHostname: make(map[netip.Addr]string), + macToHostname: make(map[string]string), + } +} + +func (r *platformNeighborResolver) Start() error { + return r.platform.StartNeighborMonitor(r) +} + +func (r *platformNeighborResolver) Close() error { + return r.platform.CloseNeighborMonitor(r) +} + +func (r *platformNeighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) { + r.access.RLock() + defer r.access.RUnlock() + mac, found := r.ipToMAC[address] + if found { + return mac, true + } + return extractMACFromEUI64(address) +} + +func (r *platformNeighborResolver) LookupHostname(address netip.Addr) (string, bool) { + r.access.RLock() + defer r.access.RUnlock() + hostname, found := r.ipToHostname[address] + if found { + return hostname, true + } + mac, found := r.ipToMAC[address] + if !found { + mac, found = extractMACFromEUI64(address) + } + if !found { + return "", false + } + hostname, found = r.macToHostname[mac.String()] + return hostname, found +} + +func (r *platformNeighborResolver) UpdateNeighborTable(entries []adapter.NeighborEntry) { + ipToMAC := make(map[netip.Addr]net.HardwareAddr) + ipToHostname := make(map[netip.Addr]string) + macToHostname := make(map[string]string) + for _, entry := range entries { + ipToMAC[entry.Address] = entry.MACAddress + if entry.Hostname != "" { + ipToHostname[entry.Address] = entry.Hostname + macToHostname[entry.MACAddress.String()] = entry.Hostname + } + } + r.access.Lock() + r.ipToMAC = ipToMAC + r.ipToHostname = ipToHostname + r.macToHostname = macToHostname + r.access.Unlock() + r.logger.Info("updated neighbor table: ", len(entries), " entries") +} diff --git a/route/neighbor_resolver_stub.go b/route/neighbor_resolver_stub.go new file mode 100644 index 00000000..177a1fcc --- /dev/null +++ b/route/neighbor_resolver_stub.go @@ -0,0 +1,14 @@ +//go:build !linux && !darwin + +package route + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/logger" +) + +func newNeighborResolver(_ logger.ContextLogger, _ []string) (adapter.NeighborResolver, error) { + return nil, os.ErrInvalid +} diff --git a/route/neighbor_table_darwin.go b/route/neighbor_table_darwin.go new file mode 100644 index 00000000..8ca2d0f0 --- /dev/null +++ b/route/neighbor_table_darwin.go @@ -0,0 +1,104 @@ +//go:build darwin + +package route + +import ( + "net" + "net/netip" + "syscall" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +func ReadNeighborEntries() ([]adapter.NeighborEntry, error) { + var entries []adapter.NeighborEntry + ipv4Entries, err := readNeighborEntriesAF(syscall.AF_INET) + if err != nil { + return nil, E.Cause(err, "read IPv4 neighbors") + } + entries = append(entries, ipv4Entries...) + ipv6Entries, err := readNeighborEntriesAF(syscall.AF_INET6) + if err != nil { + return nil, E.Cause(err, "read IPv6 neighbors") + } + entries = append(entries, ipv6Entries...) + return entries, nil +} + +func readNeighborEntriesAF(addressFamily int) ([]adapter.NeighborEntry, error) { + rib, err := route.FetchRIB(addressFamily, route.RIBType(syscall.NET_RT_FLAGS), syscall.RTF_LLINFO) + if err != nil { + return nil, err + } + messages, err := route.ParseRIB(route.RIBType(syscall.NET_RT_FLAGS), rib) + if err != nil { + return nil, err + } + var entries []adapter.NeighborEntry + for _, message := range messages { + routeMessage, isRouteMessage := message.(*route.RouteMessage) + if !isRouteMessage { + continue + } + address, macAddress, ok := parseRouteNeighborEntry(routeMessage) + if !ok { + continue + } + entries = append(entries, adapter.NeighborEntry{ + Address: address, + MACAddress: macAddress, + }) + } + return entries, nil +} + +func parseRouteNeighborEntry(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, ok bool) { + if len(message.Addrs) <= unix.RTAX_GATEWAY { + return + } + gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr) + if !isLinkAddr || len(gateway.Addr) < 6 { + return + } + switch destination := message.Addrs[unix.RTAX_DST].(type) { + case *route.Inet4Addr: + address = netip.AddrFrom4(destination.IP) + case *route.Inet6Addr: + address = netip.AddrFrom16(destination.IP) + default: + return + } + macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr))) + copy(macAddress, gateway.Addr) + ok = true + return +} + +func ParseRouteNeighborMessage(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) { + isDelete = message.Type == unix.RTM_DELETE + if len(message.Addrs) <= unix.RTAX_GATEWAY { + return + } + switch destination := message.Addrs[unix.RTAX_DST].(type) { + case *route.Inet4Addr: + address = netip.AddrFrom4(destination.IP) + case *route.Inet6Addr: + address = netip.AddrFrom16(destination.IP) + default: + return + } + if !isDelete { + gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr) + if !isLinkAddr || len(gateway.Addr) < 6 { + return + } + macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr))) + copy(macAddress, gateway.Addr) + } + ok = true + return +} diff --git a/route/neighbor_table_linux.go b/route/neighbor_table_linux.go new file mode 100644 index 00000000..61a214fd --- /dev/null +++ b/route/neighbor_table_linux.go @@ -0,0 +1,68 @@ +//go:build linux + +package route + +import ( + "net" + "net/netip" + "slices" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/jsimonetti/rtnetlink" + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +func ReadNeighborEntries() ([]adapter.NeighborEntry, error) { + connection, err := rtnetlink.Dial(nil) + if err != nil { + return nil, E.Cause(err, "dial rtnetlink") + } + defer connection.Close() + neighbors, err := connection.Neigh.List() + if err != nil { + return nil, E.Cause(err, "list neighbors") + } + var entries []adapter.NeighborEntry + for _, neighbor := range neighbors { + if neighbor.Attributes == nil { + continue + } + if neighbor.Attributes.LLAddress == nil || len(neighbor.Attributes.Address) == 0 { + continue + } + address, ok := netip.AddrFromSlice(neighbor.Attributes.Address) + if !ok { + continue + } + entries = append(entries, adapter.NeighborEntry{ + Address: address, + MACAddress: slices.Clone(neighbor.Attributes.LLAddress), + }) + } + return entries, nil +} + +func ParseNeighborMessage(message netlink.Message) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) { + var neighMessage rtnetlink.NeighMessage + err := neighMessage.UnmarshalBinary(message.Data) + if err != nil { + return + } + if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 { + return + } + address, ok = netip.AddrFromSlice(neighMessage.Attributes.Address) + if !ok { + return + } + isDelete = message.Header.Type == unix.RTM_DELNEIGH + if !isDelete && neighMessage.Attributes.LLAddress == nil { + ok = false + return + } + macAddress = slices.Clone(neighMessage.Attributes.LLAddress) + return +} diff --git a/route/network.go b/route/network.go new file mode 100644 index 00000000..1393ca90 --- /dev/null +++ b/route/network.go @@ -0,0 +1,542 @@ +package route + +import ( + "context" + "errors" + "net" + "net/netip" + "os" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/settings" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/winpowrprof" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" + + "golang.org/x/exp/slices" +) + +var _ adapter.NetworkManager = (*NetworkManager)(nil) + +type NetworkManager struct { + logger logger.ContextLogger + interfaceFinder *control.DefaultInterfaceFinder + networkInterfaces common.TypedValue[[]adapter.NetworkInterface] + + autoDetectInterface bool + defaultOptions adapter.NetworkOptions + autoRedirectOutputMark uint32 + networkMonitor tun.NetworkUpdateMonitor + interfaceMonitor tun.DefaultInterfaceMonitor + packageManager tun.PackageManager + powerListener winpowrprof.EventListener + pauseManager pause.Manager + platformInterface adapter.PlatformInterface + connectionManager adapter.ConnectionManager + endpoint adapter.EndpointManager + inbound adapter.InboundManager + outbound adapter.OutboundManager + needWIFIState bool + wifiMonitor settings.WIFIMonitor + wifiState adapter.WIFIState + wifiStateMutex sync.RWMutex + started bool +} + +func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options option.RouteOptions, dnsOptions option.DNSOptions) (*NetworkManager, error) { + defaultDomainResolver := common.PtrValueOrDefault(options.DefaultDomainResolver) + if options.AutoDetectInterface && !(C.IsLinux || C.IsDarwin || C.IsWindows) { + return nil, E.New("`auto_detect_interface` is only supported on Linux, Windows and macOS") + } else if options.OverrideAndroidVPN && !C.IsAndroid { + return nil, E.New("`override_android_vpn` is only supported on Android") + } else if options.DefaultInterface != "" && !(C.IsLinux || C.IsDarwin || C.IsWindows) { + return nil, E.New("`default_interface` is only supported on Linux, Windows and macOS") + } else if options.DefaultMark != 0 && !C.IsLinux { + return nil, E.New("`default_mark` is only supported on linux") + } + nm := &NetworkManager{ + logger: logger, + interfaceFinder: control.NewDefaultInterfaceFinder(), + autoDetectInterface: options.AutoDetectInterface, + defaultOptions: adapter.NetworkOptions{ + BindInterface: options.DefaultInterface, + RoutingMark: uint32(options.DefaultMark), + DomainResolver: defaultDomainResolver.Server, + DomainResolveOptions: adapter.DNSQueryOptions{ + Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), + DisableCache: defaultDomainResolver.DisableCache, + DisableOptimisticCache: defaultDomainResolver.DisableOptimisticCache, + RewriteTTL: defaultDomainResolver.RewriteTTL, + ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), + }, + NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy), + NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build), + FallbackNetworkType: common.Map(options.DefaultFallbackNetworkType, option.InterfaceType.Build), + FallbackDelay: time.Duration(options.DefaultFallbackDelay), + }, + pauseManager: service.FromContext[pause.Manager](ctx), + platformInterface: service.FromContext[adapter.PlatformInterface](ctx), + connectionManager: service.FromContext[adapter.ConnectionManager](ctx), + endpoint: service.FromContext[adapter.EndpointManager](ctx), + inbound: service.FromContext[adapter.InboundManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), + } + if options.DefaultNetworkStrategy != nil { + if options.DefaultInterface != "" { + return nil, E.New("`default_network_strategy` is conflict with `default_interface`") + } + if !options.AutoDetectInterface { + return nil, E.New("`auto_detect_interface` is required by `default_network_strategy`") + } + } + usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil + enforceInterfaceMonitor := options.AutoDetectInterface + if !usePlatformDefaultInterfaceMonitor { + networkMonitor, err := tun.NewNetworkUpdateMonitor(logger) + if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { + if err != nil { + return nil, E.Cause(err, "create network monitor") + } + nm.networkMonitor = networkMonitor + interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(nm.networkMonitor, logger, tun.DefaultInterfaceMonitorOptions{ + InterfaceFinder: nm.interfaceFinder, + OverrideAndroidVPN: options.OverrideAndroidVPN, + UnderNetworkExtension: nm.platformInterface != nil && nm.platformInterface.UnderNetworkExtension(), + }) + if err != nil { + return nil, E.New("auto_detect_interface unsupported on current platform") + } + interfaceMonitor.RegisterCallback(nm.notifyInterfaceUpdate) + nm.interfaceMonitor = interfaceMonitor + } + } else { + interfaceMonitor := nm.platformInterface.CreateDefaultInterfaceMonitor(logger) + interfaceMonitor.RegisterCallback(nm.notifyInterfaceUpdate) + nm.interfaceMonitor = interfaceMonitor + } + return nm, nil +} + +func (r *NetworkManager) Start(stage adapter.StartStage) error { + monitor := taskmonitor.New(r.logger, C.StartTimeout) + switch stage { + case adapter.StartStateInitialize: + if r.networkMonitor != nil { + monitor.Start("initialize network monitor") + err := r.networkMonitor.Start() + monitor.Finish() + if err != nil { + return err + } + } + if r.interfaceMonitor != nil { + monitor.Start("initialize interface monitor") + err := r.interfaceMonitor.Start() + monitor.Finish() + if err != nil { + return err + } + } + case adapter.StartStateStart: + if runtime.GOOS == "windows" { + powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent) + if err == nil { + r.powerListener = powerListener + } else { + r.logger.Warn("initialize power listener: ", err) + } + } + if r.powerListener != nil { + monitor.Start("start power listener") + err := r.powerListener.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start power listener") + } + } + if C.IsAndroid && r.platformInterface == nil { + monitor.Start("initialize package manager") + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: r, + Logger: r.logger, + }) + monitor.Finish() + if err != nil { + return E.Cause(err, "create package manager") + } + monitor.Start("start package manager") + err = packageManager.Start() + monitor.Finish() + if err != nil { + r.logger.Warn("initialize package manager: ", err) + } else { + r.packageManager = packageManager + } + } + case adapter.StartStatePostStart: + if r.needWIFIState && !(r.platformInterface != nil && r.platformInterface.UsePlatformWIFIMonitor()) { + wifiMonitor, err := settings.NewWIFIMonitor(r.onWIFIStateChanged) + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create WIFI monitor")) + } + } else { + r.wifiMonitor = wifiMonitor + err = r.wifiMonitor.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start WIFI monitor")) + } + } + } + r.started = true + } + return nil +} + +func (r *NetworkManager) Initialize(ruleSets []adapter.RuleSet) { + for _, ruleSet := range ruleSets { + metadata := ruleSet.Metadata() + if metadata.ContainsWIFIRule { + r.needWIFIState = true + break + } + } +} + +func (r *NetworkManager) Close() error { + monitor := taskmonitor.New(r.logger, C.StopTimeout) + var err error + if r.packageManager != nil { + monitor.Start("close package manager") + err = E.Append(err, r.packageManager.Close(), func(err error) error { + return E.Cause(err, "close package manager") + }) + monitor.Finish() + } + if r.powerListener != nil { + monitor.Start("close power listener") + err = E.Append(err, r.powerListener.Close(), func(err error) error { + return E.Cause(err, "close power listener") + }) + monitor.Finish() + } + if r.interfaceMonitor != nil { + monitor.Start("close interface monitor") + err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { + return E.Cause(err, "close interface monitor") + }) + monitor.Finish() + } + if r.networkMonitor != nil { + monitor.Start("close network monitor") + err = E.Append(err, r.networkMonitor.Close(), func(err error) error { + return E.Cause(err, "close network monitor") + }) + monitor.Finish() + } + if r.wifiMonitor != nil { + monitor.Start("close WIFI monitor") + err = E.Append(err, r.wifiMonitor.Close(), func(err error) error { + return E.Cause(err, "close WIFI monitor") + }) + monitor.Finish() + } + return err +} + +func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder { + return r.interfaceFinder +} + +func (r *NetworkManager) UpdateInterfaces() error { + if r.platformInterface == nil || !r.platformInterface.UsePlatformNetworkInterfaces() { + return r.interfaceFinder.Update() + } else { + interfaces, err := r.platformInterface.NetworkInterfaces() + if err != nil { + return err + } + if C.IsDarwin { + err = r.interfaceFinder.Update() + if err != nil { + return err + } + // NEInterface only provides name,index and type + interfaces = common.Map(interfaces, func(it adapter.NetworkInterface) adapter.NetworkInterface { + iif, _ := r.interfaceFinder.ByIndex(it.Index) + if iif != nil { + it.Interface = *iif + } + return it + }) + } else { + r.interfaceFinder.UpdateInterfaces(common.Map(interfaces, func(it adapter.NetworkInterface) control.Interface { return it.Interface })) + } + oldInterfaces := r.networkInterfaces.Load() + newInterfaces := common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return it.Flags&net.FlagUp != 0 + }) + r.networkInterfaces.Store(newInterfaces) + if len(newInterfaces) > 0 && !slices.EqualFunc(oldInterfaces, newInterfaces, func(oldInterface adapter.NetworkInterface, newInterface adapter.NetworkInterface) bool { + return oldInterface.Interface.Index == newInterface.Interface.Index && + oldInterface.Interface.Name == newInterface.Interface.Name && + oldInterface.Interface.Flags == newInterface.Interface.Flags && + oldInterface.Type == newInterface.Type && + oldInterface.Expensive == newInterface.Expensive && + oldInterface.Constrained == newInterface.Constrained + }) { + r.logger.Info("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string { + var options []string + options = append(options, F.ToString(it.Type)) + if it.Expensive { + options = append(options, "expensive") + } + if it.Constrained { + options = append(options, "constrained") + } + return F.ToString(it.Name, " (", strings.Join(options, ", "), ")") + }), ", ")) + } + return nil + } +} + +func (r *NetworkManager) DefaultNetworkInterface() *adapter.NetworkInterface { + iif := r.interfaceMonitor.DefaultInterface() + if iif == nil { + return nil + } + for _, it := range r.networkInterfaces.Load() { + if it.Interface.Index == iif.Index { + return &it + } + } + return &adapter.NetworkInterface{Interface: *iif} +} + +func (r *NetworkManager) NetworkInterfaces() []adapter.NetworkInterface { + return r.networkInterfaces.Load() +} + +func (r *NetworkManager) AutoDetectInterface() bool { + return r.autoDetectInterface +} + +func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func { + if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { + return func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return r.platformInterface.AutoDetectInterfaceControl(int(fd)) + }) + } + } else { + if r.interfaceMonitor == nil { + return nil + } + return control.BindToInterfaceFunc(r.interfaceFinder, func(network string, address string) (interfaceName string, interfaceIndex int, err error) { + remoteAddr := M.ParseSocksaddr(address).Addr + if remoteAddr.IsValid() { + iif, err := r.interfaceFinder.ByAddr(remoteAddr) + if err == nil { + return iif.Name, iif.Index, nil + } + } + defaultInterface := r.interfaceMonitor.DefaultInterface() + if defaultInterface == nil { + return "", -1, tun.ErrNoRoute + } + return defaultInterface.Name, defaultInterface.Index, nil + }) + } +} + +func (r *NetworkManager) ProtectFunc() control.Func { + if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { + return func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return r.platformInterface.AutoDetectInterfaceControl(int(fd)) + }) + } + } + return nil +} + +func (r *NetworkManager) DefaultOptions() adapter.NetworkOptions { + return r.defaultOptions +} + +func (r *NetworkManager) RegisterAutoRedirectOutputMark(mark uint32) error { + if r.autoRedirectOutputMark > 0 { + return E.New("only one auto-redirect can be configured") + } + r.autoRedirectOutputMark = mark + return nil +} + +func (r *NetworkManager) AutoRedirectOutputMark() uint32 { + return r.autoRedirectOutputMark +} + +func (r *NetworkManager) AutoRedirectOutputMarkFunc() control.Func { + return func(network, address string, conn syscall.RawConn) error { + if r.autoRedirectOutputMark == 0 { + return nil + } + return control.RoutingMark(r.autoRedirectOutputMark)(network, address, conn) + } +} + +func (r *NetworkManager) NetworkMonitor() tun.NetworkUpdateMonitor { + return r.networkMonitor +} + +func (r *NetworkManager) InterfaceMonitor() tun.DefaultInterfaceMonitor { + return r.interfaceMonitor +} + +func (r *NetworkManager) PackageManager() tun.PackageManager { + return r.packageManager +} + +func (r *NetworkManager) NeedWIFIState() bool { + return r.needWIFIState +} + +func (r *NetworkManager) WIFIState() adapter.WIFIState { + r.wifiStateMutex.RLock() + defer r.wifiStateMutex.RUnlock() + return r.wifiState +} + +func (r *NetworkManager) onWIFIStateChanged(state adapter.WIFIState) { + r.wifiStateMutex.Lock() + if state != r.wifiState { + r.wifiState = state + r.wifiStateMutex.Unlock() + if state.SSID != "" { + r.logger.Info("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID) + } else { + r.logger.Info("WIFI disconnected") + } + } else { + r.wifiStateMutex.Unlock() + } +} + +func (r *NetworkManager) UpdateWIFIState() { + var state adapter.WIFIState + if r.wifiMonitor != nil { + state = r.wifiMonitor.ReadWIFIState() + } else if r.platformInterface != nil && r.platformInterface.UsePlatformWIFIMonitor() { + state = r.platformInterface.ReadWIFIState() + } else { + return + } + r.onWIFIStateChanged(state) +} + +func (r *NetworkManager) ResetNetwork() { + if r.connectionManager != nil { + r.connectionManager.CloseAll() + } + + for _, endpoint := range r.endpoint.Endpoints() { + listener, isListener := endpoint.(adapter.InterfaceUpdateListener) + if isListener { + listener.InterfaceUpdated() + } + } + + for _, inbound := range r.inbound.Inbounds() { + listener, isListener := inbound.(adapter.InterfaceUpdateListener) + if isListener { + listener.InterfaceUpdated() + } + } + + for _, outbound := range r.outbound.Outbounds() { + listener, isListener := outbound.(adapter.InterfaceUpdateListener) + if isListener { + listener.InterfaceUpdated() + } + } +} + +func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interface, flags int) { + if defaultInterface == nil { + r.pauseManager.NetworkPause() + r.logger.Error("missing default interface") + return + } + + r.pauseManager.NetworkWake() + var options []string + options = append(options, F.ToString("index ", defaultInterface.Index)) + if C.IsAndroid && r.platformInterface == nil { + var vpnStatus string + if r.interfaceMonitor.AndroidVPNEnabled() { + vpnStatus = "enabled" + } else { + vpnStatus = "disabled" + } + options = append(options, "vpn "+vpnStatus) + } else if r.platformInterface != nil { + networkInterface := common.Find(r.networkInterfaces.Load(), func(it adapter.NetworkInterface) bool { + return it.Interface.Index == defaultInterface.Index + }) + if networkInterface.Name == "" { + // race + return + } + options = append(options, F.ToString("type ", networkInterface.Type)) + if networkInterface.Expensive { + options = append(options, "expensive") + } + if networkInterface.Constrained { + options = append(options, "constrained") + } + } + r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", ")) + r.UpdateWIFIState() + + if !r.started { + return + } + r.ResetNetwork() +} + +func (r *NetworkManager) notifyWindowsPowerEvent(event int) { + switch event { + case winpowrprof.EVENT_SUSPEND: + r.pauseManager.DevicePause() + r.ResetNetwork() + case winpowrprof.EVENT_RESUME: + if !r.pauseManager.IsDevicePaused() { + return + } + fallthrough + case winpowrprof.EVENT_RESUME_AUTOMATIC: + r.pauseManager.DeviceWake() + r.ResetNetwork() + } +} + +func (r *NetworkManager) OnPackagesUpdated(packages int, sharedUsers int) { + r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users") +} diff --git a/route/platform_searcher.go b/route/platform_searcher.go new file mode 100644 index 00000000..20fbda3f --- /dev/null +++ b/route/platform_searcher.go @@ -0,0 +1,49 @@ +package route + +import ( + "context" + "net/netip" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/process" + N "github.com/sagernet/sing/common/network" +) + +type platformSearcher struct { + platform adapter.PlatformInterface +} + +func newPlatformSearcher(platform adapter.PlatformInterface) process.Searcher { + return &platformSearcher{platform: platform} +} + +func (s *platformSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + if !s.platform.UsePlatformConnectionOwnerFinder() { + return nil, process.ErrNotFound + } + + var ipProtocol int32 + switch N.NetworkName(network) { + case N.NetworkTCP: + ipProtocol = syscall.IPPROTO_TCP + case N.NetworkUDP: + ipProtocol = syscall.IPPROTO_UDP + default: + return nil, process.ErrNotFound + } + + request := &adapter.FindConnectionOwnerRequest{ + IpProtocol: ipProtocol, + SourceAddress: source.Addr().String(), + SourcePort: int32(source.Port()), + DestinationAddress: destination.Addr().String(), + DestinationPort: int32(destination.Port()), + } + + return s.platform.FindConnectionOwner(request) +} + +func (s *platformSearcher) Close() error { + return nil +} diff --git a/route/process_cache.go b/route/process_cache.go new file mode 100644 index 00000000..691a4e8e --- /dev/null +++ b/route/process_cache.go @@ -0,0 +1,34 @@ +package route + +import ( + "context" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/process" +) + +type processCacheKey struct { + Network string + Source netip.AddrPort + Destination netip.AddrPort +} + +type processCacheEntry struct { + result *adapter.ConnectionOwner + err error +} + +func (r *Router) findProcessInfoCached(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + key := processCacheKey{ + Network: network, + Source: source, + Destination: destination, + } + if entry, ok := r.processCache.Get(key); ok { + return entry.result, entry.err + } + result, err := process.FindProcessInfo(r.processSearcher, ctx, network, source, destination) + r.processCache.Add(key, processCacheEntry{result: result, err: err}) + return result, err +} diff --git a/route/route.go b/route/route.go new file mode 100644 index 00000000..32e07bae --- /dev/null +++ b/route/route.go @@ -0,0 +1,833 @@ +package route + +import ( + "context" + "errors" + "net" + "net/netip" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + R "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-mux" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" + + "golang.org/x/exp/slices" +) + +// Deprecated: use RouteConnectionEx instead. +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + done := make(chan interface{}) + err := r.routeConnection(ctx, conn, metadata, N.OnceClose(func(it error) { + close(done) + })) + if err != nil { + return err + } + select { + case <-done: + case <-r.ctx.Done(): + } + return nil +} + +func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := r.routeConnection(ctx, conn, metadata, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + if E.IsClosedOrCanceled(err) || R.IsRejected(err) { + r.logger.DebugContext(ctx, "connection closed: ", err) + } else { + r.logger.ErrorContext(ctx, err) + } + } +} + +func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + //nolint:staticcheck + if metadata.InboundDetour != "" { + if metadata.LastInbound == metadata.InboundDetour { + return E.New("routing loop on detour: ", metadata.InboundDetour) + } + detour, loaded := r.inbound.Get(metadata.InboundDetour) + if !loaded { + return E.New("inbound detour not found: ", metadata.InboundDetour) + } + injectable, isInjectable := detour.(adapter.TCPInjectableInbound) + if !isInjectable { + return E.New("inbound detour is not TCP injectable: ", metadata.InboundDetour) + } + metadata.LastInbound = metadata.Inbound + metadata.Inbound = metadata.InboundDetour + metadata.InboundDetour = "" + injectable.NewConnectionEx(ctx, conn, metadata, onClose) + return nil + } + metadata.Network = N.NetworkTCP + switch metadata.Destination.Fqdn { + case mux.Destination.Fqdn: + return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in Inbound fields instead.") + case vmess.MuxDestination.Fqdn: + return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.") + case uot.MagicAddress: + return E.New("global UoT not supported since sing-box v1.7.0.") + case uot.LegacyMagicAddress: + return E.New("global UoT (legacy) not supported since sing-box v1.7.0.") + } + if deadline.NeedAdditionalReadDeadline(conn) { + conn = deadline.NewConn(conn) + } + selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, false, conn, nil) + if err != nil { + return err + } + var selectedOutbound adapter.Outbound + if selectedRule != nil { + switch action := selectedRule.Action().(type) { + case *R.RuleActionRoute: + var loaded bool + selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) + if !loaded { + buf.ReleaseMulti(buffers) + return E.New("outbound not found: ", action.Outbound) + } + if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { + buf.ReleaseMulti(buffers) + return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) + } + case *R.RuleActionBypass: + if action.Outbound == "" { + break + } + var loaded bool + selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) + if !loaded { + buf.ReleaseMulti(buffers) + return E.New("outbound not found: ", action.Outbound) + } + if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { + buf.ReleaseMulti(buffers) + return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) + } + case *R.RuleActionReject: + buf.ReleaseMulti(buffers) + if action.Method == C.RuleActionRejectMethodReply { + return E.New("reject method `reply` is not supported for TCP connections") + } + return action.Error(ctx) + case *R.RuleActionHijackDNS: + for _, buffer := range buffers { + conn = bufio.NewCachedConn(conn, buffer) + } + N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata)) + return nil + } + } + if selectedRule == nil { + defaultOutbound := r.outbound.Default() + if !common.Contains(defaultOutbound.Network(), N.NetworkTCP) { + buf.ReleaseMulti(buffers) + return E.New("TCP is not supported by default outbound: ", defaultOutbound.Tag()) + } + selectedOutbound = defaultOutbound + } + + for _, buffer := range buffers { + conn = bufio.NewCachedConn(conn, buffer) + } + for _, tracker := range r.trackers { + conn = tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) + } + if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandlerEx); isHandler { + outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) + } else { + r.connection.NewConnection(ctx, selectedOutbound, conn, metadata, onClose) + } + return nil +} + +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + done := make(chan interface{}) + err := r.routePacketConnection(ctx, conn, metadata, N.OnceClose(func(it error) { + close(done) + })) + if err != nil { + conn.Close() + if E.IsClosedOrCanceled(err) || R.IsRejected(err) { + r.logger.DebugContext(ctx, "connection closed: ", err) + } else { + r.logger.ErrorContext(ctx, err) + } + } + select { + case <-done: + case <-r.ctx.Done(): + } + return nil +} + +func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := r.routePacketConnection(ctx, conn, metadata, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + if E.IsClosedOrCanceled(err) || R.IsRejected(err) { + r.logger.DebugContext(ctx, "connection closed: ", err) + } else { + r.logger.ErrorContext(ctx, err) + } + } +} + +func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + //nolint:staticcheck + if metadata.InboundDetour != "" { + if metadata.LastInbound == metadata.InboundDetour { + return E.New("routing loop on detour: ", metadata.InboundDetour) + } + detour, loaded := r.inbound.Get(metadata.InboundDetour) + if !loaded { + return E.New("inbound detour not found: ", metadata.InboundDetour) + } + injectable, isInjectable := detour.(adapter.UDPInjectableInbound) + if !isInjectable { + return E.New("inbound detour is not UDP injectable: ", metadata.InboundDetour) + } + metadata.LastInbound = metadata.Inbound + metadata.Inbound = metadata.InboundDetour + metadata.InboundDetour = "" + injectable.NewPacketConnectionEx(ctx, conn, metadata, onClose) + return nil + } + // TODO: move to UoT + metadata.Network = N.NetworkUDP + + // Currently we don't have deadline usages for UDP connections + /*if deadline.NeedAdditionalReadDeadline(conn) { + conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) + }*/ + + selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn) + if err != nil { + return err + } + var selectedOutbound adapter.Outbound + var selectReturn bool + if selectedRule != nil { + switch action := selectedRule.Action().(type) { + case *R.RuleActionRoute: + var loaded bool + selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) + if !loaded { + N.ReleaseMultiPacketBuffer(packetBuffers) + return E.New("outbound not found: ", action.Outbound) + } + if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { + N.ReleaseMultiPacketBuffer(packetBuffers) + return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) + } + case *R.RuleActionBypass: + if action.Outbound == "" { + break + } + var loaded bool + selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) + if !loaded { + N.ReleaseMultiPacketBuffer(packetBuffers) + return E.New("outbound not found: ", action.Outbound) + } + if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { + N.ReleaseMultiPacketBuffer(packetBuffers) + return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) + } + case *R.RuleActionReject: + N.ReleaseMultiPacketBuffer(packetBuffers) + if action.Method == C.RuleActionRejectMethodReply { + return E.New("reject method `reply` is not supported for UDP connections") + } + return action.Error(ctx) + case *R.RuleActionHijackDNS: + return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose) + } + } + if selectedRule == nil || selectReturn { + defaultOutbound := r.outbound.Default() + if !common.Contains(defaultOutbound.Network(), N.NetworkUDP) { + N.ReleaseMultiPacketBuffer(packetBuffers) + return E.New("UDP is not supported by outbound: ", defaultOutbound.Tag()) + } + selectedOutbound = defaultOutbound + } + for _, buffer := range packetBuffers { + conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) + N.PutPacketBuffer(buffer) + } + for _, tracker := range r.trackers { + conn = tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound) + } + if metadata.FakeIP { + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) + } + if outboundHandler, isHandler := selectedOutbound.(adapter.PacketConnectionHandlerEx); isHandler { + outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) + } else { + r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose) + } + return nil +} + +func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) { + selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, supportBypass, nil, nil) + if err != nil { + return nil, err + } + var directRouteOutbound adapter.DirectRouteOutbound + if selectedRule != nil { + switch action := selectedRule.Action().(type) { + case *R.RuleActionReject: + switch metadata.Network { + case N.NetworkTCP: + if action.Method == C.RuleActionRejectMethodReply { + return nil, E.New("reject method `reply` is not supported for TCP connections") + } + case N.NetworkUDP: + if action.Method == C.RuleActionRejectMethodReply { + return nil, E.New("reject method `reply` is not supported for UDP connections") + } + } + return nil, action.Error(context.Background()) + case *R.RuleActionBypass: + if supportBypass { + return nil, &R.BypassedError{Cause: tun.ErrBypass} + } + if routeContext == nil { + return nil, nil + } + outbound, loaded := r.outbound.Outbound(action.Outbound) + if !loaded { + return nil, E.New("outbound not found: ", action.Outbound) + } + if !common.Contains(outbound.Network(), metadata.Network) { + return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound) + } + directRouteOutbound = outbound.(adapter.DirectRouteOutbound) + case *R.RuleActionRoute: + if routeContext == nil { + return nil, nil + } + outbound, loaded := r.outbound.Outbound(action.Outbound) + if !loaded { + return nil, E.New("outbound not found: ", action.Outbound) + } + if !common.Contains(outbound.Network(), metadata.Network) { + return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound) + } + directRouteOutbound = outbound.(adapter.DirectRouteOutbound) + } + } + if directRouteOutbound == nil { + if selectedRule != nil || metadata.Network != N.NetworkICMP { + return nil, nil + } + defaultOutbound := r.outbound.Default() + if !common.Contains(defaultOutbound.Network(), metadata.Network) { + return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag()) + } + directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound) + } + if metadata.Destination.IsDomain() { + if len(metadata.DestinationAddresses) == 0 { + var strategy C.DomainStrategy + if metadata.Source.IsIPv4() { + strategy = C.DomainStrategyIPv4Only + } else { + strategy = C.DomainStrategyIPv6Only + } + err = r.actionResolve(r.ctx, &metadata, &R.RuleActionResolve{ + Strategy: strategy, + }) + if err != nil { + return nil, err + } + } + var newDestination netip.Addr + if metadata.Source.IsIPv4() { + for _, address := range metadata.DestinationAddresses { + if address.Is4() { + newDestination = address + break + } + } + } else { + for _, address := range metadata.DestinationAddresses { + if address.Is6() { + newDestination = address + break + } + } + } + if !newDestination.IsValid() { + if metadata.Source.IsIPv4() { + return nil, E.New("no IPv4 address found for domain: ", metadata.Destination.Fqdn) + } else { + return nil, E.New("no IPv6 address found for domain: ", metadata.Destination.Fqdn) + } + } + metadata.Destination = M.Socksaddr{ + Addr: newDestination, + } + routeContext = ping.NewContextDestinationWriter(routeContext, metadata.OriginDestination.Addr) + var routeDestination tun.DirectRouteDestination + routeDestination, err = directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout) + if err != nil { + return nil, err + } + return ping.NewDestinationWriter(routeDestination, newDestination), nil + } + return directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout) +} + +func (r *Router) matchRule( + ctx context.Context, metadata *adapter.InboundContext, preMatch bool, supportBypass bool, + inputConn net.Conn, inputPacketConn N.PacketConn, +) ( + selectedRule adapter.Rule, selectedRuleIndex int, + buffers []*buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error, +) { + if r.processSearcher != nil && metadata.ProcessInfo == nil { + var originDestination netip.AddrPort + if metadata.OriginDestination.IsValid() { + originDestination = metadata.OriginDestination.AddrPort() + } else if metadata.Destination.IsIP() { + originDestination = metadata.Destination.AddrPort() + } + processInfo, fErr := r.findProcessInfoCached(ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) + if fErr != nil { + r.logger.InfoContext(ctx, "failed to search process: ", fErr) + } else { + if processInfo.ProcessPath != "" { + if processInfo.UserName != "" { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.UserName) + } else if processInfo.UserId != -1 { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId) + } else { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) + } + } else if len(processInfo.AndroidPackageNames) > 0 { + r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", ")) + } else if processInfo.UserId != -1 { + if processInfo.UserName != "" { + r.logger.InfoContext(ctx, "found user: ", processInfo.UserName) + } else { + r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId) + } + } + metadata.ProcessInfo = processInfo + } + } + if r.neighborResolver != nil && metadata.SourceMACAddress == nil && metadata.Source.Addr.IsValid() { + mac, macFound := r.neighborResolver.LookupMAC(metadata.Source.Addr) + if macFound { + metadata.SourceMACAddress = mac + } + hostname, hostnameFound := r.neighborResolver.LookupHostname(metadata.Source.Addr) + if hostnameFound { + metadata.SourceHostname = hostname + if macFound { + r.logger.InfoContext(ctx, "found neighbor: ", mac, ", hostname: ", hostname) + } else { + r.logger.InfoContext(ctx, "found neighbor hostname: ", hostname) + } + } else if macFound { + r.logger.InfoContext(ctx, "found neighbor: ", mac) + } + } + if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) { + domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr) + if !loaded { + fatalErr = E.New("missing fakeip record, try enable `experimental.cache_file`") + return + } + if domain != "" { + metadata.OriginDestination = metadata.Destination + metadata.Destination = M.Socksaddr{ + Fqdn: domain, + Port: metadata.Destination.Port, + } + metadata.FakeIP = true + r.logger.DebugContext(ctx, "found fakeip domain: ", domain) + } + } else if metadata.Domain == "" { + domain, loaded := r.dns.LookupReverseMapping(metadata.Destination.Addr) + if loaded { + metadata.Domain = domain + r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain) + } + } + if metadata.Destination.IsIPv4() { + metadata.IPVersion = 4 + } else if metadata.Destination.IsIPv6() { + metadata.IPVersion = 6 + } + +match: + for currentRuleIndex, currentRule := range r.rules { + metadata.ResetRuleCache() + if !currentRule.Match(metadata) { + continue + } + if !preMatch { + ruleDescription := currentRule.String() + if ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) + } + } else { + switch currentRule.Action().Type() { + case C.RuleActionTypeReject: + ruleDescription := currentRule.String() + if ruleDescription != "" { + r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] => ", currentRule.Action()) + } + } + } + var routeOptions *R.RuleActionRouteOptions + switch action := currentRule.Action().(type) { + case *R.RuleActionRoute: + routeOptions = &action.RuleActionRouteOptions + case *R.RuleActionRouteOptions: + routeOptions = action + } + if routeOptions != nil { + // TODO: add nat + if (routeOptions.OverrideAddress.IsValid() || routeOptions.OverridePort > 0) && !metadata.RouteOriginalDestination.IsValid() { + metadata.RouteOriginalDestination = metadata.Destination + } + if routeOptions.OverrideAddress.IsValid() { + metadata.Destination = M.Socksaddr{ + Addr: routeOptions.OverrideAddress.Addr, + Port: metadata.Destination.Port, + Fqdn: routeOptions.OverrideAddress.Fqdn, + } + metadata.DestinationAddresses = nil + } + if routeOptions.OverridePort > 0 { + metadata.Destination = M.Socksaddr{ + Addr: metadata.Destination.Addr, + Port: routeOptions.OverridePort, + Fqdn: metadata.Destination.Fqdn, + } + } + if routeOptions.NetworkStrategy != nil { + metadata.NetworkStrategy = routeOptions.NetworkStrategy + } + if len(routeOptions.NetworkType) > 0 { + metadata.NetworkType = routeOptions.NetworkType + } + if len(routeOptions.FallbackNetworkType) > 0 { + metadata.FallbackNetworkType = routeOptions.FallbackNetworkType + } + if routeOptions.FallbackDelay != 0 { + metadata.FallbackDelay = routeOptions.FallbackDelay + } + if routeOptions.UDPDisableDomainUnmapping { + metadata.UDPDisableDomainUnmapping = true + } + if routeOptions.UDPConnect { + metadata.UDPConnect = true + } + if routeOptions.UDPTimeout > 0 { + metadata.UDPTimeout = routeOptions.UDPTimeout + } + if routeOptions.TLSFragment { + metadata.TLSFragment = true + metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay + } + if routeOptions.TLSRecordFragment { + metadata.TLSRecordFragment = true + } + } + switch action := currentRule.Action().(type) { + case *R.RuleActionSniff: + if !preMatch { + newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers) + if newBuffer != nil { + buffers = append(buffers, newBuffer) + } else if len(newPacketBuffers) > 0 { + packetBuffers = append(packetBuffers, newPacketBuffers...) + } + if newErr != nil { + fatalErr = newErr + return + } + } else if metadata.Network != N.NetworkICMP { + selectedRule = currentRule + selectedRuleIndex = currentRuleIndex + break match + } + case *R.RuleActionResolve: + fatalErr = r.actionResolve(ctx, metadata, action) + if fatalErr != nil { + return + } + } + actionType := currentRule.Action().Type() + if actionType == C.RuleActionTypeRoute || + actionType == C.RuleActionTypeReject || + actionType == C.RuleActionTypeHijackDNS { + selectedRule = currentRule + selectedRuleIndex = currentRuleIndex + break match + } + if actionType == C.RuleActionTypeBypass { + bypassAction := currentRule.Action().(*R.RuleActionBypass) + if !supportBypass && bypassAction.Outbound == "" { + continue match + } + selectedRule = currentRule + selectedRuleIndex = currentRuleIndex + break match + } + } + return +} + +func (r *Router) actionSniff( + ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff, + inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer, inputPacketBuffers []*N.PacketBuffer, +) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) { + if sniff.Skip(metadata) { + r.logger.DebugContext(ctx, "sniff skipped due to port considered as server-first") + return + } else if metadata.Protocol != "" { + r.logger.DebugContext(ctx, "duplicate sniff skipped") + return + } + if inputConn != nil { + if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 { + return + } else if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) { + r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError) + return + } + var streamSniffers []sniff.StreamSniffer + if len(action.StreamSniffers) > 0 { + streamSniffers = action.StreamSniffers + } else { + streamSniffers = []sniff.StreamSniffer{ + sniff.TLSClientHello, + sniff.HTTPHost, + sniff.StreamDomainNameQuery, + sniff.BitTorrent, + sniff.SSH, + sniff.RDP, + } + } + sniffBuffer := buf.NewPacket() + err := sniff.PeekStream( + ctx, + metadata, + inputConn, + inputBuffers, + sniffBuffer, + action.Timeout, + streamSniffers..., + ) + metadata.SnifferNames = action.SnifferNames + metadata.SniffError = err + if err == nil { + //goland:noinspection GoDeprecation + if action.OverrideDestination && M.IsDomainName(metadata.Domain) { + metadata.Destination = M.Socksaddr{ + Fqdn: metadata.Domain, + Port: metadata.Destination.Port, + } + } + if metadata.Domain != "" && metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) + } else if metadata.Domain != "" { + r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + } else { + r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) + } + } + if !sniffBuffer.IsEmpty() { + buffer = sniffBuffer + } else { + sniffBuffer.Release() + } + } else if inputPacketConn != nil { + if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 { + return + } else if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) { + r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError) + return + } + quicMoreData := func() bool { + return slices.Equal(metadata.SnifferNames, action.SnifferNames) && errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) + } + var packetSniffers []sniff.PacketSniffer + if len(action.PacketSniffers) > 0 { + packetSniffers = action.PacketSniffers + } else { + packetSniffers = []sniff.PacketSniffer{ + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + sniff.DTLSRecord, + sniff.NTP, + } + } + var err error + for _, packetBuffer := range inputPacketBuffers { + if quicMoreData() { + err = sniff.PeekPacket( + ctx, + metadata, + packetBuffer.Buffer.Bytes(), + sniff.QUICClientHello, + ) + } else { + err = sniff.PeekPacket( + ctx, metadata, + packetBuffer.Buffer.Bytes(), + packetSniffers..., + ) + } + metadata.SnifferNames = action.SnifferNames + metadata.SniffError = err + if errors.Is(err, sniff.ErrNeedMoreData) { + // TODO: replace with generic message when there are more multi-packet protocols + r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") + continue + } + goto finally + } + packetBuffers = inputPacketBuffers + for { + var ( + sniffBuffer = buf.NewPacket() + destination M.Socksaddr + done = make(chan struct{}) + ) + go func() { + sniffTimeout := C.ReadPayloadTimeout + if action.Timeout > 0 { + sniffTimeout = action.Timeout + } + inputPacketConn.SetReadDeadline(time.Now().Add(sniffTimeout)) + destination, err = inputPacketConn.ReadPacket(sniffBuffer) + inputPacketConn.SetReadDeadline(time.Time{}) + close(done) + }() + select { + case <-done: + case <-ctx.Done(): + inputPacketConn.Close() + fatalErr = ctx.Err() + return + } + if err != nil { + sniffBuffer.Release() + if !errors.Is(err, context.DeadlineExceeded) { + fatalErr = err + return + } + } else { + if quicMoreData() { + err = sniff.PeekPacket( + ctx, + metadata, + sniffBuffer.Bytes(), + sniff.QUICClientHello, + ) + } else { + err = sniff.PeekPacket( + ctx, metadata, + sniffBuffer.Bytes(), + packetSniffers..., + ) + } + packetBuffer := N.NewPacketBuffer() + *packetBuffer = N.PacketBuffer{ + Buffer: sniffBuffer, + Destination: destination, + } + packetBuffers = append(packetBuffers, packetBuffer) + metadata.SnifferNames = action.SnifferNames + metadata.SniffError = err + if errors.Is(err, sniff.ErrNeedMoreData) { + // TODO: replace with generic message when there are more multi-packet protocols + r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") + continue + } + } + goto finally + } + finally: + if err == nil { + //goland:noinspection GoDeprecation + if action.OverrideDestination && M.IsDomainName(metadata.Domain) { + metadata.Destination = M.Socksaddr{ + Fqdn: metadata.Domain, + Port: metadata.Destination.Port, + } + } + if metadata.Domain != "" && metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) + } else if metadata.Domain != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + } else if metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client) + } else { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) + } + } + } + return +} + +func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionResolve) error { + if metadata.Destination.IsDomain() { + var transport adapter.DNSTransport + if action.Server != "" { + var loaded bool + transport, loaded = r.dnsTransport.Transport(action.Server) + if !loaded { + return E.New("DNS server not found: ", action.Server) + } + } + addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{ + Transport: transport, + Strategy: action.Strategy, + DisableCache: action.DisableCache, + DisableOptimisticCache: action.DisableOptimisticCache, + RewriteTTL: action.RewriteTTL, + ClientSubnet: action.ClientSubnet, + }) + if err != nil { + return err + } + metadata.DestinationAddresses = addresses + r.logger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]") + } + return nil +} diff --git a/route/router.go b/route/router.go new file mode 100644 index 00000000..03546b2a --- /dev/null +++ b/route/router.go @@ -0,0 +1,284 @@ +package route + +import ( + "context" + "os" + "runtime" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + R "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/task" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +var _ adapter.Router = (*Router)(nil) + +type Router struct { + ctx context.Context + logger log.ContextLogger + inbound adapter.InboundManager + outbound adapter.OutboundManager + dns adapter.DNSRouter + dnsTransport adapter.DNSTransportManager + connection adapter.ConnectionManager + network adapter.NetworkManager + rules []adapter.Rule + needFindProcess bool + needFindNeighbor bool + leaseFiles []string + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet + processSearcher process.Searcher + processCache freelru.Cache[processCacheKey, processCacheEntry] + neighborResolver adapter.NeighborResolver + pauseManager pause.Manager + trackers []adapter.ConnectionTracker + platformInterface adapter.PlatformInterface + started bool +} + +func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions) *Router { + return &Router{ + ctx: ctx, + logger: logFactory.NewLogger("router"), + inbound: service.FromContext[adapter.InboundManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + dns: service.FromContext[adapter.DNSRouter](ctx), + dnsTransport: service.FromContext[adapter.DNSTransportManager](ctx), + connection: service.FromContext[adapter.ConnectionManager](ctx), + network: service.FromContext[adapter.NetworkManager](ctx), + rules: make([]adapter.Rule, 0, len(options.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), + needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, + needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || options.FindNeighbor, + leaseFiles: options.DHCPLeaseFiles, + pauseManager: service.FromContext[pause.Manager](ctx), + platformInterface: service.FromContext[adapter.PlatformInterface](ctx), + } +} + +func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) error { + for i, options := range rules { + err := R.ValidateNoNestedRuleActions(options) + if err != nil { + return E.Cause(err, "parse rule[", i, "]") + } + rule, err := R.NewRule(r.ctx, r.logger, options, false) + if err != nil { + return E.Cause(err, "parse rule[", i, "]") + } + r.rules = append(r.rules, rule) + } + for i, options := range ruleSets { + if _, exists := r.ruleSetMap[options.Tag]; exists { + return E.New("duplicate rule-set tag: ", options.Tag) + } + ruleSet, err := R.NewRuleSet(r.ctx, r.logger, options) + if err != nil { + return E.Cause(err, "parse rule-set[", i, "]") + } + r.ruleSets = append(r.ruleSets, ruleSet) + r.ruleSetMap[options.Tag] = ruleSet + } + return nil +} + +func (r *Router) Start(stage adapter.StartStage) error { + monitor := taskmonitor.New(r.logger, C.StartTimeout) + switch stage { + case adapter.StartStateStart: + var cacheContext *adapter.HTTPStartContext + if len(r.ruleSets) > 0 { + monitor.Start("initialize rule-set") + cacheContext = adapter.NewHTTPStartContext(r.ctx) + var ruleSetStartGroup task.Group + for i, ruleSet := range r.ruleSets { + ruleSetInPlace := ruleSet + ruleSetStartGroup.Append0(func(ctx context.Context) error { + err := ruleSetInPlace.StartContext(ctx, cacheContext) + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + return nil + }) + } + ruleSetStartGroup.Concurrency(5) + ruleSetStartGroup.FastFail() + err := ruleSetStartGroup.Run(r.ctx) + monitor.Finish() + if err != nil { + return err + } + } + if cacheContext != nil { + cacheContext.Close() + } + r.network.Initialize(r.ruleSets) + needFindProcess := r.needFindProcess + needFindNeighbor := r.needFindNeighbor + for _, ruleSet := range r.ruleSets { + metadata := ruleSet.Metadata() + if metadata.ContainsProcessRule { + needFindProcess = true + } + } + if C.IsAndroid && r.platformInterface != nil { + needFindProcess = true + } + r.needFindProcess = needFindProcess + if needFindProcess { + if r.platformInterface != nil && r.platformInterface.UsePlatformConnectionOwnerFinder() { + r.processSearcher = newPlatformSearcher(r.platformInterface) + } else { + monitor.Start("initialize process searcher") + searcher, err := process.NewSearcher(process.Config{ + Logger: r.logger, + PackageManager: r.network.PackageManager(), + }) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create process searcher")) + } + } else { + r.processSearcher = searcher + } + } + } + if r.processSearcher != nil { + processCache := common.Must1(freelru.NewSharded[processCacheKey, processCacheEntry](256, maphash.NewHasher[processCacheKey]().Hash32)) + processCache.SetLifetime(200 * time.Millisecond) + r.processCache = processCache + } + r.needFindNeighbor = needFindNeighbor + if needFindNeighbor { + if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() { + monitor.Start("initialize neighbor resolver") + resolver := newPlatformNeighborResolver(r.logger, r.platformInterface) + err := resolver.Start() + monitor.Finish() + if err != nil { + r.logger.Error(E.Cause(err, "start neighbor resolver")) + } else { + r.neighborResolver = resolver + } + } else { + monitor.Start("initialize neighbor resolver") + resolver, err := newNeighborResolver(r.logger, r.leaseFiles) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Error(E.Cause(err, "create neighbor resolver")) + } + } else { + err = resolver.Start() + if err != nil { + r.logger.Error(E.Cause(err, "start neighbor resolver")) + } else { + r.neighborResolver = resolver + } + } + } + } + case adapter.StartStatePostStart: + for i, rule := range r.rules { + monitor.Start("initialize rule[", i, "]") + err := rule.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "initialize rule[", i, "]") + } + } + for _, ruleSet := range r.ruleSets { + monitor.Start("post start rule_set[", ruleSet.Name(), "]") + err := ruleSet.PostStart() + monitor.Finish() + if err != nil { + return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]") + } + } + r.started = true + return nil + case adapter.StartStateStarted: + for _, ruleSet := range r.ruleSets { + ruleSet.Cleanup() + } + runtime.GC() + } + return nil +} + +func (r *Router) Close() error { + monitor := taskmonitor.New(r.logger, C.StopTimeout) + var err error + if r.neighborResolver != nil { + monitor.Start("close neighbor resolver") + err = E.Append(err, r.neighborResolver.Close(), func(closeErr error) error { + return E.Cause(closeErr, "close neighbor resolver") + }) + monitor.Finish() + } + for i, rule := range r.rules { + monitor.Start("close rule[", i, "]") + err = E.Append(err, rule.Close(), func(err error) error { + return E.Cause(err, "close rule[", i, "]") + }) + monitor.Finish() + } + for i, ruleSet := range r.ruleSets { + monitor.Start("close rule-set[", i, "]") + err = E.Append(err, ruleSet.Close(), func(err error) error { + return E.Cause(err, "close rule-set[", i, "]") + }) + monitor.Finish() + } + if r.processSearcher != nil { + monitor.Start("close process searcher") + err = E.Append(err, r.processSearcher.Close(), func(err error) error { + return E.Cause(err, "close process searcher") + }) + monitor.Finish() + } + return err +} + +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + +func (r *Router) Rules() []adapter.Rule { + return r.rules +} + +func (r *Router) AppendTracker(tracker adapter.ConnectionTracker) { + r.trackers = append(r.trackers, tracker) +} + +func (r *Router) NeedFindProcess() bool { + return r.needFindProcess +} + +func (r *Router) NeedFindNeighbor() bool { + return r.needFindNeighbor +} + +func (r *Router) NeighborResolver() adapter.NeighborResolver { + return r.neighborResolver +} + +func (r *Router) ResetNetwork() { + r.network.ResetNetwork() + r.dns.ResetNetwork() +} diff --git a/route/rule/match_state.go b/route/rule/match_state.go new file mode 100644 index 00000000..feac8418 --- /dev/null +++ b/route/rule/match_state.go @@ -0,0 +1,126 @@ +package rule + +import "github.com/sagernet/sing-box/adapter" + +type ruleMatchState uint8 + +const ( + ruleMatchSourceAddress ruleMatchState = 1 << iota + ruleMatchSourcePort + ruleMatchDestinationAddress + ruleMatchDestinationPort +) + +type ruleMatchStateSet uint16 + +func singleRuleMatchState(state ruleMatchState) ruleMatchStateSet { + return 1 << state +} + +func emptyRuleMatchState() ruleMatchStateSet { + return singleRuleMatchState(0) +} + +func (s ruleMatchStateSet) isEmpty() bool { + return s == 0 +} + +func (s ruleMatchStateSet) contains(state ruleMatchState) bool { + return s&(1< 0 +} + +func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool { + return !metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 +} + +func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool { + return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata) +} + +func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.InboundContext) bool { + return len(r.destinationAddressItems) > 0 || r.destinationIPCIDRMatchesDestination(metadata) +} + +func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.matchStatesWithBase(metadata, 0) +} + +func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) ruleMatchStateSet { + if len(r.allItems) == 0 { + return emptyRuleMatchState().withBase(inheritedBase) + } + evaluationBase := inheritedBase + if r.invert { + evaluationBase = 0 + } + baseState := evaluationBase + if len(r.sourceAddressItems) > 0 { + metadata.DidMatch = true + if matchAnyItem(r.sourceAddressItems, metadata) { + baseState |= ruleMatchSourceAddress + } + } + if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) { + metadata.DidMatch = true + if matchAnyItem(r.destinationIPCIDRItems, metadata) { + baseState |= ruleMatchSourceAddress + } + } else if r.destinationIPCIDRMatchesSource(metadata) { + metadata.DidMatch = true + } + if len(r.sourcePortItems) > 0 { + metadata.DidMatch = true + if matchAnyItem(r.sourcePortItems, metadata) { + baseState |= ruleMatchSourcePort + } + } + if len(r.destinationAddressItems) > 0 { + metadata.DidMatch = true + if matchAnyItem(r.destinationAddressItems, metadata) { + baseState |= ruleMatchDestinationAddress + } + } + if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) { + metadata.DidMatch = true + if matchAnyItem(r.destinationIPCIDRItems, metadata) { + baseState |= ruleMatchDestinationAddress + } + } else if r.destinationIPCIDRMatchesDestination(metadata) { + metadata.DidMatch = true + } + if len(r.destinationPortItems) > 0 { + metadata.DidMatch = true + if matchAnyItem(r.destinationPortItems, metadata) { + baseState |= ruleMatchDestinationPort + } + } + for _, item := range r.items { + metadata.DidMatch = true + if !item.Match(metadata) { + return r.invertedFailure(inheritedBase) + } + } + var stateSet ruleMatchStateSet + if r.ruleSetItem != nil { + metadata.DidMatch = true + stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState) + } else { + stateSet = singleRuleMatchState(baseState) + } + stateSet = stateSet.filter(func(state ruleMatchState) bool { + if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) { + return false + } + if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) { + return false + } + if r.requiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) { + return false + } + if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) { + return false + } + return true + }) + if stateSet.isEmpty() { + return r.invertedFailure(inheritedBase) + } + if r.invert { + if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { + return emptyRuleMatchState().withBase(inheritedBase) + } + return 0 + } + return stateSet +} + +func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet { + if r.invert { + return emptyRuleMatchState().withBase(base) + } + return 0 +} + +func (r *abstractDefaultRule) Action() adapter.RuleAction { + return r.action +} + +func (r *abstractDefaultRule) String() string { + if !r.invert { + return strings.Join(F.MapToString(r.allItems), " ") + } else { + return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")" + } +} + +type abstractLogicalRule struct { + rules []adapter.HeadlessRule + mode string + invert bool + action adapter.RuleAction +} + +func (r *abstractLogicalRule) Type() string { + return C.RuleTypeLogical +} + +func (r *abstractLogicalRule) Start() error { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (interface { + Start() error + }, bool, + ) { + rule, loaded := it.(interface { + Start() error + }) + return rule, loaded + }) { + err := rule.Start() + if err != nil { + return err + } + } + return nil +} + +func (r *abstractLogicalRule) Close() error { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { + err := rule.Close() + if err != nil { + return err + } + } + return nil +} + +func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { + return !r.matchStates(metadata).isEmpty() +} + +func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.matchStatesWithBase(metadata, 0) +} + +func (r *abstractLogicalRule) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + evaluationBase := base + if r.invert { + evaluationBase = 0 + } + var stateSet ruleMatchStateSet + if r.mode == C.LogicalTypeAnd { + stateSet = emptyRuleMatchState().withBase(evaluationBase) + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase) + if nestedStateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState().withBase(base) + } + return 0 + } + stateSet = stateSet.combine(nestedStateSet) + } + } else { + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)) + } + if stateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState().withBase(base) + } + return 0 + } + } + if r.invert { + return 0 + } + return stateSet +} + +func (r *abstractLogicalRule) Action() adapter.RuleAction { + return r.action +} + +func (r *abstractLogicalRule) String() string { + var op string + switch r.mode { + case C.LogicalTypeAnd: + op = "&&" + case C.LogicalTypeOr: + op = "||" + } + if !r.invert { + return strings.Join(F.MapToString(r.rules), " "+op+" ") + } else { + return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")" + } +} + +func matchAnyItem(items []RuleItem, metadata *adapter.InboundContext) bool { + return common.Any(items, func(it RuleItem) bool { + return it.Match(metadata) + }) +} + +func (s ruleMatchState) has(target ruleMatchState) bool { + return s&target != 0 +} diff --git a/route/rule/rule_abstract_test.go b/route/rule/rule_abstract_test.go new file mode 100644 index 00000000..ace3dec6 --- /dev/null +++ b/route/rule/rule_abstract_test.go @@ -0,0 +1,157 @@ +package rule + +import ( + "context" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/x/list" + + "github.com/stretchr/testify/require" + "go4.org/netipx" +) + +type fakeRuleSet struct { + matched bool +} + +func (f *fakeRuleSet) Name() string { + return "fake-rule-set" +} + +func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { + return nil +} + +func (f *fakeRuleSet) PostStart() error { + return nil +} + +func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata { + return adapter.RuleSetMetadata{} +} + +func (f *fakeRuleSet) ExtractIPSet() []*netipx.IPSet { + return nil +} + +func (f *fakeRuleSet) IncRef() {} + +func (f *fakeRuleSet) DecRef() {} + +func (f *fakeRuleSet) Cleanup() {} + +func (f *fakeRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + return nil +} + +func (f *fakeRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {} + +func (f *fakeRuleSet) Close() error { + return nil +} + +func (f *fakeRuleSet) Match(*adapter.InboundContext) bool { + return f.matched +} + +func (f *fakeRuleSet) String() string { + return "fake-rule-set" +} + +type fakeRuleItem struct { + matched bool +} + +func (f *fakeRuleItem) Match(*adapter.InboundContext) bool { + return f.matched +} + +func (f *fakeRuleItem) String() string { + return "fake-rule-item" +} + +func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule { + ruleSetItem := &RuleSetItem{ + setList: []adapter.RuleSet{&fakeRuleSet{matched: ruleSetMatched}}, + } + return &DefaultRule{ + abstractDefaultRule: abstractDefaultRule{ + ruleSetItem: ruleSetItem, + allItems: []RuleItem{ruleSetItem}, + invert: invert, + }, + } +} + +func newSingleItemRule(matched bool) *DefaultRule { + item := &fakeRuleItem{matched: matched} + return &DefaultRule{ + abstractDefaultRule: abstractDefaultRule{ + items: []RuleItem{item}, + allItems: []RuleItem{item}, + }, + } +} + +func TestAbstractDefaultRule_RuleSetOnly_InvertFalse(t *testing.T) { + t.Parallel() + require.True(t, newRuleSetOnlyRule(true, false).Match(&adapter.InboundContext{})) + require.False(t, newRuleSetOnlyRule(false, false).Match(&adapter.InboundContext{})) +} + +func TestAbstractDefaultRule_RuleSetOnly_InvertTrue(t *testing.T) { + t.Parallel() + require.False(t, newRuleSetOnlyRule(true, true).Match(&adapter.InboundContext{})) + require.True(t, newRuleSetOnlyRule(false, true).Match(&adapter.InboundContext{})) +} + +func TestAbstractLogicalRule_And_WithRuleSetInvert(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + aMatched bool + ruleSetBMatch bool + expected bool + }{ + { + name: "A true B true", + aMatched: true, + ruleSetBMatch: true, + expected: false, + }, + { + name: "A true B false", + aMatched: true, + ruleSetBMatch: false, + expected: true, + }, + { + name: "A false B true", + aMatched: false, + ruleSetBMatch: true, + expected: false, + }, + { + name: "A false B false", + aMatched: false, + ruleSetBMatch: false, + expected: false, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + logicalRule := &abstractLogicalRule{ + mode: C.LogicalTypeAnd, + rules: []adapter.HeadlessRule{ + newSingleItemRule(testCase.aMatched), + newRuleSetOnlyRule(testCase.ruleSetBMatch, true), + }, + } + require.Equal(t, testCase.expected, logicalRule.Match(&adapter.InboundContext{})) + }) + } +} diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go new file mode 100644 index 00000000..ea239b68 --- /dev/null +++ b/route/rule/rule_action.go @@ -0,0 +1,610 @@ +package rule + +import ( + "context" + "errors" + "net/netip" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + 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/miekg/dns" +) + +func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { + switch action.Action { + case "": + return nil, nil + case C.RuleActionTypeRoute: + return &RuleActionRoute{ + Outbound: action.RouteOptions.Outbound, + RuleActionRouteOptions: RuleActionRouteOptions{ + OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0), + OverridePort: action.RouteOptions.OverridePort, + NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy), + FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), + UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, + UDPConnect: action.RouteOptions.UDPConnect, + TLSFragment: action.RouteOptions.TLSFragment, + TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay), + TLSRecordFragment: action.RouteOptions.TLSRecordFragment, + }, + }, nil + case C.RuleActionTypeRouteOptions: + return &RuleActionRouteOptions{ + OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0), + OverridePort: action.RouteOptionsOptions.OverridePort, + NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptionsOptions.NetworkStrategy), + FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), + UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, + UDPConnect: action.RouteOptionsOptions.UDPConnect, + UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout), + TLSFragment: action.RouteOptionsOptions.TLSFragment, + TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay), + TLSRecordFragment: action.RouteOptionsOptions.TLSRecordFragment, + }, nil + case C.RuleActionTypeBypass: + return &RuleActionBypass{ + Outbound: action.BypassOptions.Outbound, + RuleActionRouteOptions: RuleActionRouteOptions{ + OverrideAddress: M.ParseSocksaddrHostPort(action.BypassOptions.OverrideAddress, 0), + OverridePort: action.BypassOptions.OverridePort, + NetworkStrategy: (*C.NetworkStrategy)(action.BypassOptions.NetworkStrategy), + FallbackDelay: time.Duration(action.BypassOptions.FallbackDelay), + UDPDisableDomainUnmapping: action.BypassOptions.UDPDisableDomainUnmapping, + UDPConnect: action.BypassOptions.UDPConnect, + TLSFragment: action.BypassOptions.TLSFragment, + TLSFragmentFallbackDelay: time.Duration(action.BypassOptions.TLSFragmentFallbackDelay), + TLSRecordFragment: action.BypassOptions.TLSRecordFragment, + }, + }, nil + case C.RuleActionTypeDirect: + directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false) + if err != nil { + return nil, err + } + var description string + descriptions := action.DirectOptions.Descriptions() + switch len(descriptions) { + case 0: + case 1: + description = F.ToString("(", descriptions[0], ")") + case 2: + description = F.ToString("(", descriptions[0], ",", descriptions[1], ")") + default: + description = F.ToString("(", descriptions[0], ",", descriptions[1], ",...)") + } + return &RuleActionDirect{ + Dialer: directDialer, + description: description, + }, nil + case C.RuleActionTypeReject: + return &RuleActionReject{ + Method: action.RejectOptions.Method, + NoDrop: action.RejectOptions.NoDrop, + logger: logger, + }, nil + case C.RuleActionTypeHijackDNS: + return &RuleActionHijackDNS{}, nil + case C.RuleActionTypeSniff: + sniffAction := &RuleActionSniff{ + SnifferNames: action.SniffOptions.Sniffer, + Timeout: time.Duration(action.SniffOptions.Timeout), + } + return sniffAction, sniffAction.build() + case C.RuleActionTypeResolve: + return &RuleActionResolve{ + Server: action.ResolveOptions.Server, + Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), + DisableCache: action.ResolveOptions.DisableCache, + DisableOptimisticCache: action.ResolveOptions.DisableOptimisticCache, + RewriteTTL: action.ResolveOptions.RewriteTTL, + ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), + }, nil + default: + panic(F.ToString("unknown rule action: ", action.Action)) + } +} + +func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) adapter.RuleAction { + switch action.Action { + case "": + return nil + case C.RuleActionTypeRoute: + return &RuleActionDNSRoute{ + Server: action.RouteOptions.Server, + RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + }, + } + case C.RuleActionTypeEvaluate: + return &RuleActionEvaluate{ + Server: action.RouteOptions.Server, + RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + }, + } + case C.RuleActionTypeRespond: + return &RuleActionRespond{} + case C.RuleActionTypeRouteOptions: + return &RuleActionDNSRouteOptions{ + Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), + DisableCache: action.RouteOptionsOptions.DisableCache, + DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptionsOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), + } + case C.RuleActionTypeReject: + return &RuleActionReject{ + Method: action.RejectOptions.Method, + NoDrop: action.RejectOptions.NoDrop, + logger: logger, + } + case C.RuleActionTypePredefined: + return &RuleActionPredefined{ + Rcode: action.PredefinedOptions.Rcode.Build(), + Answer: common.Map(action.PredefinedOptions.Answer, option.DNSRecordOptions.Build), + Ns: common.Map(action.PredefinedOptions.Ns, option.DNSRecordOptions.Build), + Extra: common.Map(action.PredefinedOptions.Extra, option.DNSRecordOptions.Build), + } + default: + panic(F.ToString("unknown rule action: ", action.Action)) + } +} + +type RuleActionRoute struct { + Outbound string + RuleActionRouteOptions +} + +func (r *RuleActionRoute) Type() string { + return C.RuleActionTypeRoute +} + +func (r *RuleActionRoute) String() string { + var descriptions []string + descriptions = append(descriptions, r.Outbound) + descriptions = append(descriptions, r.Descriptions()...) + return F.ToString("route(", strings.Join(descriptions, ","), ")") +} + +type RuleActionBypass struct { + Outbound string + RuleActionRouteOptions +} + +func (r *RuleActionBypass) Type() string { + return C.RuleActionTypeBypass +} + +func (r *RuleActionBypass) String() string { + if r.Outbound == "" { + return "bypass()" + } + var descriptions []string + descriptions = append(descriptions, r.Outbound) + descriptions = append(descriptions, r.Descriptions()...) + return F.ToString("bypass(", strings.Join(descriptions, ","), ")") +} + +type RuleActionRouteOptions struct { + OverrideAddress M.Socksaddr + OverridePort uint16 + NetworkStrategy *C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType + FallbackDelay time.Duration + UDPDisableDomainUnmapping bool + UDPConnect bool + UDPTimeout time.Duration + TLSFragment bool + TLSFragmentFallbackDelay time.Duration + TLSRecordFragment bool +} + +func (r *RuleActionRouteOptions) Type() string { + return C.RuleActionTypeRouteOptions +} + +func (r *RuleActionRouteOptions) String() string { + return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")") +} + +func (r *RuleActionRouteOptions) Descriptions() []string { + var descriptions []string + if r.OverrideAddress.IsValid() { + descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString())) + } + if r.OverridePort > 0 { + descriptions = append(descriptions, F.ToString("override-port=", r.OverridePort)) + } + if r.NetworkStrategy != nil { + descriptions = append(descriptions, F.ToString("network-strategy=", r.NetworkStrategy)) + } + if r.NetworkType != nil { + descriptions = append(descriptions, F.ToString("network-type=", strings.Join(common.Map(r.NetworkType, C.InterfaceType.String), ","))) + } + if r.FallbackNetworkType != nil { + descriptions = append(descriptions, F.ToString("fallback-network-type=", strings.Join(common.Map(r.FallbackNetworkType, C.InterfaceType.String), ","))) + } + if r.FallbackDelay > 0 { + descriptions = append(descriptions, F.ToString("fallback-delay=", r.FallbackDelay.String())) + } + if r.UDPDisableDomainUnmapping { + descriptions = append(descriptions, "udp-disable-domain-unmapping") + } + if r.UDPConnect { + descriptions = append(descriptions, "udp-connect") + } + if r.UDPTimeout > 0 { + descriptions = append(descriptions, "udp-timeout") + } + if r.TLSFragment { + descriptions = append(descriptions, "tls-fragment") + } + if r.TLSFragmentFallbackDelay > 0 { + descriptions = append(descriptions, F.ToString("tls-fragment-fallback-delay=", r.TLSFragmentFallbackDelay.String())) + } + if r.TLSRecordFragment { + descriptions = append(descriptions, "tls-record-fragment") + } + return descriptions +} + +type RuleActionDNSRoute struct { + Server string + RuleActionDNSRouteOptions +} + +func (r *RuleActionDNSRoute) Type() string { + return C.RuleActionTypeRoute +} + +func (r *RuleActionDNSRoute) String() string { + return formatDNSRouteAction("route", r.Server, r.RuleActionDNSRouteOptions) +} + +type RuleActionEvaluate struct { + Server string + RuleActionDNSRouteOptions +} + +func (r *RuleActionEvaluate) Type() string { + return C.RuleActionTypeEvaluate +} + +func (r *RuleActionEvaluate) String() string { + return formatDNSRouteAction("evaluate", r.Server, r.RuleActionDNSRouteOptions) +} + +type RuleActionRespond struct{} + +func (r *RuleActionRespond) Type() string { + return C.RuleActionTypeRespond +} + +func (r *RuleActionRespond) String() string { + return "respond" +} + +func formatDNSRouteAction(action string, server string, options RuleActionDNSRouteOptions) string { + var descriptions []string + descriptions = append(descriptions, server) + if options.DisableCache { + descriptions = append(descriptions, "disable-cache") + } + if options.DisableOptimisticCache { + descriptions = append(descriptions, "disable-optimistic-cache") + } + if options.RewriteTTL != nil { + descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL)) + } + if options.ClientSubnet.IsValid() { + descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet)) + } + return F.ToString(action, "(", strings.Join(descriptions, ","), ")") +} + +type RuleActionDNSRouteOptions struct { + Strategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix +} + +func (r *RuleActionDNSRouteOptions) Type() string { + return C.RuleActionTypeRouteOptions +} + +func (r *RuleActionDNSRouteOptions) String() string { + var descriptions []string + if r.DisableCache { + descriptions = append(descriptions, "disable-cache") + } + if r.DisableOptimisticCache { + descriptions = append(descriptions, "disable-optimistic-cache") + } + if r.RewriteTTL != nil { + descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) + } + if r.ClientSubnet.IsValid() { + descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) + } + return F.ToString("route-options(", strings.Join(descriptions, ","), ")") +} + +type RuleActionDirect struct { + Dialer N.Dialer + description string +} + +func (r *RuleActionDirect) Type() string { + return C.RuleActionTypeDirect +} + +func (r *RuleActionDirect) String() string { + return "direct" + r.description +} + +type RejectedError struct { + Cause error +} + +func (r *RejectedError) Error() string { + return "rejected" +} + +func (r *RejectedError) Unwrap() error { + return r.Cause +} + +func IsRejected(err error) bool { + var rejected *RejectedError + return errors.As(err, &rejected) +} + +type BypassedError struct { + Cause error +} + +func (b *BypassedError) Error() string { + return "bypassed" +} + +func (b *BypassedError) Unwrap() error { + return b.Cause +} + +func IsBypassed(err error) bool { + var bypassed *BypassedError + return errors.As(err, &bypassed) +} + +type RuleActionReject struct { + Method string + NoDrop bool + logger logger.ContextLogger + dropAccess sync.Mutex + dropCounter []time.Time +} + +func (r *RuleActionReject) Type() string { + return C.RuleActionTypeReject +} + +func (r *RuleActionReject) String() string { + if r.Method == C.RuleActionRejectMethodDefault { + return "reject" + } + return F.ToString("reject(", r.Method, ")") +} + +func (r *RuleActionReject) Error(ctx context.Context) error { + var returnErr error + switch r.Method { + case C.RuleActionRejectMethodDefault: + returnErr = &RejectedError{tun.ErrReset} + case C.RuleActionRejectMethodDrop: + return &RejectedError{tun.ErrDrop} + case C.RuleActionRejectMethodReply: + return nil + default: + panic(F.ToString("unknown reject method: ", r.Method)) + } + if r.NoDrop { + return returnErr + } + r.dropAccess.Lock() + defer r.dropAccess.Unlock() + timeNow := time.Now() + r.dropCounter = common.Filter(r.dropCounter, func(t time.Time) bool { + return timeNow.Sub(t) <= 30*time.Second + }) + r.dropCounter = append(r.dropCounter, timeNow) + if len(r.dropCounter) > 50 { + if ctx != nil { + r.logger.DebugContext(ctx, "dropped due to flooding") + } + return &RejectedError{tun.ErrDrop} + } + return returnErr +} + +type RuleActionHijackDNS struct{} + +func (r *RuleActionHijackDNS) Type() string { + return C.RuleActionTypeHijackDNS +} + +func (r *RuleActionHijackDNS) String() string { + return "hijack-dns" +} + +type RuleActionSniff struct { + SnifferNames []string + StreamSniffers []sniff.StreamSniffer + PacketSniffers []sniff.PacketSniffer + Timeout time.Duration + // Deprecated + OverrideDestination bool +} + +func (r *RuleActionSniff) Type() string { + return C.RuleActionTypeSniff +} + +func (r *RuleActionSniff) build() error { + for _, name := range r.SnifferNames { + switch name { + case C.ProtocolTLS: + r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello) + case C.ProtocolHTTP: + r.StreamSniffers = append(r.StreamSniffers, sniff.HTTPHost) + case C.ProtocolQUIC: + r.PacketSniffers = append(r.PacketSniffers, sniff.QUICClientHello) + case C.ProtocolDNS: + r.StreamSniffers = append(r.StreamSniffers, sniff.StreamDomainNameQuery) + r.PacketSniffers = append(r.PacketSniffers, sniff.DomainNameQuery) + case C.ProtocolSTUN: + r.PacketSniffers = append(r.PacketSniffers, sniff.STUNMessage) + case C.ProtocolBitTorrent: + r.StreamSniffers = append(r.StreamSniffers, sniff.BitTorrent) + r.PacketSniffers = append(r.PacketSniffers, sniff.UTP) + r.PacketSniffers = append(r.PacketSniffers, sniff.UDPTracker) + case C.ProtocolDTLS: + r.PacketSniffers = append(r.PacketSniffers, sniff.DTLSRecord) + case C.ProtocolSSH: + r.StreamSniffers = append(r.StreamSniffers, sniff.SSH) + case C.ProtocolRDP: + r.StreamSniffers = append(r.StreamSniffers, sniff.RDP) + case C.ProtocolNTP: + r.PacketSniffers = append(r.PacketSniffers, sniff.NTP) + default: + return E.New("unknown sniffer: ", name) + } + } + return nil +} + +func (r *RuleActionSniff) String() string { + if len(r.SnifferNames) == 0 && r.Timeout == 0 { + return "sniff" + } else if len(r.SnifferNames) > 0 && r.Timeout == 0 { + return F.ToString("sniff(", strings.Join(r.SnifferNames, ","), ")") + } else if len(r.SnifferNames) == 0 && r.Timeout > 0 { + return F.ToString("sniff(", r.Timeout.String(), ")") + } else { + return F.ToString("sniff(", strings.Join(r.SnifferNames, ","), ",", r.Timeout.String(), ")") + } +} + +type RuleActionResolve struct { + Server string + Strategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix +} + +func (r *RuleActionResolve) Type() string { + return C.RuleActionTypeResolve +} + +func (r *RuleActionResolve) String() string { + var options []string + if r.Server != "" { + options = append(options, r.Server) + } + if r.Strategy != C.DomainStrategyAsIS { + options = append(options, F.ToString(option.DomainStrategy(r.Strategy))) + } + if r.DisableCache { + options = append(options, "disable_cache") + } + if r.DisableOptimisticCache { + options = append(options, "disable_optimistic_cache") + } + if r.RewriteTTL != nil { + options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL)) + } + if r.ClientSubnet.IsValid() { + options = append(options, F.ToString("client_subnet=", r.ClientSubnet)) + } + if len(options) == 0 { + return "resolve" + } else { + return F.ToString("resolve(", strings.Join(options, ","), ")") + } +} + +type RuleActionPredefined struct { + Rcode int + Answer []dns.RR + Ns []dns.RR + Extra []dns.RR +} + +func (r *RuleActionPredefined) Type() string { + return C.RuleActionTypePredefined +} + +func (r *RuleActionPredefined) String() string { + var options []string + options = append(options, dns.RcodeToString[r.Rcode]) + options = append(options, common.Map(r.Answer, dns.RR.String)...) + options = append(options, common.Map(r.Ns, dns.RR.String)...) + options = append(options, common.Map(r.Extra, dns.RR.String)...) + return F.ToString("predefined(", strings.Join(options, ","), ")") +} + +func (r *RuleActionPredefined) Response(request *dns.Msg) *dns.Msg { + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: request.Id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: r.Rcode, + }, + Question: request.Question, + Answer: rewriteRecords(r.Answer, request.Question[0]), + Ns: rewriteRecords(r.Ns, request.Question[0]), + Extra: rewriteRecords(r.Extra, request.Question[0]), + } +} + +func rewriteRecords(records []dns.RR, question dns.Question) []dns.RR { + return common.Map(records, func(it dns.RR) dns.RR { + if strings.HasPrefix(it.Header().Name, "*") { + if strings.HasSuffix(question.Name, it.Header().Name[1:]) { + it = dns.Copy(it) + it.Header().Name = question.Name + } + } + return it + }) +} diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go new file mode 100644 index 00000000..774e1b7c --- /dev/null +++ b/route/rule/rule_default.go @@ -0,0 +1,348 @@ +package rule + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" +) + +func NewRule(ctx context.Context, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + switch options.DefaultOptions.Action { + case "", C.RuleActionTypeRoute: + if options.DefaultOptions.RouteOptions.Outbound == "" && checkOutbound { + return nil, E.New("missing outbound field") + } + } + return NewDefaultRule(ctx, logger, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + switch options.LogicalOptions.Action { + case "", C.RuleActionTypeRoute: + if options.LogicalOptions.RouteOptions.Outbound == "" && checkOutbound { + return nil, E.New("missing outbound field") + } + } + return NewLogicalRule(ctx, logger, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.Rule = (*DefaultRule)(nil) + +type DefaultRule struct { + abstractDefaultRule +} + +func (r *DefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractDefaultRule.matchStates(metadata) +} + +type RuleItem interface { + Match(metadata *adapter.InboundContext) bool + String() string +} + +func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { + action, err := NewRuleAction(ctx, logger, options.RuleAction) + if err != nil { + return nil, E.Cause(err, "action") + } + rule := &DefaultRule{ + abstractDefaultRule{ + invert: options.Invert, + action: action, + }, + } + router := service.FromContext[adapter.Router](ctx) + networkManager := service.FromContext[adapter.NetworkManager](ctx) + if len(options.Inbound) > 0 { + item := NewInboundRule(options.Inbound) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.IPVersion > 0 { + switch options.IPVersion { + case 4, 6: + item := NewIPVersionItem(options.IPVersion == 6) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + default: + return nil, E.New("invalid ip version: ", options.IPVersion) + } + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.AuthUser) > 0 { + item := NewAuthUserItem(options.AuthUser) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Protocol) > 0 { + item := NewProtocolItem(options.Protocol) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Client) > 0 { + item := NewClientItem(options.Client) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item, err := NewDomainItem(options.Domain, options.DomainSuffix) + if err != nil { + return nil, err + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, err + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Geosite) > 0 { + return nil, E.New("geosite database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") + } + if len(options.SourceGeoIP) > 0 { + return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") + } + if len(options.GeoIP) > 0 { + return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ip_cidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageNameRegex) > 0 { + item, err := NewPackageNameRegexItem(options.PackageNameRegex) + if err != nil { + return nil, E.Cause(err, "package_name_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.User) > 0 { + item := NewUserItem(options.User) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.UserID) > 0 { + item := NewUserIDItem(options.UserID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.ClashMode != "" { + item := NewClashModeItem(ctx, options.ClashMode) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(networkManager, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 { + item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { + item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DefaultInterfaceAddress) > 0 { + item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceMACAddress) > 0 { + item := NewSourceMACAddressItem(options.SourceMACAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceHostname) > 0 { + item := NewSourceHostnameItem(options.SourceHostname) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PreferredBy) > 0 { + item := NewPreferredByItem(ctx, options.PreferredBy) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.RuleSet) > 0 { + //nolint:staticcheck + if options.Deprecated_RulesetIPCIDRMatchSource { + return nil, E.New("rule_set_ipcidr_match_source is deprecated in sing-box 1.10.0 and removed in sing-box 1.11.0") + } + var matchSource bool + if options.RuleSetIPCIDRMatchSource { + matchSource = true + } + item := NewRuleSetItem(router, options.RuleSet, matchSource, false) + rule.ruleSetItem = item + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.Rule = (*LogicalRule)(nil) + +type LogicalRule struct { + abstractLogicalRule +} + +func (r *LogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractLogicalRule.matchStates(metadata) +} + +func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { + action, err := NewRuleAction(ctx, logger, options.RuleAction) + if err != nil { + return nil, E.Cause(err, "action") + } + rule := &LogicalRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + action: action, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + rule.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + rule.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subOptions := range options.Rules { + err = validateNoNestedRuleActions(subOptions, true) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + subRule, err := NewRule(ctx, logger, subOptions, false) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + rule.rules[i] = subRule + } + return rule, nil +} diff --git a/route/rule/rule_default_interface_address.go b/route/rule/rule_default_interface_address.go new file mode 100644 index 00000000..2d7fdebe --- /dev/null +++ b/route/rule/rule_default_interface_address.go @@ -0,0 +1,56 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +var _ RuleItem = (*DefaultInterfaceAddressItem)(nil) + +type DefaultInterfaceAddressItem struct { + interfaceMonitor tun.DefaultInterfaceMonitor + interfaceAddresses []netip.Prefix +} + +func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses badoption.Listable[*badoption.Prefixable]) *DefaultInterfaceAddressItem { + item := &DefaultInterfaceAddressItem{ + interfaceMonitor: networkManager.InterfaceMonitor(), + interfaceAddresses: make([]netip.Prefix, 0, len(interfaceAddresses)), + } + for _, prefixable := range interfaceAddresses { + item.interfaceAddresses = append(item.interfaceAddresses, prefixable.Build(netip.Prefix{})) + } + return item +} + +func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + defaultInterface := r.interfaceMonitor.DefaultInterface() + if defaultInterface == nil { + return false + } + for _, address := range r.interfaceAddresses { + if common.All(defaultInterface.Addresses, func(it netip.Prefix) bool { + return !address.Overlaps(it) + }) { + return false + } + } + return true +} + +func (r *DefaultInterfaceAddressItem) String() string { + addressLen := len(r.interfaceAddresses) + switch { + case addressLen == 1: + return "default_interface_address=" + r.interfaceAddresses[0].String() + case addressLen > 3: + return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses[:3], netip.Prefix.String), " ") + "...]" + default: + return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses, netip.Prefix.String), " ") + "]" + } +} diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go new file mode 100644 index 00000000..646f987e --- /dev/null +++ b/route/rule/rule_dns.go @@ -0,0 +1,525 @@ +package rule + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" + + "github.com/miekg/dns" +) + +func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool, legacyDNSMode bool) (adapter.DNSRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + if !checkServer && options.DefaultOptions.Action == C.RuleActionTypeEvaluate { + return nil, E.New(options.DefaultOptions.Action, " is only allowed on top-level DNS rules") + } + err := validateDNSRuleAction(options.DefaultOptions.DNSRuleAction) + if err != nil { + return nil, err + } + switch options.DefaultOptions.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + if options.DefaultOptions.RouteOptions.Server == "" && checkServer { + return nil, E.New("missing server field") + } + } + return NewDefaultDNSRule(ctx, logger, options.DefaultOptions, legacyDNSMode) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + if !checkServer && options.LogicalOptions.Action == C.RuleActionTypeEvaluate { + return nil, E.New(options.LogicalOptions.Action, " is only allowed on top-level DNS rules") + } + err := validateDNSRuleAction(options.LogicalOptions.DNSRuleAction) + if err != nil { + return nil, err + } + switch options.LogicalOptions.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + if options.LogicalOptions.RouteOptions.Server == "" && checkServer { + return nil, E.New("missing server field") + } + } + return NewLogicalDNSRule(ctx, logger, options.LogicalOptions, legacyDNSMode) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +func validateDNSRuleAction(action option.DNSRuleAction) error { + if action.Action == C.RuleActionTypeReject && action.RejectOptions.Method == C.RuleActionRejectMethodReply { + return E.New("reject method `reply` is not supported for DNS rules") + } + return nil +} + +var _ adapter.DNSRule = (*DefaultDNSRule)(nil) + +type DefaultDNSRule struct { + abstractDefaultRule + matchResponse bool +} + +func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractDefaultRule.matchStates(metadata) +} + +func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule, legacyDNSMode bool) (*DefaultDNSRule, error) { + rule := &DefaultDNSRule{ + abstractDefaultRule: abstractDefaultRule{ + invert: options.Invert, + action: NewDNSRuleAction(logger, options.DNSRuleAction), + }, + matchResponse: options.MatchResponse, + } + if len(options.Inbound) > 0 { + item := NewInboundRule(options.Inbound) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + router := service.FromContext[adapter.Router](ctx) + networkManager := service.FromContext[adapter.NetworkManager](ctx) + if options.IPVersion > 0 { + switch options.IPVersion { + case 4, 6: + item := NewIPVersionItem(options.IPVersion == 6) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + default: + return nil, E.New("invalid ip version: ", options.IPVersion) + } + } + if len(options.QueryType) > 0 { + item := NewQueryTypeItem(options.QueryType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.AuthUser) > 0 { + item := NewAuthUserItem(options.AuthUser) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Protocol) > 0 { + item := NewProtocolItem(options.Protocol) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item, err := NewDomainItem(options.Domain, options.DomainSuffix) + if err != nil { + return nil, err + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Geosite) > 0 { //nolint:staticcheck + return nil, E.New("geosite database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") + } + if len(options.SourceGeoIP) > 0 { + return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") + } + if len(options.GeoIP) > 0 { + return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ip_cidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ip_cidr") + } + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } + if options.IPAcceptAny { + item := NewIPAcceptAnyItem() + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } + if options.ResponseRcode != nil { + item := NewDNSResponseRCodeItem(int(*options.ResponseRcode)) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ResponseAnswer) > 0 { + item := NewDNSResponseRecordItem("response_answer", options.ResponseAnswer, dnsResponseAnswers) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ResponseNs) > 0 { + item := NewDNSResponseRecordItem("response_ns", options.ResponseNs, dnsResponseNS) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ResponseExtra) > 0 { + item := NewDNSResponseRecordItem("response_extra", options.ResponseExtra, dnsResponseExtra) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageNameRegex) > 0 { + item, err := NewPackageNameRegexItem(options.PackageNameRegex) + if err != nil { + return nil, E.Cause(err, "package_name_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.User) > 0 { + item := NewUserItem(options.User) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.UserID) > 0 { + item := NewUserIDItem(options.UserID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Outbound) > 0 { + item := NewOutboundRule(ctx, options.Outbound) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.ClashMode != "" { + item := NewClashModeItem(ctx, options.ClashMode) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(networkManager, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 { + item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { + item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DefaultInterfaceAddress) > 0 { + item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceMACAddress) > 0 { + item := NewSourceMACAddressItem(options.SourceMACAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceHostname) > 0 { + item := NewSourceHostnameItem(options.SourceHostname) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + if legacyDNSMode { + deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty) + } else { + return nil, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) + } + } + if len(options.RuleSet) > 0 { + //nolint:staticcheck + if options.Deprecated_RulesetIPCIDRMatchSource { + return nil, E.New("rule_set_ipcidr_match_source is deprecated in sing-box 1.10.0 and removed in sing-box 1.11.0") + } + var matchSource bool + if options.RuleSetIPCIDRMatchSource { + matchSource = true + } + item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck + rule.ruleSetItem = item + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +func (r *DefaultDNSRule) Action() adapter.RuleAction { + return r.action +} + +func (r *DefaultDNSRule) WithAddressLimit() bool { + if len(r.destinationIPCIDRItems) > 0 { + return true + } + if r.ruleSetItem != nil { + ruleSet, isRuleSet := r.ruleSetItem.(*RuleSetItem) + if isRuleSet && ruleSet.ContainsDestinationIPCIDRRule() { + return true + } + } + return false +} + +func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool { + return !r.matchStatesForMatch(metadata).isEmpty() +} + +func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { + if r.matchResponse { + return false + } + metadata.IgnoreDestinationIPCIDRMatch = true + defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() + return !r.abstractDefaultRule.matchStates(metadata).isEmpty() +} + +func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet { + if r.matchResponse { + if metadata.DNSResponse == nil { + return r.abstractDefaultRule.invertedFailure(0) + } + matchMetadata := *metadata + matchMetadata.DestinationAddressMatchFromResponse = true + return r.abstractDefaultRule.matchStates(&matchMetadata) + } + return r.abstractDefaultRule.matchStates(metadata) +} + +func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { + matchMetadata := *metadata + matchMetadata.DNSResponse = response + matchMetadata.DestinationAddressMatchFromResponse = true + return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty() +} + +var _ adapter.DNSRule = (*LogicalDNSRule)(nil) + +type LogicalDNSRule struct { + abstractLogicalRule +} + +func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractLogicalRule.matchStates(metadata) +} + +func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet { + switch typedRule := rule.(type) { + case *DefaultDNSRule: + return typedRule.matchStatesForMatch(metadata) + case *LogicalDNSRule: + return typedRule.matchStatesForMatch(metadata) + default: + return matchHeadlessRuleStates(typedRule, metadata) + } +} + +func (r *LogicalDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet { + var stateSet ruleMatchStateSet + if r.mode == C.LogicalTypeAnd { + stateSet = emptyRuleMatchState() + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + nestedStateSet := matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata) + if nestedStateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState() + } + return 0 + } + stateSet = stateSet.combine(nestedStateSet) + } + } else { + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + stateSet = stateSet.merge(matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata)) + } + if stateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState() + } + return 0 + } + } + if r.invert { + return 0 + } + return stateSet +} + +func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) { + r := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + action: NewDNSRuleAction(logger, options.DNSRuleAction), + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + err := validateNoNestedDNSRuleActions(subRule, true) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + rule, err := NewDNSRule(ctx, logger, subRule, false, legacyDNSMode) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} + +func (r *LogicalDNSRule) Action() adapter.RuleAction { + return r.action +} + +func (r *LogicalDNSRule) WithAddressLimit() bool { + for _, rawRule := range r.rules { + switch rule := rawRule.(type) { + case *DefaultDNSRule: + if rule.WithAddressLimit() { + return true + } + case *LogicalDNSRule: + if rule.WithAddressLimit() { + return true + } + } + } + return false +} + +func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool { + return !r.matchStatesForMatch(metadata).isEmpty() +} + +func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { + metadata.IgnoreDestinationIPCIDRMatch = true + defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() + return !r.abstractLogicalRule.matchStates(metadata).isEmpty() +} + +func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { + matchMetadata := *metadata + matchMetadata.DNSResponse = response + matchMetadata.DestinationAddressMatchFromResponse = true + return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty() +} diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go new file mode 100644 index 00000000..ab85e0d5 --- /dev/null +++ b/route/rule/rule_headless.go @@ -0,0 +1,246 @@ +package rule + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" +) + +func NewHeadlessRule(ctx context.Context, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(ctx, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(ctx, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func (r *DefaultHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractDefaultRule.matchStates(metadata) +} + +func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + networkManager := service.FromContext[adapter.NetworkManager](ctx) + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.QueryType) > 0 { + item := NewQueryTypeItem(options.QueryType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item, err := NewDomainItem(options.Domain, options.DomainSuffix) + if err != nil { + return nil, err + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ip_cidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageNameRegex) > 0 { + item, err := NewPackageNameRegexItem(options.PackageNameRegex) + if err != nil { + return nil, E.Cause(err, "package_name_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if networkManager != nil { + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(networkManager, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { + item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DefaultInterfaceAddress) > 0 { + item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + } + if len(options.AdGuardDomain) > 0 { + item := NewAdGuardDomainItem(options.AdGuardDomain) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.AdGuardDomainMatcher != nil { + item := NewRawAdGuardDomainItem(options.AdGuardDomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func (r *LogicalHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractLogicalRule.matchStates(metadata) +} + +func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(ctx, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule/rule_interface_address.go b/route/rule/rule_interface_address.go new file mode 100644 index 00000000..d4c75d38 --- /dev/null +++ b/route/rule/rule_interface_address.go @@ -0,0 +1,62 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +var _ RuleItem = (*InterfaceAddressItem)(nil) + +type InterfaceAddressItem struct { + networkManager adapter.NetworkManager + interfaceAddresses map[string][]netip.Prefix + description string +} + +func NewInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]]) *InterfaceAddressItem { + item := &InterfaceAddressItem{ + networkManager: networkManager, + interfaceAddresses: make(map[string][]netip.Prefix, interfaceAddresses.Size()), + } + var entryDescriptions []string + for _, entry := range interfaceAddresses.Entries() { + prefixes := make([]netip.Prefix, 0, len(entry.Value)) + for _, prefixable := range entry.Value { + prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) + } + item.interfaceAddresses[entry.Key] = prefixes + entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) + } + item.description = "interface_address=[" + strings.Join(entryDescriptions, " ") + "]" + return item +} + +func (r *InterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + interfaces := r.networkManager.InterfaceFinder().Interfaces() + for ifName, addresses := range r.interfaceAddresses { + iface := common.Find(interfaces, func(it control.Interface) bool { + return it.Name == ifName + }) + if iface.Name == "" { + return false + } + if common.All(addresses, func(address netip.Prefix) bool { + return common.All(iface.Addresses, func(it netip.Prefix) bool { + return !address.Overlaps(it) + }) + }) { + return false + } + } + return true +} + +func (r *InterfaceAddressItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_adguard.go b/route/rule/rule_item_adguard.go new file mode 100644 index 00000000..84252e60 --- /dev/null +++ b/route/rule/rule_item_adguard.go @@ -0,0 +1,43 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/domain" +) + +var _ RuleItem = (*AdGuardDomainItem)(nil) + +type AdGuardDomainItem struct { + matcher *domain.AdGuardMatcher +} + +func NewAdGuardDomainItem(ruleLines []string) *AdGuardDomainItem { + return &AdGuardDomainItem{ + domain.NewAdGuardMatcher(ruleLines), + } +} + +func NewRawAdGuardDomainItem(matcher *domain.AdGuardMatcher) *AdGuardDomainItem { + return &AdGuardDomainItem{ + matcher, + } +} + +func (r *AdGuardDomainItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + return r.matcher.Match(strings.ToLower(domainHost)) +} + +func (r *AdGuardDomainItem) String() string { + return "!adguard_domain_rules=" +} diff --git a/route/rule/rule_item_auth_user.go b/route/rule/rule_item_auth_user.go new file mode 100644 index 00000000..5799e3c7 --- /dev/null +++ b/route/rule/rule_item_auth_user.go @@ -0,0 +1,37 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*AuthUserItem)(nil) + +type AuthUserItem struct { + users []string + userMap map[string]bool +} + +func NewAuthUserItem(users []string) *AuthUserItem { + userMap := make(map[string]bool) + for _, protocol := range users { + userMap[protocol] = true + } + return &AuthUserItem{ + users: users, + userMap: userMap, + } +} + +func (r *AuthUserItem) Match(metadata *adapter.InboundContext) bool { + return r.userMap[metadata.User] +} + +func (r *AuthUserItem) String() string { + if len(r.users) == 1 { + return F.ToString("auth_user=", r.users[0]) + } + return F.ToString("auth_user=[", strings.Join(r.users, " "), "]") +} diff --git a/route/rule/rule_item_cidr.go b/route/rule/rule_item_cidr.go new file mode 100644 index 00000000..28f74161 --- /dev/null +++ b/route/rule/rule_item_cidr.go @@ -0,0 +1,110 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + + "go4.org/netipx" +) + +var _ RuleItem = (*IPCIDRItem)(nil) + +type IPCIDRItem struct { + ipSet *netipx.IPSet + isSource bool + description string +} + +func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { + var builder netipx.IPSetBuilder + for i, prefixString := range prefixStrings { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return nil, E.Cause(err, "parse [", i, "]") + } + var description string + if isSource { + description = "source_ip_cidr=" + } else { + description = "ip_cidr=" + } + if dLen := len(prefixStrings); dLen == 1 { + description += prefixStrings[0] + } else if dLen > 3 { + description += "[" + strings.Join(prefixStrings[:3], " ") + "...]" + } else { + description += "[" + strings.Join(prefixStrings, " ") + "]" + } + ipSet, err := builder.IPSet() + if err != nil { + return nil, err + } + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + }, nil +} + +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ip_cidr=" + } else { + description = "ip_cidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + +func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { + if r.isSource || metadata.IPCIDRMatchSource { + return r.ipSet.Contains(metadata.Source.Addr) + } + if metadata.DestinationAddressMatchFromResponse { + addresses := metadata.DNSResponseAddressesForMatch() + if len(addresses) == 0 { + // Legacy rule_set_ip_cidr_accept_empty only applies when the DNS response + // does not expose any address answers for matching. + return metadata.IPCIDRAcceptEmpty + } + for _, address := range addresses { + if r.ipSet.Contains(address) { + return true + } + } + return false + } + if metadata.Destination.IsIP() { + return r.ipSet.Contains(metadata.Destination.Addr) + } + addresses := metadata.DestinationAddresses + if len(addresses) > 0 { + for _, address := range addresses { + if r.ipSet.Contains(address) { + return true + } + } + return false + } + return metadata.IPCIDRAcceptEmpty +} + +func (r *IPCIDRItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_clash_mode.go b/route/rule/rule_item_clash_mode.go new file mode 100644 index 00000000..fe2347a0 --- /dev/null +++ b/route/rule/rule_item_clash_mode.go @@ -0,0 +1,40 @@ +package rule + +import ( + "context" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" +) + +var _ RuleItem = (*ClashModeItem)(nil) + +type ClashModeItem struct { + ctx context.Context + clashServer adapter.ClashServer + mode string +} + +func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem { + return &ClashModeItem{ + ctx: ctx, + mode: mode, + } +} + +func (r *ClashModeItem) Start() error { + r.clashServer = service.FromContext[adapter.ClashServer](r.ctx) + return nil +} + +func (r *ClashModeItem) Match(metadata *adapter.InboundContext) bool { + if r.clashServer == nil { + return false + } + return strings.EqualFold(r.clashServer.Mode(), r.mode) +} + +func (r *ClashModeItem) String() string { + return "clash_mode=" + r.mode +} diff --git a/route/rule/rule_item_client.go b/route/rule/rule_item_client.go new file mode 100644 index 00000000..63ff4103 --- /dev/null +++ b/route/rule/rule_item_client.go @@ -0,0 +1,37 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ClientItem)(nil) + +type ClientItem struct { + clients []string + clientMap map[string]bool +} + +func NewClientItem(clients []string) *ClientItem { + clientMap := make(map[string]bool) + for _, client := range clients { + clientMap[client] = true + } + return &ClientItem{ + clients: clients, + clientMap: clientMap, + } +} + +func (r *ClientItem) Match(metadata *adapter.InboundContext) bool { + return r.clientMap[metadata.Client] +} + +func (r *ClientItem) String() string { + if len(r.clients) == 1 { + return F.ToString("client=", r.clients[0]) + } + return F.ToString("client=[", strings.Join(r.clients, " "), "]") +} diff --git a/route/rule/rule_item_domain.go b/route/rule/rule_item_domain.go new file mode 100644 index 00000000..af790aa3 --- /dev/null +++ b/route/rule/rule_item_domain.go @@ -0,0 +1,79 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ RuleItem = (*DomainItem)(nil) + +type DomainItem struct { + matcher *domain.Matcher + description string +} + +func NewDomainItem(domains []string, domainSuffixes []string) (*DomainItem, error) { + for _, domainItem := range domains { + if domainItem == "" { + return nil, E.New("domain: empty item is not allowed") + } + } + for _, domainSuffixItem := range domainSuffixes { + if domainSuffixItem == "" { + return nil, E.New("domain_suffix: empty item is not allowed") + } + } + var description string + if dLen := len(domains); dLen > 0 { + if dLen == 1 { + description = "domain=" + domains[0] + } else if dLen > 3 { + description = "domain=[" + strings.Join(domains[:3], " ") + "...]" + } else { + description = "domain=[" + strings.Join(domains, " ") + "]" + } + } + if dsLen := len(domainSuffixes); dsLen > 0 { + if len(description) > 0 { + description += " " + } + if dsLen == 1 { + description += "domain_suffix=" + domainSuffixes[0] + } else if dsLen > 3 { + description += "domain_suffix=[" + strings.Join(domainSuffixes[:3], " ") + "...]" + } else { + description += "domain_suffix=[" + strings.Join(domainSuffixes, " ") + "]" + } + } + return &DomainItem{ + domain.NewMatcher(domains, domainSuffixes, false), + description, + }, nil +} + +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + +func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + return r.matcher.Match(strings.ToLower(domainHost)) +} + +func (r *DomainItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_domain_keyword.go b/route/rule/rule_item_domain_keyword.go new file mode 100644 index 00000000..6e19a10c --- /dev/null +++ b/route/rule/rule_item_domain_keyword.go @@ -0,0 +1,47 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*DomainKeywordItem)(nil) + +type DomainKeywordItem struct { + keywords []string +} + +func NewDomainKeywordItem(keywords []string) *DomainKeywordItem { + return &DomainKeywordItem{keywords} +} + +func (r *DomainKeywordItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + domainHost = strings.ToLower(domainHost) + for _, keyword := range r.keywords { + if strings.Contains(domainHost, keyword) { + return true + } + } + return false +} + +func (r *DomainKeywordItem) String() string { + kLen := len(r.keywords) + if kLen == 1 { + return "domain_keyword=" + r.keywords[0] + } else if kLen > 3 { + return "domain_keyword=[" + strings.Join(r.keywords[:3], " ") + "...]" + } else { + return "domain_keyword=[" + strings.Join(r.keywords, " ") + "]" + } +} diff --git a/route/rule/rule_item_domain_regex.go b/route/rule/rule_item_domain_regex.go new file mode 100644 index 00000000..b9752a45 --- /dev/null +++ b/route/rule/rule_item_domain_regex.go @@ -0,0 +1,61 @@ +package rule + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*DomainRegexItem)(nil) + +type DomainRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewDomainRegexItem(expressions []string) (*DomainRegexItem, error) { + matchers := make([]*regexp.Regexp, 0, len(expressions)) + for i, regex := range expressions { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, E.Cause(err, "parse expression ", i) + } + matchers = append(matchers, matcher) + } + description := "domain_regex=" + eLen := len(expressions) + if eLen == 1 { + description += expressions[0] + } else if eLen > 3 { + description += F.ToString("[", strings.Join(expressions[:3], " "), "]") + } else { + description += F.ToString("[", strings.Join(expressions, " "), "]") + } + return &DomainRegexItem{matchers, description}, nil +} + +func (r *DomainRegexItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + domainHost = strings.ToLower(domainHost) + for _, matcher := range r.matchers { + if matcher.MatchString(domainHost) { + return true + } + } + return false +} + +func (r *DomainRegexItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_inbound.go b/route/rule/rule_item_inbound.go new file mode 100644 index 00000000..87e84740 --- /dev/null +++ b/route/rule/rule_item_inbound.go @@ -0,0 +1,35 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*InboundItem)(nil) + +type InboundItem struct { + inbounds []string + inboundMap map[string]bool +} + +func NewInboundRule(inbounds []string) *InboundItem { + rule := &InboundItem{inbounds, make(map[string]bool)} + for _, inbound := range inbounds { + rule.inboundMap[inbound] = true + } + return rule +} + +func (r *InboundItem) Match(metadata *adapter.InboundContext) bool { + return r.inboundMap[metadata.Inbound] +} + +func (r *InboundItem) String() string { + if len(r.inbounds) == 1 { + return F.ToString("inbound=", r.inbounds[0]) + } else { + return F.ToString("inbound=[", strings.Join(r.inbounds, " "), "]") + } +} diff --git a/route/rule/rule_item_ip_accept_any.go b/route/rule/rule_item_ip_accept_any.go new file mode 100644 index 00000000..fceebc18 --- /dev/null +++ b/route/rule/rule_item_ip_accept_any.go @@ -0,0 +1,24 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*IPAcceptAnyItem)(nil) + +type IPAcceptAnyItem struct{} + +func NewIPAcceptAnyItem() *IPAcceptAnyItem { + return &IPAcceptAnyItem{} +} + +func (r *IPAcceptAnyItem) Match(metadata *adapter.InboundContext) bool { + if metadata.DestinationAddressMatchFromResponse { + return len(metadata.DNSResponseAddressesForMatch()) > 0 + } + return len(metadata.DestinationAddresses) > 0 +} + +func (r *IPAcceptAnyItem) String() string { + return "ip_accept_any=true" +} diff --git a/route/rule/rule_item_ip_is_private.go b/route/rule/rule_item_ip_is_private.go new file mode 100644 index 00000000..c9688773 --- /dev/null +++ b/route/rule/rule_item_ip_is_private.go @@ -0,0 +1,47 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" +) + +var _ RuleItem = (*IPIsPrivateItem)(nil) + +type IPIsPrivateItem struct { + isSource bool +} + +func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { + return &IPIsPrivateItem{isSource} +} + +func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { + if r.isSource { + return !N.IsPublicAddr(metadata.Source.Addr) + } + if metadata.DestinationAddressMatchFromResponse { + for _, destinationAddress := range metadata.DNSResponseAddressesForMatch() { + if !N.IsPublicAddr(destinationAddress) { + return true + } + } + return false + } + if metadata.Destination.Addr.IsValid() { + return !N.IsPublicAddr(metadata.Destination.Addr) + } + for _, destinationAddress := range metadata.DestinationAddresses { + if !N.IsPublicAddr(destinationAddress) { + return true + } + } + return false +} + +func (r *IPIsPrivateItem) String() string { + if r.isSource { + return "source_ip_is_private=true" + } else { + return "ip_is_private=true" + } +} diff --git a/route/rule/rule_item_ipversion.go b/route/rule/rule_item_ipversion.go new file mode 100644 index 00000000..8ab64942 --- /dev/null +++ b/route/rule/rule_item_ipversion.go @@ -0,0 +1,30 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*IPVersionItem)(nil) + +type IPVersionItem struct { + isIPv6 bool +} + +func NewIPVersionItem(isIPv6 bool) *IPVersionItem { + return &IPVersionItem{isIPv6} +} + +func (r *IPVersionItem) Match(metadata *adapter.InboundContext) bool { + return metadata.IPVersion != 0 && metadata.IPVersion == 6 == r.isIPv6 || + metadata.Destination.IsIP() && metadata.Destination.IsIPv6() == r.isIPv6 +} + +func (r *IPVersionItem) String() string { + var versionStr string + if r.isIPv6 { + versionStr = "6" + } else { + versionStr = "4" + } + return "ip_version=" + versionStr +} diff --git a/route/rule/rule_item_network.go b/route/rule/rule_item_network.go new file mode 100644 index 00000000..bfb334d3 --- /dev/null +++ b/route/rule/rule_item_network.go @@ -0,0 +1,42 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*NetworkItem)(nil) + +type NetworkItem struct { + networks []string + networkMap map[string]bool +} + +func NewNetworkItem(networks []string) *NetworkItem { + networkMap := make(map[string]bool) + for _, network := range networks { + networkMap[network] = true + } + return &NetworkItem{ + networks: networks, + networkMap: networkMap, + } +} + +func (r *NetworkItem) Match(metadata *adapter.InboundContext) bool { + return r.networkMap[metadata.Network] +} + +func (r *NetworkItem) String() string { + description := "network=" + + pLen := len(r.networks) + if pLen == 1 { + description += F.ToString(r.networks[0]) + } else { + description += "[" + strings.Join(F.MapToString(r.networks), " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_network_is_constrained.go b/route/rule/rule_item_network_is_constrained.go new file mode 100644 index 00000000..e0368b75 --- /dev/null +++ b/route/rule/rule_item_network_is_constrained.go @@ -0,0 +1,29 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*NetworkIsConstrainedItem)(nil) + +type NetworkIsConstrainedItem struct { + networkManager adapter.NetworkManager +} + +func NewNetworkIsConstrainedItem(networkManager adapter.NetworkManager) *NetworkIsConstrainedItem { + return &NetworkIsConstrainedItem{ + networkManager: networkManager, + } +} + +func (r *NetworkIsConstrainedItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return networkInterface.Constrained +} + +func (r *NetworkIsConstrainedItem) String() string { + return "network_is_expensive=true" +} diff --git a/route/rule/rule_item_network_is_expensive.go b/route/rule/rule_item_network_is_expensive.go new file mode 100644 index 00000000..83e4f96f --- /dev/null +++ b/route/rule/rule_item_network_is_expensive.go @@ -0,0 +1,29 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*NetworkIsExpensiveItem)(nil) + +type NetworkIsExpensiveItem struct { + networkManager adapter.NetworkManager +} + +func NewNetworkIsExpensiveItem(networkManager adapter.NetworkManager) *NetworkIsExpensiveItem { + return &NetworkIsExpensiveItem{ + networkManager: networkManager, + } +} + +func (r *NetworkIsExpensiveItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return networkInterface.Expensive +} + +func (r *NetworkIsExpensiveItem) String() string { + return "network_is_expensive=true" +} diff --git a/route/rule/rule_item_network_type.go b/route/rule/rule_item_network_type.go new file mode 100644 index 00000000..31856e70 --- /dev/null +++ b/route/rule/rule_item_network_type.go @@ -0,0 +1,40 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*NetworkTypeItem)(nil) + +type NetworkTypeItem struct { + networkManager adapter.NetworkManager + networkType []C.InterfaceType +} + +func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []C.InterfaceType) *NetworkTypeItem { + return &NetworkTypeItem{ + networkManager: networkManager, + networkType: networkType, + } +} + +func (r *NetworkTypeItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return common.Contains(r.networkType, networkInterface.Type) +} + +func (r *NetworkTypeItem) String() string { + if len(r.networkType) == 1 { + return F.ToString("network_type=", r.networkType[0]) + } else { + return F.ToString("network_type=", "["+strings.Join(F.MapToString(r.networkType), " ")+"]") + } +} diff --git a/route/rule/rule_item_outbound.go b/route/rule/rule_item_outbound.go new file mode 100644 index 00000000..a13d0597 --- /dev/null +++ b/route/rule/rule_item_outbound.go @@ -0,0 +1,46 @@ +package rule + +import ( + "context" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental/deprecated" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*OutboundItem)(nil) + +type OutboundItem struct { + outbounds []string + outboundMap map[string]bool + matchAny bool +} + +func NewOutboundRule(ctx context.Context, outbounds []string) *OutboundItem { + deprecated.Report(ctx, deprecated.OptionOutboundDNSRuleItem) + rule := &OutboundItem{outbounds: outbounds, outboundMap: make(map[string]bool)} + for _, outbound := range outbounds { + if outbound == "any" { + rule.matchAny = true + } else { + rule.outboundMap[outbound] = true + } + } + return rule +} + +func (r *OutboundItem) Match(metadata *adapter.InboundContext) bool { + if r.matchAny { + return metadata.Outbound != "" + } + return r.outboundMap[metadata.Outbound] +} + +func (r *OutboundItem) String() string { + if len(r.outbounds) == 1 { + return F.ToString("outbound=", r.outbounds[0]) + } else { + return F.ToString("outbound=[", strings.Join(r.outbounds, " "), "]") + } +} diff --git a/route/rule/rule_item_package_name.go b/route/rule/rule_item_package_name.go new file mode 100644 index 00000000..514768de --- /dev/null +++ b/route/rule/rule_item_package_name.go @@ -0,0 +1,48 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*PackageNameItem)(nil) + +type PackageNameItem struct { + packageNames []string + packageMap map[string]bool +} + +func NewPackageNameItem(packageNameList []string) *PackageNameItem { + rule := &PackageNameItem{ + packageNames: packageNameList, + packageMap: make(map[string]bool), + } + for _, packageName := range packageNameList { + rule.packageMap[packageName] = true + } + return rule +} + +func (r *PackageNameItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 { + return false + } + for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { + if r.packageMap[packageName] { + return true + } + } + return false +} + +func (r *PackageNameItem) String() string { + var description string + pLen := len(r.packageNames) + if pLen == 1 { + description = "package_name=" + r.packageNames[0] + } else { + description = "package_name=[" + strings.Join(r.packageNames, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_package_name_regex.go b/route/rule/rule_item_package_name_regex.go new file mode 100644 index 00000000..9db4504a --- /dev/null +++ b/route/rule/rule_item_package_name_regex.go @@ -0,0 +1,56 @@ +package rule + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*PackageNameRegexItem)(nil) + +type PackageNameRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewPackageNameRegexItem(expressions []string) (*PackageNameRegexItem, error) { + matchers := make([]*regexp.Regexp, 0, len(expressions)) + for i, regex := range expressions { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, E.Cause(err, "parse expression ", i) + } + matchers = append(matchers, matcher) + } + description := "package_name_regex=" + eLen := len(expressions) + if eLen == 1 { + description += expressions[0] + } else if eLen > 3 { + description += F.ToString("[", strings.Join(expressions[:3], " "), "]") + } else { + description += F.ToString("[", strings.Join(expressions, " "), "]") + } + return &PackageNameRegexItem{matchers, description}, nil +} + +func (r *PackageNameRegexItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 { + return false + } + for _, matcher := range r.matchers { + for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { + if matcher.MatchString(packageName) { + return true + } + } + } + return false +} + +func (r *PackageNameRegexItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_port.go b/route/rule/rule_item_port.go new file mode 100644 index 00000000..af166ee6 --- /dev/null +++ b/route/rule/rule_item_port.go @@ -0,0 +1,52 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*PortItem)(nil) + +type PortItem struct { + ports []uint16 + portMap map[uint16]bool + isSource bool +} + +func NewPortItem(isSource bool, ports []uint16) *PortItem { + portMap := make(map[uint16]bool) + for _, port := range ports { + portMap[port] = true + } + return &PortItem{ + ports: ports, + portMap: portMap, + isSource: isSource, + } +} + +func (r *PortItem) Match(metadata *adapter.InboundContext) bool { + if r.isSource { + return r.portMap[metadata.Source.Port] + } else { + return r.portMap[metadata.Destination.Port] + } +} + +func (r *PortItem) String() string { + var description string + if r.isSource { + description = "source_port=" + } else { + description = "port=" + } + pLen := len(r.ports) + if pLen == 1 { + description += F.ToString(r.ports[0]) + } else { + description += "[" + strings.Join(F.MapToString(r.ports), " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_port_range.go b/route/rule/rule_item_port_range.go new file mode 100644 index 00000000..980f7d23 --- /dev/null +++ b/route/rule/rule_item_port_range.go @@ -0,0 +1,87 @@ +package rule + +import ( + "strconv" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" +) + +var ErrBadPortRange = E.New("bad port range") + +var _ RuleItem = (*PortRangeItem)(nil) + +type PortRangeItem struct { + isSource bool + portRanges []string + portRangeList []rangeItem +} + +type rangeItem struct { + start uint16 + end uint16 +} + +func NewPortRangeItem(isSource bool, rangeList []string) (*PortRangeItem, error) { + portRangeList := make([]rangeItem, 0, len(rangeList)) + for _, portRange := range rangeList { + if !strings.Contains(portRange, ":") { + return nil, E.Extend(ErrBadPortRange, portRange) + } + subIndex := strings.Index(portRange, ":") + var start, end uint64 + var err error + if subIndex > 0 { + start, err = strconv.ParseUint(portRange[:subIndex], 10, 16) + if err != nil { + return nil, E.Cause(err, E.Extend(ErrBadPortRange, portRange)) + } + } + if subIndex == len(portRange)-1 { + end = 0xFFFF + } else { + end, err = strconv.ParseUint(portRange[subIndex+1:], 10, 16) + if err != nil { + return nil, E.Cause(err, E.Extend(ErrBadPortRange, portRange)) + } + } + portRangeList = append(portRangeList, rangeItem{uint16(start), uint16(end)}) + } + return &PortRangeItem{ + isSource: isSource, + portRanges: rangeList, + portRangeList: portRangeList, + }, nil +} + +func (r *PortRangeItem) Match(metadata *adapter.InboundContext) bool { + var port uint16 + if r.isSource { + port = metadata.Source.Port + } else { + port = metadata.Destination.Port + } + for _, portRange := range r.portRangeList { + if port >= portRange.start && port <= portRange.end { + return true + } + } + return false +} + +func (r *PortRangeItem) String() string { + var description string + if r.isSource { + description = "source_port_range=" + } else { + description = "port_range=" + } + pLen := len(r.portRanges) + if pLen == 1 { + description += r.portRanges[0] + } else { + description += "[" + strings.Join(r.portRanges, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_preferred_by.go b/route/rule/rule_item_preferred_by.go new file mode 100644 index 00000000..42c8a627 --- /dev/null +++ b/route/rule/rule_item_preferred_by.go @@ -0,0 +1,86 @@ +package rule + +import ( + "context" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/service" +) + +var _ RuleItem = (*PreferredByItem)(nil) + +type PreferredByItem struct { + ctx context.Context + outboundTags []string + outbounds []adapter.OutboundWithPreferredRoutes +} + +func NewPreferredByItem(ctx context.Context, outboundTags []string) *PreferredByItem { + return &PreferredByItem{ + ctx: ctx, + outboundTags: outboundTags, + } +} + +func (r *PreferredByItem) Start() error { + outboundManager := service.FromContext[adapter.OutboundManager](r.ctx) + for _, outboundTag := range r.outboundTags { + rawOutbound, loaded := outboundManager.Outbound(outboundTag) + if !loaded { + return E.New("outbound not found: ", outboundTag) + } + outboundWithPreferredRoutes, withRoutes := rawOutbound.(adapter.OutboundWithPreferredRoutes) + if !withRoutes { + return E.New("outbound type does not support preferred routes: ", rawOutbound.Type()) + } + r.outbounds = append(r.outbounds, outboundWithPreferredRoutes) + } + return nil +} + +func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost != "" { + for _, outbound := range r.outbounds { + if outbound.PreferredDomain(domainHost) { + return true + } + } + } + if metadata.Destination.IsIP() { + for _, outbound := range r.outbounds { + if outbound.PreferredAddress(metadata.Destination.Addr) { + return true + } + } + } + if len(metadata.DestinationAddresses) > 0 { + for _, address := range metadata.DestinationAddresses { + for _, outbound := range r.outbounds { + if outbound.PreferredAddress(address) { + return true + } + } + } + } + return false +} + +func (r *PreferredByItem) String() string { + description := "preferred_by=" + pLen := len(r.outboundTags) + if pLen == 1 { + description += F.ToString(r.outboundTags[0]) + } else { + description += "[" + strings.Join(F.MapToString(r.outboundTags), " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_process_name.go b/route/rule/rule_item_process_name.go new file mode 100644 index 00000000..fa0f7165 --- /dev/null +++ b/route/rule/rule_item_process_name.go @@ -0,0 +1,44 @@ +package rule + +import ( + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*ProcessItem)(nil) + +type ProcessItem struct { + processes []string + processMap map[string]bool +} + +func NewProcessItem(processNameList []string) *ProcessItem { + rule := &ProcessItem{ + processes: processNameList, + processMap: make(map[string]bool), + } + for _, processName := range processNameList { + rule.processMap[processName] = true + } + return rule +} + +func (r *ProcessItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { + return false + } + return r.processMap[filepath.Base(metadata.ProcessInfo.ProcessPath)] +} + +func (r *ProcessItem) String() string { + var description string + pLen := len(r.processes) + if pLen == 1 { + description = "process_name=" + r.processes[0] + } else { + description = "process_name=[" + strings.Join(r.processes, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_process_path.go b/route/rule/rule_item_process_path.go new file mode 100644 index 00000000..ac5c6a18 --- /dev/null +++ b/route/rule/rule_item_process_path.go @@ -0,0 +1,54 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +var _ RuleItem = (*ProcessPathItem)(nil) + +type ProcessPathItem struct { + processes []string + processMap map[string]bool +} + +func NewProcessPathItem(processNameList []string) *ProcessPathItem { + rule := &ProcessPathItem{ + processes: processNameList, + processMap: make(map[string]bool), + } + for _, processName := range processNameList { + rule.processMap[processName] = true + } + return rule +} + +func (r *ProcessPathItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil { + return false + } + if metadata.ProcessInfo.ProcessPath != "" && r.processMap[metadata.ProcessInfo.ProcessPath] { + return true + } + if C.IsAndroid { + for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { + if r.processMap[packageName] { + return true + } + } + } + return false +} + +func (r *ProcessPathItem) String() string { + var description string + pLen := len(r.processes) + if pLen == 1 { + description = "process_path=" + r.processes[0] + } else { + description = "process_path=[" + strings.Join(r.processes, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_process_path_regex.go b/route/rule/rule_item_process_path_regex.go new file mode 100644 index 00000000..76cf67b9 --- /dev/null +++ b/route/rule/rule_item_process_path_regex.go @@ -0,0 +1,54 @@ +package rule + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ProcessPathRegexItem)(nil) + +type ProcessPathRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewProcessPathRegexItem(expressions []string) (*ProcessPathRegexItem, error) { + matchers := make([]*regexp.Regexp, 0, len(expressions)) + for i, regex := range expressions { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, E.Cause(err, "parse expression ", i) + } + matchers = append(matchers, matcher) + } + description := "process_path_regex=" + eLen := len(expressions) + if eLen == 1 { + description += expressions[0] + } else if eLen > 3 { + description += F.ToString("[", strings.Join(expressions[:3], " "), "]") + } else { + description += F.ToString("[", strings.Join(expressions, " "), "]") + } + return &ProcessPathRegexItem{matchers, description}, nil +} + +func (r *ProcessPathRegexItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { + return false + } + for _, matcher := range r.matchers { + if matcher.MatchString(metadata.ProcessInfo.ProcessPath) { + return true + } + } + return false +} + +func (r *ProcessPathRegexItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_protocol.go b/route/rule/rule_item_protocol.go new file mode 100644 index 00000000..319b81d5 --- /dev/null +++ b/route/rule/rule_item_protocol.go @@ -0,0 +1,37 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ProtocolItem)(nil) + +type ProtocolItem struct { + protocols []string + protocolMap map[string]bool +} + +func NewProtocolItem(protocols []string) *ProtocolItem { + protocolMap := make(map[string]bool) + for _, protocol := range protocols { + protocolMap[protocol] = true + } + return &ProtocolItem{ + protocols: protocols, + protocolMap: protocolMap, + } +} + +func (r *ProtocolItem) Match(metadata *adapter.InboundContext) bool { + return r.protocolMap[metadata.Protocol] +} + +func (r *ProtocolItem) String() string { + if len(r.protocols) == 1 { + return F.ToString("protocol=", r.protocols[0]) + } + return F.ToString("protocol=[", strings.Join(r.protocols, " "), "]") +} diff --git a/route/rule/rule_item_query_type.go b/route/rule/rule_item_query_type.go new file mode 100644 index 00000000..36b615f3 --- /dev/null +++ b/route/rule/rule_item_query_type.go @@ -0,0 +1,47 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" +) + +var _ RuleItem = (*QueryTypeItem)(nil) + +type QueryTypeItem struct { + typeList []uint16 + typeMap map[uint16]bool +} + +func NewQueryTypeItem(typeList []option.DNSQueryType) *QueryTypeItem { + rule := &QueryTypeItem{ + typeList: common.Map(typeList, func(it option.DNSQueryType) uint16 { + return uint16(it) + }), + typeMap: make(map[uint16]bool), + } + for _, userId := range rule.typeList { + rule.typeMap[userId] = true + } + return rule +} + +func (r *QueryTypeItem) Match(metadata *adapter.InboundContext) bool { + if metadata.QueryType == 0 { + return false + } + return r.typeMap[metadata.QueryType] +} + +func (r *QueryTypeItem) String() string { + var description string + pLen := len(r.typeList) + if pLen == 1 { + description = "query_type=" + option.DNSQueryTypeToString(r.typeList[0]) + } else { + description = "query_type=[" + strings.Join(common.Map(r.typeList, option.DNSQueryTypeToString), " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_response_rcode.go b/route/rule/rule_item_response_rcode.go new file mode 100644 index 00000000..cac75e80 --- /dev/null +++ b/route/rule/rule_item_response_rcode.go @@ -0,0 +1,26 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" + + "github.com/miekg/dns" +) + +var _ RuleItem = (*DNSResponseRCodeItem)(nil) + +type DNSResponseRCodeItem struct { + rcode int +} + +func NewDNSResponseRCodeItem(rcode int) *DNSResponseRCodeItem { + return &DNSResponseRCodeItem{rcode: rcode} +} + +func (r *DNSResponseRCodeItem) Match(metadata *adapter.InboundContext) bool { + return metadata.DNSResponse != nil && metadata.DNSResponse.Rcode == r.rcode +} + +func (r *DNSResponseRCodeItem) String() string { + return F.ToString("response_rcode=", dns.RcodeToString[r.rcode]) +} diff --git a/route/rule/rule_item_response_record.go b/route/rule/rule_item_response_record.go new file mode 100644 index 00000000..3a2c889b --- /dev/null +++ b/route/rule/rule_item_response_record.go @@ -0,0 +1,63 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + + "github.com/miekg/dns" +) + +var _ RuleItem = (*DNSResponseRecordItem)(nil) + +type DNSResponseRecordItem struct { + field string + records []option.DNSRecordOptions + selector func(*dns.Msg) []dns.RR +} + +func NewDNSResponseRecordItem(field string, records []option.DNSRecordOptions, selector func(*dns.Msg) []dns.RR) *DNSResponseRecordItem { + return &DNSResponseRecordItem{ + field: field, + records: records, + selector: selector, + } +} + +func (r *DNSResponseRecordItem) Match(metadata *adapter.InboundContext) bool { + if metadata.DNSResponse == nil { + return false + } + records := r.selector(metadata.DNSResponse) + for _, expected := range r.records { + for _, record := range records { + if expected.Match(record) { + return true + } + } + } + return false +} + +func (r *DNSResponseRecordItem) String() string { + descriptions := make([]string, 0, len(r.records)) + for _, record := range r.records { + if record.RR != nil { + descriptions = append(descriptions, record.RR.String()) + } + } + return r.field + "=[" + strings.Join(descriptions, " ") + "]" +} + +func dnsResponseAnswers(message *dns.Msg) []dns.RR { + return message.Answer +} + +func dnsResponseNS(message *dns.Msg) []dns.RR { + return message.Ns +} + +func dnsResponseExtra(message *dns.Msg) []dns.RR { + return message.Extra +} diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go new file mode 100644 index 00000000..01364943 --- /dev/null +++ b/route/rule/rule_item_rule_set.go @@ -0,0 +1,89 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.RuleSet + ipCidrMatchSource bool + ipCidrAcceptEmpty bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool, ipCidrAcceptEmpty bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipCidrMatchSource: ipCIDRMatchSource, + ipCidrAcceptEmpty: ipCidrAcceptEmpty, + } +} + +func (r *RuleSetItem) Start() error { + _ = r.Close() + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + _ = r.Close() + return E.New("rule-set not found: ", tag) + } + ruleSet.IncRef() + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Close() error { + for _, ruleSet := range r.setList { + ruleSet.DecRef() + } + clear(r.setList) + r.setList = nil + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + return !r.matchStates(metadata).isEmpty() +} + +func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.matchStatesWithBase(metadata, 0) +} + +func (r *RuleSetItem) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + var stateSet ruleMatchStateSet + for _, ruleSet := range r.setList { + nestedMetadata := *metadata + nestedMetadata.ResetRuleMatchCache() + nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource + nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base)) + } + return stateSet +} + +func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { + if r.ipCidrMatchSource { + return false + } + return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { + return ruleSet.Metadata().ContainsIPCIDRRule + }) +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule/rule_item_rule_set_test.go b/route/rule/rule_item_rule_set_test.go new file mode 100644 index 00000000..21d2070d --- /dev/null +++ b/route/rule/rule_item_rule_set_test.go @@ -0,0 +1,138 @@ +package rule + +import ( + "context" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" + + "github.com/stretchr/testify/require" + "go4.org/netipx" +) + +type ruleSetItemTestRouter struct { + ruleSets map[string]adapter.RuleSet +} + +func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil } +func (r *ruleSetItemTestRouter) Close() error { return nil } +func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) { + return nil, nil +} + +func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error { + return nil +} + +func (r *ruleSetItemTestRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error { + return nil +} + +func (r *ruleSetItemTestRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *ruleSetItemTestRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *ruleSetItemTestRouter) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSets[tag] + return ruleSet, loaded +} +func (r *ruleSetItemTestRouter) Rules() []adapter.Rule { return nil } +func (r *ruleSetItemTestRouter) NeedFindProcess() bool { return false } +func (r *ruleSetItemTestRouter) NeedFindNeighbor() bool { return false } +func (r *ruleSetItemTestRouter) NeighborResolver() adapter.NeighborResolver { return nil } +func (r *ruleSetItemTestRouter) AppendTracker(adapter.ConnectionTracker) {} +func (r *ruleSetItemTestRouter) ResetNetwork() {} + +type countingRuleSet struct { + name string + refs atomic.Int32 +} + +func (s *countingRuleSet) Name() string { return s.name } +func (s *countingRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil } +func (s *countingRuleSet) PostStart() error { return nil } +func (s *countingRuleSet) Metadata() adapter.RuleSetMetadata { return adapter.RuleSetMetadata{} } +func (s *countingRuleSet) ExtractIPSet() []*netipx.IPSet { return nil } +func (s *countingRuleSet) IncRef() { s.refs.Add(1) } +func (s *countingRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} +func (s *countingRuleSet) Cleanup() {} +func (s *countingRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + return nil +} +func (s *countingRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {} +func (s *countingRuleSet) Close() error { return nil } +func (s *countingRuleSet) Match(*adapter.InboundContext) bool { return true } +func (s *countingRuleSet) String() string { return s.name } +func (s *countingRuleSet) RefCount() int32 { return s.refs.Load() } + +func TestRuleSetItemCloseReleasesRefs(t *testing.T) { + t.Parallel() + + firstSet := &countingRuleSet{name: "first"} + secondSet := &countingRuleSet{name: "second"} + item := NewRuleSetItem(&ruleSetItemTestRouter{ + ruleSets: map[string]adapter.RuleSet{ + "first": firstSet, + "second": secondSet, + }, + }, []string{"first", "second"}, false, false) + + require.NoError(t, item.Start()) + require.EqualValues(t, 1, firstSet.RefCount()) + require.EqualValues(t, 1, secondSet.RefCount()) + + require.NoError(t, item.Close()) + require.Zero(t, firstSet.RefCount()) + require.Zero(t, secondSet.RefCount()) + + require.NoError(t, item.Close()) + require.Zero(t, firstSet.RefCount()) + require.Zero(t, secondSet.RefCount()) +} + +func TestRuleSetItemStartRollbackOnFailure(t *testing.T) { + t.Parallel() + + firstSet := &countingRuleSet{name: "first"} + item := NewRuleSetItem(&ruleSetItemTestRouter{ + ruleSets: map[string]adapter.RuleSet{ + "first": firstSet, + }, + }, []string{"first", "missing"}, false, false) + + err := item.Start() + require.ErrorContains(t, err, "rule-set not found: missing") + require.Zero(t, firstSet.RefCount()) +} + +func TestRuleSetItemRestartKeepsBalancedRefs(t *testing.T) { + t.Parallel() + + firstSet := &countingRuleSet{name: "first"} + item := NewRuleSetItem(&ruleSetItemTestRouter{ + ruleSets: map[string]adapter.RuleSet{ + "first": firstSet, + }, + }, []string{"first"}, false, false) + + require.NoError(t, item.Start()) + require.EqualValues(t, 1, firstSet.RefCount()) + + require.NoError(t, item.Start()) + require.EqualValues(t, 1, firstSet.RefCount()) + + require.NoError(t, item.Close()) + require.Zero(t, firstSet.RefCount()) +} diff --git a/route/rule/rule_item_source_hostname.go b/route/rule/rule_item_source_hostname.go new file mode 100644 index 00000000..0df11c8c --- /dev/null +++ b/route/rule/rule_item_source_hostname.go @@ -0,0 +1,42 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*SourceHostnameItem)(nil) + +type SourceHostnameItem struct { + hostnames []string + hostnameMap map[string]bool +} + +func NewSourceHostnameItem(hostnameList []string) *SourceHostnameItem { + rule := &SourceHostnameItem{ + hostnames: hostnameList, + hostnameMap: make(map[string]bool), + } + for _, hostname := range hostnameList { + rule.hostnameMap[hostname] = true + } + return rule +} + +func (r *SourceHostnameItem) Match(metadata *adapter.InboundContext) bool { + if metadata.SourceHostname == "" { + return false + } + return r.hostnameMap[metadata.SourceHostname] +} + +func (r *SourceHostnameItem) String() string { + var description string + if len(r.hostnames) == 1 { + description = "source_hostname=" + r.hostnames[0] + } else { + description = "source_hostname=[" + strings.Join(r.hostnames, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_source_mac_address.go b/route/rule/rule_item_source_mac_address.go new file mode 100644 index 00000000..feeadb1d --- /dev/null +++ b/route/rule/rule_item_source_mac_address.go @@ -0,0 +1,48 @@ +package rule + +import ( + "net" + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*SourceMACAddressItem)(nil) + +type SourceMACAddressItem struct { + addresses []string + addressMap map[string]bool +} + +func NewSourceMACAddressItem(addressList []string) *SourceMACAddressItem { + rule := &SourceMACAddressItem{ + addresses: addressList, + addressMap: make(map[string]bool), + } + for _, address := range addressList { + parsed, err := net.ParseMAC(address) + if err == nil { + rule.addressMap[parsed.String()] = true + } else { + rule.addressMap[address] = true + } + } + return rule +} + +func (r *SourceMACAddressItem) Match(metadata *adapter.InboundContext) bool { + if metadata.SourceMACAddress == nil { + return false + } + return r.addressMap[metadata.SourceMACAddress.String()] +} + +func (r *SourceMACAddressItem) String() string { + var description string + if len(r.addresses) == 1 { + description = "source_mac_address=" + r.addresses[0] + } else { + description = "source_mac_address=[" + strings.Join(r.addresses, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_user.go b/route/rule/rule_item_user.go new file mode 100644 index 00000000..87a8bff1 --- /dev/null +++ b/route/rule/rule_item_user.go @@ -0,0 +1,40 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*UserItem)(nil) + +type UserItem struct { + users []string + userMap map[string]bool +} + +func NewUserItem(users []string) *UserItem { + userMap := make(map[string]bool) + for _, protocol := range users { + userMap[protocol] = true + } + return &UserItem{ + users: users, + userMap: userMap, + } +} + +func (r *UserItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || metadata.ProcessInfo.UserName == "" { + return false + } + return r.userMap[metadata.ProcessInfo.UserName] +} + +func (r *UserItem) String() string { + if len(r.users) == 1 { + return F.ToString("user=", r.users[0]) + } + return F.ToString("user=[", strings.Join(r.users, " "), "]") +} diff --git a/route/rule/rule_item_user_id.go b/route/rule/rule_item_user_id.go new file mode 100644 index 00000000..57372de0 --- /dev/null +++ b/route/rule/rule_item_user_id.go @@ -0,0 +1,44 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*UserIdItem)(nil) + +type UserIdItem struct { + userIds []int32 + userIdMap map[int32]bool +} + +func NewUserIDItem(userIdList []int32) *UserIdItem { + rule := &UserIdItem{ + userIds: userIdList, + userIdMap: make(map[int32]bool), + } + for _, userId := range userIdList { + rule.userIdMap[userId] = true + } + return rule +} + +func (r *UserIdItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || metadata.ProcessInfo.UserId == -1 { + return false + } + return r.userIdMap[metadata.ProcessInfo.UserId] +} + +func (r *UserIdItem) String() string { + var description string + pLen := len(r.userIds) + if pLen == 1 { + description = "user_id=" + F.ToString(r.userIds[0]) + } else { + description = "user_id=[" + strings.Join(F.MapToString(r.userIds), " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_wifi_bssid.go b/route/rule/rule_item_wifi_bssid.go new file mode 100644 index 00000000..8f887322 --- /dev/null +++ b/route/rule/rule_item_wifi_bssid.go @@ -0,0 +1,39 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*WIFIBSSIDItem)(nil) + +type WIFIBSSIDItem struct { + bssidList []string + bssidMap map[string]bool + networkManager adapter.NetworkManager +} + +func NewWIFIBSSIDItem(networkManager adapter.NetworkManager, bssidList []string) *WIFIBSSIDItem { + bssidMap := make(map[string]bool) + for _, bssid := range bssidList { + bssidMap[bssid] = true + } + return &WIFIBSSIDItem{ + bssidList, + bssidMap, + networkManager, + } +} + +func (r *WIFIBSSIDItem) Match(metadata *adapter.InboundContext) bool { + return r.bssidMap[r.networkManager.WIFIState().BSSID] +} + +func (r *WIFIBSSIDItem) String() string { + if len(r.bssidList) == 1 { + return F.ToString("wifi_bssid=", r.bssidList[0]) + } + return F.ToString("wifi_bssid=[", strings.Join(r.bssidList, " "), "]") +} diff --git a/route/rule/rule_item_wifi_ssid.go b/route/rule/rule_item_wifi_ssid.go new file mode 100644 index 00000000..ab9fdd88 --- /dev/null +++ b/route/rule/rule_item_wifi_ssid.go @@ -0,0 +1,39 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*WIFISSIDItem)(nil) + +type WIFISSIDItem struct { + ssidList []string + ssidMap map[string]bool + networkManager adapter.NetworkManager +} + +func NewWIFISSIDItem(networkManager adapter.NetworkManager, ssidList []string) *WIFISSIDItem { + ssidMap := make(map[string]bool) + for _, ssid := range ssidList { + ssidMap[ssid] = true + } + return &WIFISSIDItem{ + ssidList, + ssidMap, + networkManager, + } +} + +func (r *WIFISSIDItem) Match(metadata *adapter.InboundContext) bool { + return r.ssidMap[r.networkManager.WIFIState().SSID] +} + +func (r *WIFISSIDItem) String() string { + if len(r.ssidList) == 1 { + return F.ToString("wifi_ssid=", r.ssidList[0]) + } + return F.ToString("wifi_ssid=[", strings.Join(r.ssidList, " "), "]") +} diff --git a/route/rule/rule_nested_action.go b/route/rule/rule_nested_action.go new file mode 100644 index 00000000..44e58839 --- /dev/null +++ b/route/rule/rule_nested_action.go @@ -0,0 +1,71 @@ +package rule + +import ( + "reflect" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func ValidateNoNestedRuleActions(rule option.Rule) error { + return validateNoNestedRuleActions(rule, false) +} + +func ValidateNoNestedDNSRuleActions(rule option.DNSRule) error { + return validateNoNestedDNSRuleActions(rule, false) +} + +func validateNoNestedRuleActions(rule option.Rule, nested bool) error { + if nested && ruleHasConfiguredAction(rule) { + return E.New(option.RouteRuleActionNestedUnsupportedMessage) + } + if rule.Type != C.RuleTypeLogical { + return nil + } + for i, subRule := range rule.LogicalOptions.Rules { + err := validateNoNestedRuleActions(subRule, true) + if err != nil { + return E.Cause(err, "sub rule[", i, "]") + } + } + return nil +} + +func validateNoNestedDNSRuleActions(rule option.DNSRule, nested bool) error { + if nested && dnsRuleHasConfiguredAction(rule) { + return E.New(option.DNSRuleActionNestedUnsupportedMessage) + } + if rule.Type != C.RuleTypeLogical { + return nil + } + for i, subRule := range rule.LogicalOptions.Rules { + err := validateNoNestedDNSRuleActions(subRule, true) + if err != nil { + return E.Cause(err, "sub rule[", i, "]") + } + } + return nil +} + +func ruleHasConfiguredAction(rule option.Rule) bool { + switch rule.Type { + case "", C.RuleTypeDefault: + return !reflect.DeepEqual(rule.DefaultOptions.RuleAction, option.RuleAction{}) + case C.RuleTypeLogical: + return !reflect.DeepEqual(rule.LogicalOptions.RuleAction, option.RuleAction{}) + default: + return false + } +} + +func dnsRuleHasConfiguredAction(rule option.DNSRule) bool { + switch rule.Type { + case "", C.RuleTypeDefault: + return !reflect.DeepEqual(rule.DefaultOptions.DNSRuleAction, option.DNSRuleAction{}) + case C.RuleTypeLogical: + return !reflect.DeepEqual(rule.LogicalOptions.DNSRuleAction, option.DNSRuleAction{}) + default: + return false + } +} diff --git a/route/rule/rule_nested_action_test.go b/route/rule/rule_nested_action_test.go new file mode 100644 index 00000000..f895b892 --- /dev/null +++ b/route/rule/rule_nested_action_test.go @@ -0,0 +1,88 @@ +package rule + +import ( + "context" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/stretchr/testify/require" +) + +func TestNewRuleRejectsNestedRuleAction(t *testing.T) { + t.Parallel() + + _, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), option.Rule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalRule{ + RawLogicalRule: option.RawLogicalRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.Rule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "direct", + }, + }, + }, + }}, + }, + }, + }, false) + require.ErrorContains(t, err, option.RouteRuleActionNestedUnsupportedMessage) +} + +func TestNewDNSRuleRejectsNestedRuleAction(t *testing.T) { + t.Parallel() + + _, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + }, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + }, + }, + }, + }, true, false) + require.ErrorContains(t, err, option.DNSRuleActionNestedUnsupportedMessage) +} + +func TestNewDNSRuleRejectsReplyRejectMethod(t *testing.T) { + t.Parallel() + + _, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: []string{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodReply, + }, + }, + }, + }, false, false) + require.ErrorContains(t, err, "reject method `reply` is not supported for DNS rules") +} diff --git a/route/rule/rule_network_interface_address.go b/route/rule/rule_network_interface_address.go new file mode 100644 index 00000000..c699c593 --- /dev/null +++ b/route/rule/rule_network_interface_address.go @@ -0,0 +1,64 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +var _ RuleItem = (*NetworkInterfaceAddressItem)(nil) + +type NetworkInterfaceAddressItem struct { + networkManager adapter.NetworkManager + interfaceAddresses map[C.InterfaceType][]netip.Prefix + description string +} + +func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]]) *NetworkInterfaceAddressItem { + item := &NetworkInterfaceAddressItem{ + networkManager: networkManager, + interfaceAddresses: make(map[C.InterfaceType][]netip.Prefix, interfaceAddresses.Size()), + } + var entryDescriptions []string + for _, entry := range interfaceAddresses.Entries() { + prefixes := make([]netip.Prefix, 0, len(entry.Value)) + for _, prefixable := range entry.Value { + prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) + } + item.interfaceAddresses[entry.Key.Build()] = prefixes + entryDescriptions = append(entryDescriptions, entry.Key.Build().String()+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) + } + item.description = "network_interface_address=[" + strings.Join(entryDescriptions, " ") + "]" + return item +} + +func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + interfaces := r.networkManager.NetworkInterfaces() +match: + for ifType, addresses := range r.interfaceAddresses { + for _, networkInterface := range interfaces { + if networkInterface.Type != ifType { + continue + } + if common.Any(networkInterface.Addresses, func(it netip.Prefix) bool { + return common.Any(addresses, func(prefix netip.Prefix) bool { + return prefix.Overlaps(it) + }) + }) { + continue match + } + } + return false + } + return true +} + +func (r *NetworkInterfaceAddressItem) String() string { + return r.description +} diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go new file mode 100644 index 00000000..7c82b602 --- /dev/null +++ b/route/rule/rule_set.go @@ -0,0 +1,93 @@ +package rule + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" + + "go4.org/netipx" +) + +func NewRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeInline, C.RuleSetTypeLocal, "": + return NewLocalRuleSet(ctx, logger, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, logger, options), nil + default: + return nil, E.New("unknown rule-set type: ", options.Type) + } +} + +func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { + switch rule := rawRule.(type) { + case *DefaultHeadlessRule: + return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet { + switch item := rawItem.(type) { + case *IPCIDRItem: + return []*netipx.IPSet{item.ipSet} + default: + return nil + } + }) + case *LogicalHeadlessRule: + return common.FlatMap(rule.rules, extractIPSetFromRule) + default: + panic("unexpected rule type") + } +} + +func HasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if HasHeadlessRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.PackageNameRegex) > 0 +} + +func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.IPCIDR) > 0 || rule.IPSet != nil +} + +func isDNSQueryTypeHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.QueryType) > 0 +} + +func buildRuleSetMetadata(headlessRules []option.HeadlessRule) adapter.RuleSetMetadata { + return adapter.RuleSetMetadata{ + ContainsProcessRule: HasHeadlessRule(headlessRules, isProcessHeadlessRule), + ContainsWIFIRule: HasHeadlessRule(headlessRules, isWIFIHeadlessRule), + ContainsIPCIDRRule: HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule), + ContainsDNSQueryTypeRule: HasHeadlessRule(headlessRules, isDNSQueryTypeHeadlessRule), + } +} + +func validateRuleSetMetadataUpdate(ctx context.Context, tag string, metadata adapter.RuleSetMetadata) error { + validator := service.FromContext[adapter.DNSRuleSetUpdateValidator](ctx) + if validator == nil { + return nil + } + return validator.ValidateRuleSetMetadataUpdate(tag, metadata) +} diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go new file mode 100644 index 00000000..5408615f --- /dev/null +++ b/route/rule/rule_set_local.go @@ -0,0 +1,221 @@ +package rule + +import ( + "context" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "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" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service/filemanager" + + "go4.org/netipx" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + ctx context.Context + logger logger.Logger + tag string + access sync.RWMutex + rules []adapter.HeadlessRule + metadata adapter.RuleSetMetadata + fileFormat string + watcher *fswatch.Watcher + callbacks list.List[adapter.RuleSetUpdateCallback] + refs atomic.Int32 +} + +func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { + ruleSet := &LocalRuleSet{ + ctx: ctx, + logger: logger, + tag: options.Tag, + fileFormat: options.Format, + } + if options.Type == C.RuleSetTypeInline { + if len(options.InlineOptions.Rules) == 0 { + return nil, E.New("empty inline rule-set") + } + err := ruleSet.reloadRules(options.InlineOptions.Rules) + if err != nil { + return nil, err + } + } else { + filePath := filemanager.BasePath(ctx, options.LocalOptions.Path) + filePath, _ = filepath.Abs(filePath) + err := ruleSet.reloadFile(filePath) + if err != nil { + return nil, err + } + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: []string{filePath}, + Callback: func(path string) { + uErr := ruleSet.reloadFile(path) + if uErr != nil { + logger.Error(E.Cause(uErr, "reload rule-set ", options.Tag)) + } + }, + }) + if err != nil { + return nil, err + } + ruleSet.watcher = watcher + } + return ruleSet, nil +} + +func (s *LocalRuleSet) Name() string { + return s.tag +} + +func (s *LocalRuleSet) String() string { + return strings.Join(F.MapToString(s.rules), " ") +} + +func (s *LocalRuleSet) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error { + if s.watcher != nil { + err := s.watcher.Start() + if err != nil { + s.logger.Error(E.Cause(err, "watch rule-set file")) + } + } + return nil +} + +func (s *LocalRuleSet) reloadFile(path string) error { + var ruleSet option.PlainRuleSetCompat + switch s.fileFormat { + case C.RuleSetFormatSource, "": + content, err := os.ReadFile(path) + if err != nil { + return err + } + ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + + case C.RuleSetFormatBinary: + setFile, err := os.Open(path) + if err != nil { + return err + } + ruleSet, err = srs.Read(setFile, false) + if err != nil { + return err + } + default: + return E.New("unknown rule-set format: ", s.fileFormat) + } + plainRuleSet, err := ruleSet.Upgrade() + if err != nil { + return err + } + return s.reloadRules(plainRuleSet.Rules) +} + +func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { + rules := make([]adapter.HeadlessRule, len(headlessRules)) + var err error + for i, ruleOptions := range headlessRules { + rules[i], err = NewHeadlessRule(s.ctx, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + metadata := buildRuleSetMetadata(headlessRules) + err = validateRuleSetMetadataUpdate(s.ctx, s.tag, metadata) + if err != nil { + return err + } + s.access.Lock() + s.rules = rules + s.metadata = metadata + callbacks := s.callbacks.Array() + s.access.Unlock() + for _, callback := range callbacks { + callback(s) + } + return nil +} + +func (s *LocalRuleSet) PostStart() error { + return nil +} + +func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { + s.access.RLock() + defer s.access.RUnlock() + return s.metadata +} + +func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { + s.access.RLock() + defer s.access.RUnlock() + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *LocalRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *LocalRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *LocalRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.access.Lock() + defer s.access.Unlock() + s.callbacks.Remove(element) +} + +func (s *LocalRuleSet) Close() error { + s.rules = nil + return common.Close(common.PtrOrNil(s.watcher)) +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + return !s.matchStates(metadata).isEmpty() +} + +func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return s.matchStatesWithBase(metadata, 0) +} + +func (s *LocalRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + var stateSet ruleMatchStateSet + for _, rule := range s.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleMatchCache() + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base)) + } + return stateSet +} diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go new file mode 100644 index 00000000..53d353b3 --- /dev/null +++ b/route/rule/rule_set_remote.go @@ -0,0 +1,343 @@ +package rule + +import ( + "bytes" + "context" + "crypto/tls" + "io" + "net" + "net/http" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "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" + "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" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" + + "go4.org/netipx" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + logger logger.ContextLogger + outbound adapter.OutboundManager + options option.RuleSet + updateInterval time.Duration + dialer N.Dialer + access sync.RWMutex + rules []adapter.HeadlessRule + metadata adapter.RuleSetMetadata + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker + cacheFile adapter.CacheFile + pauseManager pause.Manager + callbacks list.List[adapter.RuleSetUpdateCallback] + refs atomic.Int32 +} + +func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 24 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + outbound: service.FromContext[adapter.OutboundManager](ctx), + logger: logger, + options: options, + updateInterval: updateInterval, + pauseManager: service.FromContext[pause.Manager](ctx), + } +} + +func (s *RemoteRuleSet) Name() string { + return s.options.Tag +} + +func (s *RemoteRuleSet) String() string { + return strings.Join(F.MapToString(s.rules), " ") +} + +func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error { + s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx) + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.outbound.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + dialer = s.outbound.Default() + } + s.dialer = dialer + if s.cacheFile != nil { + if savedSet := s.cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() { + err := s.fetch(ctx, startContext) + if err != nil { + return E.Cause(err, "initial rule-set: ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + return nil +} + +func (s *RemoteRuleSet) PostStart() error { + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { + s.access.RLock() + defer s.access.RUnlock() + return s.metadata +} + +func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { + s.access.RLock() + defer s.access.RUnlock() + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *RemoteRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *RemoteRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *RemoteRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.access.Lock() + defer s.access.Unlock() + s.callbacks.Remove(element) +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + ruleSet option.PlainRuleSetCompat + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource: + ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + case C.RuleSetFormatBinary: + ruleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule-set format: ", s.options.Format) + } + plainRuleSet, err := ruleSet.Upgrade() + if err != nil { + return err + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.ctx, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + metadata := buildRuleSetMetadata(plainRuleSet.Rules) + err = validateRuleSetMetadataUpdate(s.ctx, s.options.Tag, metadata) + if err != nil { + return err + } + s.access.Lock() + s.metadata = metadata + s.rules = rules + callbacks := s.callbacks.Array() + s.access.Unlock() + for _, callback := range callbacks { + callback(s) + } + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + if time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetch(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } else if s.refs.Load() == 0 { + s.rules = nil + } + } + for { + runtime.GC() + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + s.updateOnce() + } + } +} + +func (s *RemoteRuleSet) updateOnce() { + err := s.fetch(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } else if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPStartContext) error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + var httpClient *http.Client + if startContext != nil { + httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) + } else { + httpClient = &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + Time: ntp.TimeFuncFromContext(s.ctx), + RootCAs: adapter.RootPoolFromContext(s.ctx), + }, + }, + } + } + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.lastUpdated = time.Now() + if s.cacheFile != nil { + savedRuleSet := s.cacheFile.LoadRuleSet(s.options.Tag) + if savedRuleSet != nil { + savedRuleSet.LastUpdated = s.lastUpdated + err = s.cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet) + if err != nil { + s.logger.Error("save rule-set updated time: ", err) + return nil + } + } + } + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + if s.cacheFile != nil { + err = s.cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedBinary{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.rules = nil + s.cancel() + if s.updateTicker != nil { + s.updateTicker.Stop() + } + return nil +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + return !s.matchStates(metadata).isEmpty() +} + +func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return s.matchStatesWithBase(metadata, 0) +} + +func (s *RemoteRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + var stateSet ruleMatchStateSet + for _, rule := range s.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleMatchCache() + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base)) + } + return stateSet +} diff --git a/route/rule/rule_set_semantics_test.go b/route/rule/rule_set_semantics_test.go new file mode 100644 index 00000000..2fc559d2 --- /dev/null +++ b/route/rule/rule_set_semantics_test.go @@ -0,0 +1,1261 @@ +package rule + +import ( + "context" + "net" + "net/netip" + "strings" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/convertor/adguard" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + slogger "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestRouteRuleSetMergeDestinationAddressGroup(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + metadata adapter.InboundContext + inner adapter.HeadlessRule + }{ + { + name: "domain", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, []string{"www.example.com"}, nil) }), + }, + { + name: "domain_suffix", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), + }, + { + name: "domain_keyword", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationKeywordItem(rule, []string{"example"}) }), + }, + { + name: "domain_regex", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationRegexItem(t, rule, []string{`^www\.example\.com$`}) }), + }, + { + name: "ip_cidr", + metadata: func() adapter.InboundContext { + metadata := testMetadata("lookup.example") + metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")} + return metadata + }(), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"8.8.8.0/24"}) + }), + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + ruleSet := newLocalRuleSetForTest("merge-destination", testCase.inner) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&testCase.metadata)) + }) + } +} + +func TestRouteRuleSetMergeSourceAndPortGroups(t *testing.T) { + t.Parallel() + t.Run("source address", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-address", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourceAddressItem(t, rule, []string{"10.0.0.0/8"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("source address via ruleset ipcidr match source", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-address-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"10.0.0.0/8"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{ + setList: []adapter.RuleSet{ruleSet}, + ipCidrMatchSource: true, + }) + addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("destination port", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-destination-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationPortItem(rule, []uint16{443}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationPortItem(rule, []uint16{8443}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("destination port range", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-destination-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationPortRangeItem(t, rule, []string{"400:500"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationPortItem(rule, []uint16{8443}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("source port", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addSourcePortItem(rule, []uint16{2000}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("source port range", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortRangeItem(t, rule, []string{"900:1100"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addSourcePortItem(rule, []uint16{2000}) + }) + require.True(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetOuterGroupedStateMergesIntoSameGroup(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + metadata adapter.InboundContext + buildOuter func(*testing.T, *abstractDefaultRule) + buildInner func(*testing.T, *abstractDefaultRule) + }{ + { + name: "destination address", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + }, + }, + { + name: "source address", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourceAddressItem(t, rule, []string{"10.0.0.0/8"}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) + }, + }, + { + name: "source port", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourcePortItem(rule, []uint16{1000}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourcePortItem(rule, []uint16{2000}) + }, + }, + { + name: "destination port", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationPortItem(rule, []uint16{443}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationPortItem(rule, []uint16{8443}) + }, + }, + { + name: "destination ip cidr", + metadata: func() adapter.InboundContext { + metadata := testMetadata("lookup.example") + metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")} + return metadata + }(), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"198.51.100.0/24"}) + }, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + ruleSet := newLocalRuleSetForTest("outer-merge-"+testCase.name, headlessDefaultRule(t, func(rule *abstractDefaultRule) { + testCase.buildInner(t, rule) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + testCase.buildOuter(t, rule) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&testCase.metadata)) + }) + } +} + +func TestRouteRuleSetOtherFieldsStayAnd(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("other-fields-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + }) + require.False(t, rule.Match(&metadata)) +} + +func TestRouteRuleSetMergedBranchKeepsAndConstraints(t *testing.T) { + t.Parallel() + t.Run("outer group does not bypass inner non grouped condition", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("outer group does not satisfy different grouped branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("different-group", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetOrSemantics(t *testing.T) { + t.Parallel() + t.Run("later ruleset can satisfy outer group", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + emptyStateSet := newLocalRuleSetForTest("network-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + })) + destinationStateSet := newLocalRuleSetForTest("domain-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("later rule in same set can satisfy outer group", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest( + "rule-set-or", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + ) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("cross ruleset union is not allowed", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + sourceStateSet := newLocalRuleSetForTest("source-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + })) + destinationStateSet := newLocalRuleSetForTest("destination-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{sourceStateSet, destinationStateSet}}) + addSourcePortItem(rule, []uint16{2000}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetLogicalSemantics(t *testing.T) { + t.Parallel() + t.Run("logical or keeps all successful branch states", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-or", headlessLogicalRule( + C.LogicalTypeOr, + false, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("logical and unions child states", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-and", headlessLogicalRule( + C.LogicalTypeAnd, + false, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + addSourcePortItem(rule, []uint16{2000}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("invert success does not contribute positive state", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("invert", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationAddressItem(t, rule, nil, []string{"cn"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) { + t.Parallel() + t.Run("default invert keeps inherited group outside grouped predicate", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("default invert keeps inherited group after negation succeeds", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("invert-network", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("logical invert keeps inherited group outside grouped predicate", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-invert-grouped", headlessLogicalRule( + C.LogicalTypeOr, + true, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("logical invert keeps inherited group after negation succeeds", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-invert-network", headlessLogicalRule( + C.LogicalTypeOr, + true, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetNoLeakageRegressions(t *testing.T) { + t.Parallel() + t.Run("same ruleset failed branch does not leak", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest( + "same-set", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addSourcePortItem(rule, []uint16{1}) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + addSourcePortItem(rule, []uint16{1000}) + }), + ) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("adguard exclusion remains isolated across rulesets", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("im.qq.com") + excludeSet := newLocalRuleSetForTest("adguard", mustAdGuardRule(t, "@@||im.qq.com^\n||whatever1.com^\n")) + otherSet := newLocalRuleSetForTest("other", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"whatever2.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{excludeSet, otherSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestDefaultRuleDoesNotReuseGroupedMatchCacheAcrossEvaluations(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }) + require.True(t, rule.Match(&metadata)) + + metadata.Destination.Fqdn = "www.example.org" + require.False(t, rule.Match(&metadata)) +} + +func TestRouteRuleSetRemoteUsesSameSemantics(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newRemoteRuleSetForTest( + "remote", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + ) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) +} + +func TestDNSRuleSetSemantics(t *testing.T) { + t.Parallel() + t.Run("outer destination group merges into matching ruleset branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.baidu.com") + ruleSet := newLocalRuleSetForTest("dns-merged-branch", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"baidu.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("outer destination group does not bypass ruleset non grouped condition", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("outer destination group stays outside inverted grouped branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.baidu.com") + ruleSet := newLocalRuleSetForTest("dns-invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"baidu.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("outer destination group stays outside inverted logical branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-logical-invert-network", headlessLogicalRule( + C.LogicalTypeOr, + true, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + }), + )) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("match address limit merges destination group", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-merge", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + }) + t.Run("dns keeps ruleset or semantics", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + emptyStateSet := newLocalRuleSetForTest("dns-empty", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + })) + destinationStateSet := newLocalRuleSetForTest("dns-destination", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + }) + t.Run("ruleset ip cidr flags stay scoped", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{ + setList: []adapter.RuleSet{ruleSet}, + ipCidrAcceptEmpty: true, + }) + }) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + require.False(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest())) + require.False(t, metadata.IPCIDRMatchSource) + require.False(t, metadata.IPCIDRAcceptEmpty) + }) + t.Run("pre lookup ruleset only deferred fields fail closed", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + ruleSet := newLocalRuleSetForTest("dns-prelookup-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + // This is accepted without match_response so mixed rule_set deployments keep + // working; the destination-IP-only branch simply cannot match before a DNS + // response is available. + require.False(t, rule.Match(&metadata)) + }) + t.Run("pre lookup ruleset destination cidr does not fall back to other predicates", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + ruleSet := newLocalRuleSetForTest("dns-prelookup-network-and-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("pre lookup mixed ruleset still matches non response branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest( + "dns-prelookup-mixed", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + ) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + // Destination-IP predicates inside rule_set fail closed before the DNS response, + // but they must not force validation errors or suppress sibling non-response + // branches. + require.True(t, rule.Match(&metadata)) + }) +} + +func TestDNSMatchResponseRuleSetDestinationCIDRUsesDNSResponse(t *testing.T) { + t.Parallel() + + ruleSet := newLocalRuleSetForTest("dns-response-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + rule.matchResponse = true + + matchedMetadata := testMetadata("lookup.example") + matchedMetadata.DNSResponse = dnsResponseForTest(netip.MustParseAddr("203.0.113.1")) + require.True(t, rule.Match(&matchedMetadata)) + require.Empty(t, matchedMetadata.DestinationAddresses) + + unmatchedMetadata := testMetadata("lookup.example") + unmatchedMetadata.DNSResponse = dnsResponseForTest(netip.MustParseAddr("8.8.8.8")) + require.False(t, rule.Match(&unmatchedMetadata)) +} + +func TestDNSMatchResponseMissingResponseUsesBooleanSemantics(t *testing.T) { + t.Parallel() + + t.Run("plain rule remains false", func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) {}) + rule.matchResponse = true + + metadata := testMetadata("lookup.example") + require.False(t, rule.Match(&metadata)) + }) + + t.Run("invert rule becomes true", func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + }) + rule.matchResponse = true + + metadata := testMetadata("lookup.example") + require.True(t, rule.Match(&metadata)) + }) + + t.Run("logical wrapper respects inverted child", func(t *testing.T) { + t.Parallel() + + nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + }) + nestedRule.matchResponse = true + + logicalRule := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: []adapter.HeadlessRule{nestedRule}, + mode: C.LogicalTypeAnd, + }, + } + + metadata := testMetadata("lookup.example") + require.True(t, logicalRule.Match(&metadata)) + }) +} + +func TestDNSAddressLimitIgnoresDestinationAddresses(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedResponse *mDNS.Msg + unmatchedResponse *mDNS.Msg + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("10.0.0.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(), + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + testCase.build(t, rule) + }) + + mismatchMetadata := testMetadata("lookup.example") + mismatchMetadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")} + require.False(t, rule.MatchAddressLimit(&mismatchMetadata, testCase.unmatchedResponse)) + + matchMetadata := testMetadata("lookup.example") + matchMetadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")} + require.True(t, rule.MatchAddressLimit(&matchMetadata, testCase.matchedResponse)) + }) + } +} + +func TestDNSLegacyAddressLimitPreLookupDefersDirectRules(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedResponse *mDNS.Msg + unmatchedResponse *mDNS.Msg + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("10.0.0.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(), + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + testCase.build(t, rule) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&matchedMetadata, testCase.matchedResponse)) + + unmatchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&unmatchedMetadata, testCase.unmatchedResponse)) + }) + } +} + +func TestDNSLegacyAddressLimitPreLookupDefersRuleSetDestinationCIDR(t *testing.T) { + t.Parallel() + + ruleSet := newLocalRuleSetForTest("dns-legacy-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + + unmatchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) +} + +func TestDNSLegacyLogicalAddressLimitPreLookupDefersNestedRules(t *testing.T) { + t.Parallel() + + nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationIPIsPrivateItem(rule) + }) + logicalRule := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: []adapter.HeadlessRule{nestedRule}, + mode: C.LogicalTypeAnd, + }, + } + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.True(t, logicalRule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("10.0.0.1")))) + + unmatchedMetadata := testMetadata("lookup.example") + require.False(t, logicalRule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) +} + +func TestDNSLegacyInvertAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedAddrs []netip.Addr + unmatchedAddrs []netip.Addr + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + testCase.build(t, rule) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...))) + + unmatchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...))) + }) + } +} + +func TestDNSLegacyInvertLogicalAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + + t.Run("inverted deferred child does not suppress branch", func(t *testing.T) { + t.Parallel() + + logicalRule := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: []adapter.HeadlessRule{ + dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationIPIsPrivateItem(rule) + }), + }, + mode: C.LogicalTypeAnd, + }, + } + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata)) + }) +} + +func TestDNSLegacyInvertRuleSetAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + + ruleSet := newLocalRuleSetForTest("dns-legacy-invert-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + + unmatchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) +} + +func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedAddrs []netip.Addr + unmatchedAddrs []netip.Addr + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + testCase.build(t, rule) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.Match(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + matchedMetadata.DestinationAddresses = testCase.matchedAddrs + require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...))) + + unmatchedMetadata := testMetadata("lookup.example") + unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...))) + }) + } + t.Run("mixed resolved and deferred fields invert matches pre lookup", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("ruleset only deferred fields invert matches pre lookup", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + ruleSet := newLocalRuleSetForTest("dns-ruleset-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) +} + +func routeRuleForTest(build func(*abstractDefaultRule)) *DefaultRule { + rule := &DefaultRule{} + build(&rule.abstractDefaultRule) + return rule +} + +func dnsRuleForTest(build func(*abstractDefaultRule)) *DefaultDNSRule { + rule := &DefaultDNSRule{} + build(&rule.abstractDefaultRule) + return rule +} + +func headlessDefaultRule(t *testing.T, build func(*abstractDefaultRule)) *DefaultHeadlessRule { + t.Helper() + rule := &DefaultHeadlessRule{} + build(&rule.abstractDefaultRule) + return rule +} + +func headlessLogicalRule(mode string, invert bool, rules ...adapter.HeadlessRule) *LogicalHeadlessRule { + return &LogicalHeadlessRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: rules, + mode: mode, + invert: invert, + }, + } +} + +func newLocalRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *LocalRuleSet { + return &LocalRuleSet{ + tag: tag, + rules: rules, + } +} + +func newRemoteRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *RemoteRuleSet { + return &RemoteRuleSet{ + options: option.RuleSet{Tag: tag}, + rules: rules, + } +} + +func mustAdGuardRule(t *testing.T, content string) adapter.HeadlessRule { + t.Helper() + rules, err := adguard.ToOptions(strings.NewReader(content), slogger.NOP()) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := NewHeadlessRule(context.Background(), rules[0]) + require.NoError(t, err) + return rule +} + +func testMetadata(domain string) adapter.InboundContext { + return adapter.InboundContext{ + Network: N.NetworkTCP, + Source: M.Socksaddr{ + Addr: netip.MustParseAddr("10.0.0.1"), + Port: 1000, + }, + Destination: M.Socksaddr{ + Fqdn: domain, + Port: 443, + }, + } +} + +func dnsResponseForTest(addresses ...netip.Addr) *mDNS.Msg { + response := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeSuccess, + }, + } + for _, address := range addresses { + if address.Is4() { + response.Answer = append(response.Answer, &mDNS.A{ + Hdr: mDNS.RR_Header{ + Name: mDNS.Fqdn("lookup.example"), + Rrtype: mDNS.TypeA, + Class: mDNS.ClassINET, + Ttl: 60, + }, + A: net.IP(append([]byte(nil), address.AsSlice()...)), + }) + } else { + response.Answer = append(response.Answer, &mDNS.AAAA{ + Hdr: mDNS.RR_Header{ + Name: mDNS.Fqdn("lookup.example"), + Rrtype: mDNS.TypeAAAA, + Class: mDNS.ClassINET, + Ttl: 60, + }, + AAAA: net.IP(append([]byte(nil), address.AsSlice()...)), + }) + } + } + return response +} + +func addRuleSetItem(rule *abstractDefaultRule, item *RuleSetItem) { + rule.ruleSetItem = item + rule.allItems = append(rule.allItems, item) +} + +func addOtherItem(rule *abstractDefaultRule, item RuleItem) { + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) +} + +func addSourceAddressItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) { + t.Helper() + item, err := NewIPCIDRItem(true, cidrs) + require.NoError(t, err) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationAddressItem(t *testing.T, rule *abstractDefaultRule, domains []string, suffixes []string) { + t.Helper() + item, err := NewDomainItem(domains, suffixes) + require.NoError(t, err) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationKeywordItem(rule *abstractDefaultRule, keywords []string) { + item := NewDomainKeywordItem(keywords) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationRegexItem(t *testing.T, rule *abstractDefaultRule, regexes []string) { + t.Helper() + item, err := NewDomainRegexItem(regexes) + require.NoError(t, err) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationIPCIDRItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) { + t.Helper() + item, err := NewIPCIDRItem(false, cidrs) + require.NoError(t, err) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationIPIsPrivateItem(rule *abstractDefaultRule) { + item := NewIPIsPrivateItem(false) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationIPAcceptAnyItem(rule *abstractDefaultRule) { + item := NewIPAcceptAnyItem() + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addSourcePortItem(rule *abstractDefaultRule, ports []uint16) { + item := NewPortItem(true, ports) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addSourcePortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) { + t.Helper() + item, err := NewPortRangeItem(true, ranges) + require.NoError(t, err) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationPortItem(rule *abstractDefaultRule, ports []uint16) { + item := NewPortItem(false, ports) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationPortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) { + t.Helper() + item, err := NewPortRangeItem(false, ranges) + require.NoError(t, err) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) +} diff --git a/route/rule/rule_set_update_validation_test.go b/route/rule/rule_set_update_validation_test.go new file mode 100644 index 00000000..0583d7bb --- /dev/null +++ b/route/rule/rule_set_update_validation_test.go @@ -0,0 +1,111 @@ +package rule + +import ( + "context" + "sync/atomic" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + "github.com/stretchr/testify/require" +) + +type fakeDNSRuleSetUpdateValidator struct { + validate func(tag string, metadata adapter.RuleSetMetadata) error +} + +func (v *fakeDNSRuleSetUpdateValidator) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error { + if v.validate == nil { + return nil + } + return v.validate(tag, metadata) +} + +func TestLocalRuleSetReloadRulesRejectsInvalidUpdateBeforeCommit(t *testing.T) { + t.Parallel() + + var callbackCount atomic.Int32 + ctx := service.ContextWith[adapter.DNSRuleSetUpdateValidator](context.Background(), &fakeDNSRuleSetUpdateValidator{ + validate: func(tag string, metadata adapter.RuleSetMetadata) error { + require.Equal(t, "dynamic-set", tag) + if metadata.ContainsDNSQueryTypeRule { + return E.New("dns conflict") + } + return nil + }, + }) + ruleSet := &LocalRuleSet{ + ctx: ctx, + tag: "dynamic-set", + fileFormat: C.RuleSetFormatSource, + } + _ = ruleSet.callbacks.PushBack(func(adapter.RuleSet) { + callbackCount.Add(1) + }) + + err := ruleSet.reloadRules([]option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }}) + require.NoError(t, err) + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) + + err = ruleSet.reloadRules([]option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(1)}, + }, + }}) + require.ErrorContains(t, err, "dns conflict") + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) +} + +func TestRemoteRuleSetLoadBytesRejectsInvalidUpdateBeforeCommit(t *testing.T) { + t.Parallel() + + var callbackCount atomic.Int32 + ctx := service.ContextWith[adapter.DNSRuleSetUpdateValidator](context.Background(), &fakeDNSRuleSetUpdateValidator{ + validate: func(tag string, metadata adapter.RuleSetMetadata) error { + require.Equal(t, "dynamic-set", tag) + if metadata.ContainsDNSQueryTypeRule { + return E.New("dns conflict") + } + return nil + }, + }) + ruleSet := &RemoteRuleSet{ + ctx: ctx, + options: option.RuleSet{ + Tag: "dynamic-set", + Format: C.RuleSetFormatSource, + }, + callbacks: list.List[adapter.RuleSetUpdateCallback]{}, + } + _ = ruleSet.callbacks.PushBack(func(adapter.RuleSet) { + callbackCount.Add(1) + }) + + err := ruleSet.loadBytes([]byte(`{"version":4,"rules":[{"domain":["example.com"]}]}`)) + require.NoError(t, err) + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) + + err = ruleSet.loadBytes([]byte(`{"version":4,"rules":[{"query_type":["A"]}]}`)) + require.ErrorContains(t, err, "dns conflict") + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) +} diff --git a/route/rule_conds.go b/route/rule_conds.go new file mode 100644 index 00000000..2c629029 --- /dev/null +++ b/route/rule_conds.go @@ -0,0 +1,62 @@ +package route + +import ( + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func isProcessRule(rule option.DefaultRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.PackageNameRegex) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 +} + +func isProcessDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.PackageNameRegex) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 +} + +func isNeighborRule(rule option.DefaultRule) bool { + return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0 +} + +func isNeighborDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0 +} + +func isWIFIRule(rule option.DefaultRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} diff --git a/service/acme/service.go b/service/acme/service.go new file mode 100644 index 00000000..8286a197 --- /dev/null +++ b/service/acme/service.go @@ -0,0 +1,411 @@ +//go:build with_acme + +package acme + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "net" + "net/http" + "net/url" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/common/dialer" + boxtls "github.com/sagernet/sing-box/common/tls" + 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" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + + "github.com/caddyserver/certmagic" + "github.com/caddyserver/zerossl" + "github.com/libdns/alidns" + "github.com/libdns/cloudflare" + "github.com/libdns/libdns" + "github.com/mholt/acmez/v3/acme" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func RegisterCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.ACMECertificateProviderOptions](registry, C.TypeACME, NewCertificateProvider) +} + +var ( + _ adapter.CertificateProviderService = (*Service)(nil) + _ adapter.ACMECertificateProvider = (*Service)(nil) +) + +type Service struct { + certificate.Adapter + ctx context.Context + config *certmagic.Config + cache *certmagic.Cache + domain []string + nextProtos []string +} + +func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) { + if len(options.Domain) == 0 { + return nil, E.New("missing domain") + } + var acmeServer string + switch options.Provider { + case "", "letsencrypt": + acmeServer = certmagic.LetsEncryptProductionCA + case "zerossl": + acmeServer = certmagic.ZeroSSLProductionCA + default: + if !strings.HasPrefix(options.Provider, "https://") { + return nil, E.New("unsupported ACME provider: ", options.Provider) + } + acmeServer = options.Provider + } + if acmeServer == certmagic.ZeroSSLProductionCA && + (options.ExternalAccount == nil || options.ExternalAccount.KeyID == "") && + strings.TrimSpace(options.Email) == "" && + strings.TrimSpace(options.AccountKey) == "" { + return nil, E.New("email is required to use the ZeroSSL ACME endpoint without external_account or account_key") + } + + var storage certmagic.Storage + if options.DataDirectory != "" { + storage = &certmagic.FileStorage{Path: options.DataDirectory} + } else { + storage = certmagic.Default.Storage + } + + zapLogger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(boxtls.ACMEEncoderConfig()), + &boxtls.ACMELogWriter{Logger: logger}, + zap.DebugLevel, + )) + + config := &certmagic.Config{ + DefaultServerName: options.DefaultServerName, + Storage: storage, + Logger: zapLogger, + } + if options.KeyType != "" { + var keyType certmagic.KeyType + switch options.KeyType { + case option.ACMEKeyTypeED25519: + keyType = certmagic.ED25519 + case option.ACMEKeyTypeP256: + keyType = certmagic.P256 + case option.ACMEKeyTypeP384: + keyType = certmagic.P384 + case option.ACMEKeyTypeRSA2048: + keyType = certmagic.RSA2048 + case option.ACMEKeyTypeRSA4096: + keyType = certmagic.RSA4096 + default: + return nil, E.New("unsupported ACME key type: ", options.KeyType) + } + config.KeySource = certmagic.StandardKeyGenerator{KeyType: keyType} + } + + acmeIssuer := certmagic.ACMEIssuer{ + CA: acmeServer, + Email: options.Email, + AccountKeyPEM: options.AccountKey, + Agreed: true, + DisableHTTPChallenge: options.DisableHTTPChallenge, + DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, + AltHTTPPort: int(options.AlternativeHTTPPort), + AltTLSALPNPort: int(options.AlternativeTLSPort), + Logger: zapLogger, + } + acmeHTTPClient, err := newACMEHTTPClient(ctx, options.Detour) + if err != nil { + return nil, err + } + dnsSolver, err := newDNSSolver(options.DNS01Challenge, zapLogger, acmeHTTPClient) + if err != nil { + return nil, err + } + if dnsSolver != nil { + acmeIssuer.DNS01Solver = dnsSolver + } + if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" { + acmeIssuer.ExternalAccount = (*acme.EAB)(options.ExternalAccount) + } + if acmeServer == certmagic.ZeroSSLProductionCA { + acmeIssuer.NewAccountFunc = func(ctx context.Context, acmeIssuer *certmagic.ACMEIssuer, account acme.Account) (acme.Account, error) { + if acmeIssuer.ExternalAccount != nil { + return account, nil + } + var err error + acmeIssuer.ExternalAccount, account, err = createZeroSSLExternalAccountBinding(ctx, acmeIssuer, account, acmeHTTPClient) + return account, err + } + } + + certmagicIssuer := certmagic.NewACMEIssuer(config, acmeIssuer) + httpClientField := reflect.ValueOf(certmagicIssuer).Elem().FieldByName("httpClient") + if !httpClientField.IsValid() || !httpClientField.CanAddr() { + return nil, E.New("certmagic ACME issuer HTTP client field is unavailable") + } + reflect.NewAt(httpClientField.Type(), unsafe.Pointer(httpClientField.UnsafeAddr())).Elem().Set(reflect.ValueOf(acmeHTTPClient)) + config.Issuers = []certmagic.Issuer{certmagicIssuer} + cache := certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { + return config, nil + }, + Logger: zapLogger, + }) + config = certmagic.New(cache, *config) + + var nextProtos []string + if !acmeIssuer.DisableTLSALPNChallenge && acmeIssuer.DNS01Solver == nil { + nextProtos = []string{C.ACMETLS1Protocol} + } + return &Service{ + Adapter: certificate.NewAdapter(C.TypeACME, tag), + ctx: ctx, + config: config, + cache: cache, + domain: options.Domain, + nextProtos: nextProtos, + }, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return s.config.ManageAsync(s.ctx, s.domain) +} + +func (s *Service) Close() error { + if s.cache != nil { + s.cache.Stop() + } + return nil +} + +func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return s.config.GetCertificate(hello) +} + +func (s *Service) GetACMENextProtos() []string { + return s.nextProtos +} + +func newDNSSolver(dnsOptions *option.ACMEProviderDNS01ChallengeOptions, logger *zap.Logger, httpClient *http.Client) (*certmagic.DNS01Solver, error) { + if dnsOptions == nil || dnsOptions.Provider == "" { + return nil, nil + } + if dnsOptions.TTL < 0 { + return nil, E.New("invalid ACME DNS01 ttl: ", dnsOptions.TTL) + } + if dnsOptions.PropagationDelay < 0 { + return nil, E.New("invalid ACME DNS01 propagation_delay: ", dnsOptions.PropagationDelay) + } + if dnsOptions.PropagationTimeout < -1 { + return nil, E.New("invalid ACME DNS01 propagation_timeout: ", dnsOptions.PropagationTimeout) + } + solver := &certmagic.DNS01Solver{ + DNSManager: certmagic.DNSManager{ + TTL: time.Duration(dnsOptions.TTL), + PropagationDelay: time.Duration(dnsOptions.PropagationDelay), + PropagationTimeout: time.Duration(dnsOptions.PropagationTimeout), + Resolvers: dnsOptions.Resolvers, + OverrideDomain: dnsOptions.OverrideDomain, + Logger: logger.Named("dns_manager"), + }, + } + switch dnsOptions.Provider { + case C.DNSProviderAliDNS: + solver.DNSProvider = &alidns.Provider{ + CredentialInfo: alidns.CredentialInfo{ + AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID, + AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret, + RegionID: dnsOptions.AliDNSOptions.RegionID, + SecurityToken: dnsOptions.AliDNSOptions.SecurityToken, + }, + } + case C.DNSProviderCloudflare: + solver.DNSProvider = &cloudflare.Provider{ + APIToken: dnsOptions.CloudflareOptions.APIToken, + ZoneToken: dnsOptions.CloudflareOptions.ZoneToken, + HTTPClient: httpClient, + } + case C.DNSProviderACMEDNS: + solver.DNSProvider = &acmeDNSProvider{ + username: dnsOptions.ACMEDNSOptions.Username, + password: dnsOptions.ACMEDNSOptions.Password, + subdomain: dnsOptions.ACMEDNSOptions.Subdomain, + serverURL: dnsOptions.ACMEDNSOptions.ServerURL, + httpClient: httpClient, + } + default: + return nil, E.New("unsupported ACME DNS01 provider type: ", dnsOptions.Provider) + } + return solver, nil +} + +func createZeroSSLExternalAccountBinding(ctx context.Context, acmeIssuer *certmagic.ACMEIssuer, account acme.Account, httpClient *http.Client) (*acme.EAB, acme.Account, error) { + email := strings.TrimSpace(acmeIssuer.Email) + if email == "" { + return nil, acme.Account{}, E.New("email is required to use the ZeroSSL ACME endpoint without external_account") + } + if len(account.Contact) == 0 { + account.Contact = []string{"mailto:" + email} + } + if acmeIssuer.CertObtainTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, acmeIssuer.CertObtainTimeout) + defer cancel() + } + + form := url.Values{"email": []string{email}} + request, err := http.NewRequestWithContext(ctx, http.MethodPost, zerossl.BaseURL+"/acme/eab-credentials-email", strings.NewReader(form.Encode())) + if err != nil { + return nil, account, E.Cause(err, "create ZeroSSL EAB request") + } + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.Header.Set("User-Agent", certmagic.UserAgent) + + response, err := httpClient.Do(request) + if err != nil { + return nil, account, E.Cause(err, "request ZeroSSL EAB") + } + defer response.Body.Close() + + var result struct { + Success bool `json:"success"` + Error struct { + Code int `json:"code"` + Type string `json:"type"` + } `json:"error"` + EABKID string `json:"eab_kid"` + EABHMACKey string `json:"eab_hmac_key"` + } + err = json.NewDecoder(response.Body).Decode(&result) + if err != nil { + return nil, account, E.Cause(err, "decode ZeroSSL EAB response") + } + if response.StatusCode != http.StatusOK { + return nil, account, E.New("failed getting ZeroSSL EAB credentials: HTTP ", response.StatusCode) + } + if result.Error.Code != 0 { + return nil, account, E.New("failed getting ZeroSSL EAB credentials: ", result.Error.Type, " (code ", result.Error.Code, ")") + } + + acmeIssuer.Logger.Info("generated ZeroSSL EAB credentials", zap.String("key_id", result.EABKID)) + + return &acme.EAB{ + KeyID: result.EABKID, + MACKey: result.EABHMACKey, + }, account, nil +} + +func newACMEHTTPClient(ctx context.Context, detour string) (*http.Client, error) { + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{ + Detour: detour, + }, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "create ACME provider dialer") + } + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: ntp.TimeFuncFromContext(ctx), + }, + // from certmagic defaults (acmeissuer.go) + TLSHandshakeTimeout: 30 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + ExpectContinueTimeout: 2 * time.Second, + ForceAttemptHTTP2: true, + }, + Timeout: certmagic.HTTPTimeout, + }, nil +} + +type acmeDNSProvider struct { + username string + password string + subdomain string + serverURL string + httpClient *http.Client +} + +type acmeDNSRecord struct { + resourceRecord libdns.RR +} + +func (r acmeDNSRecord) RR() libdns.RR { + return r.resourceRecord +} + +func (p *acmeDNSProvider) AppendRecords(ctx context.Context, _ string, records []libdns.Record) ([]libdns.Record, error) { + if p.username == "" { + return nil, E.New("ACME-DNS username cannot be empty") + } + if p.password == "" { + return nil, E.New("ACME-DNS password cannot be empty") + } + if p.subdomain == "" { + return nil, E.New("ACME-DNS subdomain cannot be empty") + } + if p.serverURL == "" { + return nil, E.New("ACME-DNS server_url cannot be empty") + } + appendedRecords := make([]libdns.Record, 0, len(records)) + for _, record := range records { + resourceRecord := record.RR() + if resourceRecord.Type != "TXT" { + return appendedRecords, E.New("ACME-DNS only supports adding TXT records") + } + requestBody, err := json.Marshal(map[string]string{ + "subdomain": p.subdomain, + "txt": resourceRecord.Data, + }) + if err != nil { + return appendedRecords, E.Cause(err, "marshal ACME-DNS update request") + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, p.serverURL+"/update", bytes.NewReader(requestBody)) + if err != nil { + return appendedRecords, E.Cause(err, "create ACME-DNS update request") + } + request.Header.Set("X-Api-User", p.username) + request.Header.Set("X-Api-Key", p.password) + request.Header.Set("Content-Type", "application/json") + response, err := p.httpClient.Do(request) + if err != nil { + return appendedRecords, E.Cause(err, "update ACME-DNS record") + } + _ = response.Body.Close() + if response.StatusCode != http.StatusOK { + return appendedRecords, E.New("update ACME-DNS record: HTTP ", response.StatusCode) + } + appendedRecords = append(appendedRecords, acmeDNSRecord{resourceRecord: libdns.RR{ + Type: "TXT", + Name: resourceRecord.Name, + Data: resourceRecord.Data, + }}) + } + return appendedRecords, nil +} + +func (p *acmeDNSProvider) DeleteRecords(context.Context, string, []libdns.Record) ([]libdns.Record, error) { + return nil, nil +} diff --git a/service/acme/stub.go b/service/acme/stub.go new file mode 100644 index 00000000..43a58d64 --- /dev/null +++ b/service/acme/stub.go @@ -0,0 +1,3 @@ +//go:build !with_acme + +package acme diff --git a/service/ccm/credential.go b/service/ccm/credential.go new file mode 100644 index 00000000..695efc7a --- /dev/null +++ b/service/ccm/credential.go @@ -0,0 +1,139 @@ +package ccm + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "os/user" + "path/filepath" + "time" + + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + oauth2ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" + oauth2TokenURL = "https://console.anthropic.com/v1/oauth/token" + claudeAPIBaseURL = "https://api.anthropic.com" + tokenRefreshBufferMs = 60000 + anthropicBetaOAuthValue = "oauth-2025-04-20" +) + +func getRealUser() (*user.User, error) { + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + sudoUserInfo, err := user.Lookup(sudoUser) + if err == nil { + return sudoUserInfo, nil + } + } + return user.Current() +} + +func getDefaultCredentialsPath() (string, error) { + if configDir := os.Getenv("CLAUDE_CONFIG_DIR"); configDir != "" { + return filepath.Join(configDir, ".credentials.json"), nil + } + userInfo, err := getRealUser() + if err != nil { + return "", err + } + return filepath.Join(userInfo.HomeDir, ".claude", ".credentials.json"), nil +} + +func readCredentialsFromFile(path string) (*oauthCredentials, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var credentialsContainer struct { + ClaudeAIAuth *oauthCredentials `json:"claudeAiOauth,omitempty"` + } + err = json.Unmarshal(data, &credentialsContainer) + if err != nil { + return nil, err + } + if credentialsContainer.ClaudeAIAuth == nil { + return nil, E.New("claudeAiOauth field not found in credentials") + } + return credentialsContainer.ClaudeAIAuth, nil +} + +func writeCredentialsToFile(oauthCredentials *oauthCredentials, path string) error { + data, err := json.MarshalIndent(map[string]any{ + "claudeAiOauth": oauthCredentials, + }, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o600) +} + +type oauthCredentials struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + ExpiresAt int64 `json:"expiresAt"` + Scopes []string `json:"scopes,omitempty"` + SubscriptionType string `json:"subscriptionType,omitempty"` + IsMax bool `json:"isMax,omitempty"` +} + +func (c *oauthCredentials) needsRefresh() bool { + if c.ExpiresAt == 0 { + return false + } + return time.Now().UnixMilli() >= c.ExpiresAt-tokenRefreshBufferMs +} + +func refreshToken(httpClient *http.Client, credentials *oauthCredentials) (*oauthCredentials, error) { + if credentials.RefreshToken == "" { + return nil, E.New("refresh token is empty") + } + + requestBody, err := json.Marshal(map[string]string{ + "grant_type": "refresh_token", + "refresh_token": credentials.RefreshToken, + "client_id": oauth2ClientID, + }) + if err != nil { + return nil, E.Cause(err, "marshal request") + } + + request, err := http.NewRequest("POST", oauth2TokenURL, bytes.NewReader(requestBody)) + if err != nil { + return nil, err + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + + response, err := httpClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return nil, E.New("refresh failed: ", response.Status, " ", string(body)) + } + + var tokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + } + err = json.NewDecoder(response.Body).Decode(&tokenResponse) + if err != nil { + return nil, E.Cause(err, "decode response") + } + + newCredentials := *credentials + newCredentials.AccessToken = tokenResponse.AccessToken + if tokenResponse.RefreshToken != "" { + newCredentials.RefreshToken = tokenResponse.RefreshToken + } + newCredentials.ExpiresAt = time.Now().UnixMilli() + int64(tokenResponse.ExpiresIn)*1000 + + return &newCredentials, nil +} diff --git a/service/ccm/credential_darwin.go b/service/ccm/credential_darwin.go new file mode 100644 index 00000000..24047b85 --- /dev/null +++ b/service/ccm/credential_darwin.go @@ -0,0 +1,116 @@ +//go:build darwin && cgo + +package ccm + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/keybase/go-keychain" +) + +func getKeychainServiceName() string { + configDirectory := os.Getenv("CLAUDE_CONFIG_DIR") + if configDirectory == "" { + return "Claude Code-credentials" + } + + userInfo, err := getRealUser() + if err != nil { + return "Claude Code-credentials" + } + defaultConfigDirectory := filepath.Join(userInfo.HomeDir, ".claude") + if configDirectory == defaultConfigDirectory { + return "Claude Code-credentials" + } + + hash := sha256.Sum256([]byte(configDirectory)) + return "Claude Code-credentials-" + hex.EncodeToString(hash[:])[:8] +} + +func platformReadCredentials(customPath string) (*oauthCredentials, error) { + if customPath != "" { + return readCredentialsFromFile(customPath) + } + + userInfo, err := getRealUser() + if err == nil { + query := keychain.NewItem() + query.SetSecClass(keychain.SecClassGenericPassword) + query.SetService(getKeychainServiceName()) + query.SetAccount(userInfo.Username) + query.SetMatchLimit(keychain.MatchLimitOne) + query.SetReturnData(true) + + results, err := keychain.QueryItem(query) + if err == nil && len(results) == 1 { + var container struct { + ClaudeAIAuth *oauthCredentials `json:"claudeAiOauth,omitempty"` + } + unmarshalErr := json.Unmarshal(results[0].Data, &container) + if unmarshalErr == nil && container.ClaudeAIAuth != nil { + return container.ClaudeAIAuth, nil + } + } + if err != nil && err != keychain.ErrorItemNotFound { + return nil, E.Cause(err, "query keychain") + } + } + + defaultPath, err := getDefaultCredentialsPath() + if err != nil { + return nil, err + } + return readCredentialsFromFile(defaultPath) +} + +func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath string) error { + if customPath != "" { + return writeCredentialsToFile(oauthCredentials, customPath) + } + + userInfo, err := getRealUser() + if err == nil { + data, err := json.Marshal(map[string]any{"claudeAiOauth": oauthCredentials}) + if err == nil { + serviceName := getKeychainServiceName() + item := keychain.NewItem() + item.SetSecClass(keychain.SecClassGenericPassword) + item.SetService(serviceName) + item.SetAccount(userInfo.Username) + item.SetData(data) + item.SetAccessible(keychain.AccessibleWhenUnlocked) + + err = keychain.AddItem(item) + if err == nil { + return nil + } + + if err == keychain.ErrorDuplicateItem { + query := keychain.NewItem() + query.SetSecClass(keychain.SecClassGenericPassword) + query.SetService(serviceName) + query.SetAccount(userInfo.Username) + + updateItem := keychain.NewItem() + updateItem.SetData(data) + + updateErr := keychain.UpdateItem(query, updateItem) + if updateErr == nil { + return nil + } + } + } + } + + defaultPath, err := getDefaultCredentialsPath() + if err != nil { + return err + } + return writeCredentialsToFile(oauthCredentials, defaultPath) +} diff --git a/service/ccm/credential_other.go b/service/ccm/credential_other.go new file mode 100644 index 00000000..11888b50 --- /dev/null +++ b/service/ccm/credential_other.go @@ -0,0 +1,25 @@ +//go:build !darwin + +package ccm + +func platformReadCredentials(customPath string) (*oauthCredentials, error) { + if customPath == "" { + var err error + customPath, err = getDefaultCredentialsPath() + if err != nil { + return nil, err + } + } + return readCredentialsFromFile(customPath) +} + +func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath string) error { + if customPath == "" { + var err error + customPath, err = getDefaultCredentialsPath() + if err != nil { + return err + } + } + return writeCredentialsToFile(oauthCredentials, customPath) +} diff --git a/service/ccm/service.go b/service/ccm/service.go new file mode 100644 index 00000000..34c38824 --- /dev/null +++ b/service/ccm/service.go @@ -0,0 +1,597 @@ +package ccm + +import ( + "bytes" + "context" + stdTLS "crypto/tls" + "encoding/json" + "errors" + "io" + "mime" + "net" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" + aTLS "github.com/sagernet/sing/common/tls" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/go-chi/chi/v5" + "golang.org/x/net/http2" +) + +const ( + contextWindowStandard = 200000 + contextWindowPremium = 1000000 + premiumContextThreshold = 200000 +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.CCMServiceOptions](registry, C.TypeCCM, NewService) +} + +type errorResponse struct { + Type string `json:"type"` + Error errorDetails `json:"error"` + RequestID string `json:"request_id,omitempty"` +} + +type errorDetails struct { + Type string `json:"type"` + Message string `json:"message"` +} + +func writeJSONError(w http.ResponseWriter, r *http.Request, statusCode int, errorType string, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + + json.NewEncoder(w).Encode(errorResponse{ + Type: "error", + Error: errorDetails{ + Type: errorType, + Message: message, + }, + RequestID: r.Header.Get("Request-Id"), + }) +} + +func isHopByHopHeader(header string) bool { + switch strings.ToLower(header) { + case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host": + return true + default: + return false + } +} + +const ( + weeklyWindowSeconds = 604800 + weeklyWindowMinutes = weeklyWindowSeconds / 60 +) + +func parseInt64Header(headers http.Header, headerName string) (int64, bool) { + headerValue := strings.TrimSpace(headers.Get(headerName)) + if headerValue == "" { + return 0, false + } + parsedValue, parseError := strconv.ParseInt(headerValue, 10, 64) + if parseError != nil { + return 0, false + } + return parsedValue, true +} + +func extractWeeklyCycleHint(headers http.Header) *WeeklyCycleHint { + resetAtUnix, hasResetAt := parseInt64Header(headers, "anthropic-ratelimit-unified-7d-reset") + if !hasResetAt || resetAtUnix <= 0 { + return nil + } + + return &WeeklyCycleHint{ + WindowMinutes: weeklyWindowMinutes, + ResetAt: time.Unix(resetAtUnix, 0).UTC(), + } +} + +type Service struct { + boxService.Adapter + ctx context.Context + logger log.ContextLogger + credentialPath string + credentials *oauthCredentials + users []option.CCMUser + httpClient *http.Client + httpHeaders http.Header + listener *listener.Listener + tlsConfig tls.ServerConfig + httpServer *http.Server + userManager *UserManager + accessMutex sync.RWMutex + usageTracker *AggregatedUsage + trackingGroup sync.WaitGroup + shuttingDown bool +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.CCMServiceOptions) (adapter.Service, error) { + serviceDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{ + Detour: options.Detour, + }, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "create dialer") + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSClientConfig: &stdTLS.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: ntp.TimeFuncFromContext(ctx), + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + + userManager := &UserManager{ + tokenMap: make(map[string]string), + } + + var usageTracker *AggregatedUsage + if options.UsagesPath != "" { + usageTracker = &AggregatedUsage{ + LastUpdated: time.Now(), + Combinations: make([]CostCombination, 0), + filePath: options.UsagesPath, + logger: logger, + } + } + + service := &Service{ + Adapter: boxService.NewAdapter(C.TypeCCM, tag), + ctx: ctx, + logger: logger, + credentialPath: options.CredentialPath, + users: options.Users, + httpClient: httpClient, + httpHeaders: options.Headers.Build(), + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + userManager: userManager, + usageTracker: usageTracker, + } + + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + service.tlsConfig = tlsConfig + } + + return service, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + + s.userManager.UpdateUsers(s.users) + + credentials, err := platformReadCredentials(s.credentialPath) + if err != nil { + return E.Cause(err, "read credentials") + } + s.credentials = credentials + + if s.usageTracker != nil { + err = s.usageTracker.Load() + if err != nil { + s.logger.Warn("load usage statistics: ", err) + } + } + + router := chi.NewRouter() + router.Mount("/", s) + + s.httpServer = &http.Server{Handler: router} + + if s.tlsConfig != nil { + err = s.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + + tcpListener, err := s.listener.ListenTCP() + if err != nil { + return err + } + + if s.tlsConfig != nil { + if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig) + } + + go func() { + serveErr := s.httpServer.Serve(tcpListener) + if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { + s.logger.Error("serve error: ", serveErr) + } + }() + + return nil +} + +func (s *Service) getAccessToken() (string, error) { + s.accessMutex.RLock() + if !s.credentials.needsRefresh() { + token := s.credentials.AccessToken + s.accessMutex.RUnlock() + return token, nil + } + s.accessMutex.RUnlock() + + s.accessMutex.Lock() + defer s.accessMutex.Unlock() + + if !s.credentials.needsRefresh() { + return s.credentials.AccessToken, nil + } + + newCredentials, err := refreshToken(s.httpClient, s.credentials) + if err != nil { + return "", err + } + + s.credentials = newCredentials + + err = platformWriteCredentials(newCredentials, s.credentialPath) + if err != nil { + s.logger.Warn("persist refreshed token: ", err) + } + + return newCredentials.AccessToken, nil +} + +func detectContextWindow(betaHeader string, totalInputTokens int64) int { + if totalInputTokens > premiumContextThreshold { + features := strings.Split(betaHeader, ",") + for _, feature := range features { + if strings.HasPrefix(strings.TrimSpace(feature), "context-1m") { + return contextWindowPremium + } + } + } + return contextWindowStandard +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/v1/") { + writeJSONError(w, r, http.StatusNotFound, "not_found_error", "Not found") + return + } + + var username string + if len(s.users) > 0 { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": missing Authorization header") + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "missing api key") + return + } + clientToken := strings.TrimPrefix(authHeader, "Bearer ") + if clientToken == authHeader { + s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": invalid Authorization format") + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key format") + return + } + var ok bool + username, ok = s.userManager.Authenticate(clientToken) + if !ok { + s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": unknown key: ", clientToken) + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key") + return + } + } + + var requestModel string + var messagesCount int + + if s.usageTracker != nil && r.Body != nil { + bodyBytes, err := io.ReadAll(r.Body) + if err == nil { + var request struct { + Model string `json:"model"` + Messages []anthropic.MessageParam `json:"messages"` + } + err := json.Unmarshal(bodyBytes, &request) + if err == nil { + requestModel = request.Model + messagesCount = len(request.Messages) + } + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + } + + accessToken, err := s.getAccessToken() + if err != nil { + s.logger.Error("get access token: ", err) + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "Authentication failed") + return + } + + proxyURL := claudeAPIBaseURL + r.URL.RequestURI() + proxyRequest, err := http.NewRequestWithContext(r.Context(), r.Method, proxyURL, r.Body) + if err != nil { + s.logger.Error("create proxy request: ", err) + writeJSONError(w, r, http.StatusInternalServerError, "api_error", "Internal server error") + return + } + + for key, values := range r.Header { + if !isHopByHopHeader(key) && key != "Authorization" { + proxyRequest.Header[key] = values + } + } + + serviceOverridesAcceptEncoding := len(s.httpHeaders.Values("Accept-Encoding")) > 0 + if s.usageTracker != nil && !serviceOverridesAcceptEncoding { + // Strip Accept-Encoding so Go Transport adds it automatically + // and transparently decompresses the response for correct usage counting. + proxyRequest.Header.Del("Accept-Encoding") + } + + anthropicBetaHeader := proxyRequest.Header.Get("anthropic-beta") + if anthropicBetaHeader != "" { + proxyRequest.Header.Set("anthropic-beta", anthropicBetaOAuthValue+","+anthropicBetaHeader) + } else { + proxyRequest.Header.Set("anthropic-beta", anthropicBetaOAuthValue) + } + + for key, values := range s.httpHeaders { + proxyRequest.Header.Del(key) + proxyRequest.Header[key] = values + } + + proxyRequest.Header.Set("Authorization", "Bearer "+accessToken) + + response, err := s.httpClient.Do(proxyRequest) + if err != nil { + writeJSONError(w, r, http.StatusBadGateway, "api_error", err.Error()) + return + } + defer response.Body.Close() + + for key, values := range response.Header { + if !isHopByHopHeader(key) { + w.Header()[key] = values + } + } + w.WriteHeader(response.StatusCode) + + if s.usageTracker != nil && response.StatusCode == http.StatusOK { + s.handleResponseWithTracking(w, response, requestModel, anthropicBetaHeader, messagesCount, username) + } else { + mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) + if err == nil && mediaType != "text/event-stream" { + _, _ = io.Copy(w, response.Body) + return + } + flusher, ok := w.(http.Flusher) + if !ok { + s.logger.Error("streaming not supported") + return + } + buffer := make([]byte, buf.BufferSize) + for { + n, err := response.Body.Read(buffer) + if n > 0 { + _, writeError := w.Write(buffer[:n]) + if writeError != nil { + s.logger.Error("write streaming response: ", writeError) + return + } + flusher.Flush() + } + if err != nil { + return + } + } + } +} + +func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, response *http.Response, requestModel string, anthropicBetaHeader string, messagesCount int, username string) { + weeklyCycleHint := extractWeeklyCycleHint(response.Header) + mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) + isStreaming := err == nil && mediaType == "text/event-stream" + + if !isStreaming { + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + s.logger.Error("read response body: ", err) + return + } + + var message anthropic.Message + var usage anthropic.Usage + var responseModel string + err = json.Unmarshal(bodyBytes, &message) + if err == nil { + responseModel = string(message.Model) + usage = message.Usage + } + if responseModel == "" { + responseModel = requestModel + } + + if usage.InputTokens > 0 || usage.OutputTokens > 0 { + if responseModel != "" { + totalInputTokens := usage.InputTokens + usage.CacheCreationInputTokens + usage.CacheReadInputTokens + contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + contextWindow, + messagesCount, + usage.InputTokens, + usage.OutputTokens, + usage.CacheReadInputTokens, + usage.CacheCreationInputTokens, + usage.CacheCreation.Ephemeral5mInputTokens, + usage.CacheCreation.Ephemeral1hInputTokens, + username, + time.Now(), + weeklyCycleHint, + ) + } + } + + _, _ = writer.Write(bodyBytes) + return + } + + flusher, ok := writer.(http.Flusher) + if !ok { + s.logger.Error("streaming not supported") + return + } + + var accumulatedUsage anthropic.Usage + var responseModel string + buffer := make([]byte, buf.BufferSize) + var leftover []byte + + for { + n, err := response.Body.Read(buffer) + if n > 0 { + data := append(leftover, buffer[:n]...) + lines := bytes.Split(data, []byte("\n")) + + if err == nil { + leftover = lines[len(lines)-1] + lines = lines[:len(lines)-1] + } else { + leftover = nil + } + + for _, line := range lines { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + + if bytes.HasPrefix(line, []byte("data: ")) { + eventData := bytes.TrimPrefix(line, []byte("data: ")) + if bytes.Equal(eventData, []byte("[DONE]")) { + continue + } + + var event anthropic.MessageStreamEventUnion + err := json.Unmarshal(eventData, &event) + if err != nil { + continue + } + switch event.Type { + case "message_start": + messageStart := event.AsMessageStart() + if messageStart.Message.Model != "" { + responseModel = string(messageStart.Message.Model) + } + if messageStart.Message.Usage.InputTokens > 0 { + accumulatedUsage.InputTokens = messageStart.Message.Usage.InputTokens + accumulatedUsage.CacheReadInputTokens = messageStart.Message.Usage.CacheReadInputTokens + accumulatedUsage.CacheCreationInputTokens = messageStart.Message.Usage.CacheCreationInputTokens + accumulatedUsage.CacheCreation.Ephemeral5mInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral5mInputTokens + accumulatedUsage.CacheCreation.Ephemeral1hInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral1hInputTokens + } + case "message_delta": + messageDelta := event.AsMessageDelta() + if messageDelta.Usage.OutputTokens > 0 { + accumulatedUsage.OutputTokens = messageDelta.Usage.OutputTokens + } + } + } + } + + _, writeError := writer.Write(buffer[:n]) + if writeError != nil { + s.logger.Error("write streaming response: ", writeError) + return + } + flusher.Flush() + } + + if err != nil { + if responseModel == "" { + responseModel = requestModel + } + + if accumulatedUsage.InputTokens > 0 || accumulatedUsage.OutputTokens > 0 { + if responseModel != "" { + totalInputTokens := accumulatedUsage.InputTokens + accumulatedUsage.CacheCreationInputTokens + accumulatedUsage.CacheReadInputTokens + contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + contextWindow, + messagesCount, + accumulatedUsage.InputTokens, + accumulatedUsage.OutputTokens, + accumulatedUsage.CacheReadInputTokens, + accumulatedUsage.CacheCreationInputTokens, + accumulatedUsage.CacheCreation.Ephemeral5mInputTokens, + accumulatedUsage.CacheCreation.Ephemeral1hInputTokens, + username, + time.Now(), + weeklyCycleHint, + ) + } + } + return + } + } +} + +func (s *Service) Close() error { + err := common.Close( + common.PtrOrNil(s.httpServer), + common.PtrOrNil(s.listener), + s.tlsConfig, + ) + + if s.usageTracker != nil { + s.usageTracker.cancelPendingSave() + saveErr := s.usageTracker.Save() + if saveErr != nil { + s.logger.Error("save usage statistics: ", saveErr) + } + } + + return err +} diff --git a/service/ccm/service_usage.go b/service/ccm/service_usage.go new file mode 100644 index 00000000..36e9ee65 --- /dev/null +++ b/service/ccm/service_usage.go @@ -0,0 +1,706 @@ +package ccm + +import ( + "encoding/json" + "fmt" + "math" + "os" + "regexp" + "sync" + "time" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" +) + +type UsageStats struct { + RequestCount int `json:"request_count"` + MessagesCount int `json:"messages_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"` + CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"` +} + +type CostCombination struct { + Model string `json:"model"` + ContextWindow int `json:"context_window"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` + Total UsageStats `json:"total"` + ByUser map[string]UsageStats `json:"by_user"` +} + +type AggregatedUsage struct { + LastUpdated time.Time `json:"last_updated"` + Combinations []CostCombination `json:"combinations"` + mutex sync.Mutex + filePath string + logger log.ContextLogger + lastSaveTime time.Time + pendingSave bool + saveTimer *time.Timer + saveMutex sync.Mutex +} + +type UsageStatsJSON struct { + RequestCount int `json:"request_count"` + MessagesCount int `json:"messages_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"` + CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"` + CostUSD float64 `json:"cost_usd"` +} + +type CostCombinationJSON struct { + Model string `json:"model"` + ContextWindow int `json:"context_window"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` + Total UsageStatsJSON `json:"total"` + ByUser map[string]UsageStatsJSON `json:"by_user"` +} + +type CostsSummaryJSON struct { + TotalUSD float64 `json:"total_usd"` + ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` + ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` +} + +type AggregatedUsageJSON struct { + LastUpdated time.Time `json:"last_updated"` + Costs CostsSummaryJSON `json:"costs"` + Combinations []CostCombinationJSON `json:"combinations"` +} + +type WeeklyCycleHint struct { + WindowMinutes int64 + ResetAt time.Time +} + +type ModelPricing struct { + InputPrice float64 + OutputPrice float64 + CacheReadPrice float64 + CacheWritePrice5Minute float64 + CacheWritePrice1Hour float64 +} + +type modelFamily struct { + pattern *regexp.Regexp + standardPricing ModelPricing + premiumPricing *ModelPricing +} + +var ( + opus46StandardPricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 25.0, + CacheReadPrice: 0.5, + CacheWritePrice5Minute: 6.25, + CacheWritePrice1Hour: 10.0, + } + + opus46PremiumPricing = ModelPricing{ + InputPrice: 10.0, + OutputPrice: 37.5, + CacheReadPrice: 1.0, + CacheWritePrice5Minute: 12.5, + CacheWritePrice1Hour: 20.0, + } + + opus45Pricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 25.0, + CacheReadPrice: 0.5, + CacheWritePrice5Minute: 6.25, + CacheWritePrice1Hour: 10.0, + } + + opus4Pricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 75.0, + CacheReadPrice: 1.5, + CacheWritePrice5Minute: 18.75, + CacheWritePrice1Hour: 30.0, + } + + sonnet46StandardPricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet46PremiumPricing = ModelPricing{ + InputPrice: 6.0, + OutputPrice: 22.5, + CacheReadPrice: 0.6, + CacheWritePrice5Minute: 7.5, + CacheWritePrice1Hour: 12.0, + } + + sonnet45StandardPricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet45PremiumPricing = ModelPricing{ + InputPrice: 6.0, + OutputPrice: 22.5, + CacheReadPrice: 0.6, + CacheWritePrice5Minute: 7.5, + CacheWritePrice1Hour: 12.0, + } + + sonnet4StandardPricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet4PremiumPricing = ModelPricing{ + InputPrice: 6.0, + OutputPrice: 22.5, + CacheReadPrice: 0.6, + CacheWritePrice5Minute: 7.5, + CacheWritePrice1Hour: 12.0, + } + + sonnet37Pricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet35Pricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + haiku45Pricing = ModelPricing{ + InputPrice: 1.0, + OutputPrice: 5.0, + CacheReadPrice: 0.1, + CacheWritePrice5Minute: 1.25, + CacheWritePrice1Hour: 2.0, + } + + haiku4Pricing = ModelPricing{ + InputPrice: 1.0, + OutputPrice: 5.0, + CacheReadPrice: 0.1, + CacheWritePrice5Minute: 1.25, + CacheWritePrice1Hour: 2.0, + } + + haiku35Pricing = ModelPricing{ + InputPrice: 0.8, + OutputPrice: 4.0, + CacheReadPrice: 0.08, + CacheWritePrice5Minute: 1.0, + CacheWritePrice1Hour: 1.6, + } + + haiku3Pricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 1.25, + CacheReadPrice: 0.03, + CacheWritePrice5Minute: 0.3, + CacheWritePrice1Hour: 0.5, + } + + opus3Pricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 75.0, + CacheReadPrice: 1.5, + CacheWritePrice5Minute: 18.75, + CacheWritePrice1Hour: 30.0, + } + + modelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^claude-opus-4-6(?:-|$)`), + standardPricing: opus46StandardPricing, + premiumPricing: &opus46PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^claude-opus-4-5(?:-|$)`), + standardPricing: opus45Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`), + standardPricing: opus4Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`), + standardPricing: opus3Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`), + standardPricing: sonnet46StandardPricing, + premiumPricing: &sonnet46PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`), + standardPricing: sonnet45StandardPricing, + premiumPricing: &sonnet45PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`), + standardPricing: sonnet4StandardPricing, + premiumPricing: &sonnet4PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`), + standardPricing: sonnet37Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`), + standardPricing: sonnet35Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`), + standardPricing: haiku45Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-haiku-4(?:-|$)`), + standardPricing: haiku4Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`), + standardPricing: haiku35Pricing, + premiumPricing: nil, + }, + { + pattern: regexp.MustCompile(`^claude-3-haiku(?:-|$)`), + standardPricing: haiku3Pricing, + premiumPricing: nil, + }, + } +) + +func getPricing(model string, contextWindow int) ModelPricing { + isPremium := contextWindow >= contextWindowPremium + + for _, family := range modelFamilies { + if family.pattern.MatchString(model) { + if isPremium && family.premiumPricing != nil { + return *family.premiumPricing + } + return family.standardPricing + } + } + + return sonnet4StandardPricing +} + +func calculateCost(stats UsageStats, model string, contextWindow int) float64 { + pricing := getPricing(model, contextWindow) + + cacheCreationCost := 0.0 + if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 { + cacheCreationCost = float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute + + float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour + } else { + // Backward compatibility for usage files generated before TTL split tracking. + cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute + } + + cost := (float64(stats.InputTokens)*pricing.InputPrice + + float64(stats.OutputTokens)*pricing.OutputPrice + + float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice + + cacheCreationCost) / 1_000_000 + + return math.Round(cost*100) / 100 +} + +func roundCost(cost float64) float64 { + return math.Round(cost*100) / 100 +} + +func normalizeCombinations(combinations []CostCombination) { + for index := range combinations { + if combinations[index].ByUser == nil { + combinations[index].ByUser = make(map[string]UsageStats) + } + } +} + +func addUsageToCombinations( + combinations *[]CostCombination, + model string, + contextWindow int, + weekStartUnix int64, + messagesCount int, + inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, + user string, +) { + var matchedCombination *CostCombination + for index := range *combinations { + combination := &(*combinations)[index] + if combination.Model == model && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix { + matchedCombination = combination + break + } + } + + if matchedCombination == nil { + newCombination := CostCombination{ + Model: model, + ContextWindow: contextWindow, + WeekStartUnix: weekStartUnix, + Total: UsageStats{}, + ByUser: make(map[string]UsageStats), + } + *combinations = append(*combinations, newCombination) + matchedCombination = &(*combinations)[len(*combinations)-1] + } + + if cacheCreationTokens == 0 { + cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens + } + + matchedCombination.Total.RequestCount++ + matchedCombination.Total.MessagesCount += messagesCount + matchedCombination.Total.InputTokens += inputTokens + matchedCombination.Total.OutputTokens += outputTokens + matchedCombination.Total.CacheReadInputTokens += cacheReadTokens + matchedCombination.Total.CacheCreationInputTokens += cacheCreationTokens + matchedCombination.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens + matchedCombination.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens + + if user != "" { + userStats := matchedCombination.ByUser[user] + userStats.RequestCount++ + userStats.MessagesCount += messagesCount + userStats.InputTokens += inputTokens + userStats.OutputTokens += outputTokens + userStats.CacheReadInputTokens += cacheReadTokens + userStats.CacheCreationInputTokens += cacheCreationTokens + userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens + userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens + matchedCombination.ByUser[user] = userStats + } +} + +func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) { + result := make([]CostCombinationJSON, len(combinations)) + var totalCost float64 + + for index, combination := range combinations { + combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ContextWindow) + totalCost += combinationTotalCost + + combinationJSON := CostCombinationJSON{ + Model: combination.Model, + ContextWindow: combination.ContextWindow, + WeekStartUnix: combination.WeekStartUnix, + Total: UsageStatsJSON{ + RequestCount: combination.Total.RequestCount, + MessagesCount: combination.Total.MessagesCount, + InputTokens: combination.Total.InputTokens, + OutputTokens: combination.Total.OutputTokens, + CacheReadInputTokens: combination.Total.CacheReadInputTokens, + CacheCreationInputTokens: combination.Total.CacheCreationInputTokens, + CacheCreation5MinuteInputTokens: combination.Total.CacheCreation5MinuteInputTokens, + CacheCreation1HourInputTokens: combination.Total.CacheCreation1HourInputTokens, + CostUSD: combinationTotalCost, + }, + ByUser: make(map[string]UsageStatsJSON), + } + + for user, userStats := range combination.ByUser { + userCost := calculateCost(userStats, combination.Model, combination.ContextWindow) + if aggregateUserCosts != nil { + aggregateUserCosts[user] += userCost + } + + combinationJSON.ByUser[user] = UsageStatsJSON{ + RequestCount: userStats.RequestCount, + MessagesCount: userStats.MessagesCount, + InputTokens: userStats.InputTokens, + OutputTokens: userStats.OutputTokens, + CacheReadInputTokens: userStats.CacheReadInputTokens, + CacheCreationInputTokens: userStats.CacheCreationInputTokens, + CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens, + CacheCreation1HourInputTokens: userStats.CacheCreation1HourInputTokens, + CostUSD: userCost, + } + } + + result[index] = combinationJSON + } + + return result, roundCost(totalCost) +} + +func formatUTCOffsetLabel(timestamp time.Time) string { + _, offsetSeconds := timestamp.Zone() + sign := "+" + if offsetSeconds < 0 { + sign = "-" + offsetSeconds = -offsetSeconds + } + offsetHours := offsetSeconds / 3600 + offsetMinutes := (offsetSeconds % 3600) / 60 + if offsetMinutes == 0 { + return fmt.Sprintf("UTC%s%d", sign, offsetHours) + } + return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes) +} + +func formatWeekStartKey(cycleStartAt time.Time) string { + localCycleStart := cycleStartAt.In(time.Local) + return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart)) +} + +func buildByWeekCost(combinations []CostCombination) map[string]float64 { + byWeek := make(map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ContextWindow) + } + for weekKey, weekCost := range byWeek { + byWeek[weekKey] = roundCost(weekCost) + } + return byWeek +} + +func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { + byUserAndWeek := make(map[string]map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + for user, userStats := range combination.ByUser { + userWeeks, exists := byUserAndWeek[user] + if !exists { + userWeeks = make(map[string]float64) + byUserAndWeek[user] = userWeeks + } + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow) + } + } + for _, weekCosts := range byUserAndWeek { + for weekKey, cost := range weekCosts { + weekCosts[weekKey] = roundCost(cost) + } + } + return byUserAndWeek +} + +func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { + if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { + return 0 + } + windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute + return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix() +} + +func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { + u.mutex.Lock() + defer u.mutex.Unlock() + + result := &AggregatedUsageJSON{ + LastUpdated: u.LastUpdated, + Costs: CostsSummaryJSON{ + TotalUSD: 0, + ByUser: make(map[string]float64), + ByWeek: make(map[string]float64), + }, + } + + globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser) + result.Combinations = globalCombinationsJSON + result.Costs.TotalUSD = totalCost + result.Costs.ByWeek = buildByWeekCost(u.Combinations) + + if len(result.Costs.ByWeek) == 0 { + result.Costs.ByWeek = nil + } + + result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) + if len(result.Costs.ByUserAndWeek) == 0 { + result.Costs.ByUserAndWeek = nil + } + + for user, cost := range result.Costs.ByUser { + result.Costs.ByUser[user] = roundCost(cost) + } + + return result +} + +func (u *AggregatedUsage) Load() error { + u.mutex.Lock() + defer u.mutex.Unlock() + + u.LastUpdated = time.Time{} + u.Combinations = nil + + data, err := os.ReadFile(u.filePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + var temp struct { + LastUpdated time.Time `json:"last_updated"` + Combinations []CostCombination `json:"combinations"` + } + + err = json.Unmarshal(data, &temp) + if err != nil { + return err + } + + u.LastUpdated = temp.LastUpdated + u.Combinations = temp.Combinations + normalizeCombinations(u.Combinations) + + return nil +} + +func (u *AggregatedUsage) Save() error { + jsonData := u.ToJSON() + + data, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + return err + } + + tmpFile := u.filePath + ".tmp" + err = os.WriteFile(tmpFile, data, 0o644) + if err != nil { + return err + } + defer os.Remove(tmpFile) + err = os.Rename(tmpFile, u.filePath) + if err == nil { + u.saveMutex.Lock() + u.lastSaveTime = time.Now() + u.saveMutex.Unlock() + } + return err +} + +func (u *AggregatedUsage) AddUsage( + model string, + contextWindow int, + messagesCount int, + inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, + user string, +) error { + return u.AddUsageWithCycleHint(model, contextWindow, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user, time.Now(), nil) +} + +func (u *AggregatedUsage) AddUsageWithCycleHint( + model string, + contextWindow int, + messagesCount int, + inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, + user string, + observedAt time.Time, + cycleHint *WeeklyCycleHint, +) error { + if model == "" { + return E.New("model cannot be empty") + } + if contextWindow <= 0 { + return E.New("contextWindow must be positive") + } + if observedAt.IsZero() { + observedAt = time.Now() + } + + u.mutex.Lock() + defer u.mutex.Unlock() + + u.LastUpdated = observedAt + weekStartUnix := deriveWeekStartUnix(cycleHint) + + addUsageToCombinations(&u.Combinations, model, contextWindow, weekStartUnix, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user) + + go u.scheduleSave() + + return nil +} + +func (u *AggregatedUsage) scheduleSave() { + const saveInterval = time.Minute + + u.saveMutex.Lock() + defer u.saveMutex.Unlock() + + timeSinceLastSave := time.Since(u.lastSaveTime) + + if timeSinceLastSave >= saveInterval { + go u.saveAsync() + return + } + + if u.pendingSave { + return + } + + u.pendingSave = true + remainingTime := saveInterval - timeSinceLastSave + + u.saveTimer = time.AfterFunc(remainingTime, func() { + u.saveMutex.Lock() + u.pendingSave = false + u.saveMutex.Unlock() + u.saveAsync() + }) +} + +func (u *AggregatedUsage) saveAsync() { + err := u.Save() + if err != nil { + if u.logger != nil { + u.logger.Error("save usage statistics: ", err) + } + } +} + +func (u *AggregatedUsage) cancelPendingSave() { + u.saveMutex.Lock() + defer u.saveMutex.Unlock() + + if u.saveTimer != nil { + u.saveTimer.Stop() + u.saveTimer = nil + } + u.pendingSave = false +} diff --git a/service/ccm/service_user.go b/service/ccm/service_user.go new file mode 100644 index 00000000..94637ed8 --- /dev/null +++ b/service/ccm/service_user.go @@ -0,0 +1,29 @@ +package ccm + +import ( + "sync" + + "github.com/sagernet/sing-box/option" +) + +type UserManager struct { + accessMutex sync.RWMutex + tokenMap map[string]string +} + +func (m *UserManager) UpdateUsers(users []option.CCMUser) { + m.accessMutex.Lock() + defer m.accessMutex.Unlock() + tokenMap := make(map[string]string, len(users)) + for _, user := range users { + tokenMap[user.Token] = user.Name + } + m.tokenMap = tokenMap +} + +func (m *UserManager) Authenticate(token string) (string, bool) { + m.accessMutex.RLock() + username, found := m.tokenMap[token] + m.accessMutex.RUnlock() + return username, found +} diff --git a/service/derp/service.go b/service/derp/service.go new file mode 100644 index 00000000..02dac60b --- /dev/null +++ b/service/derp/service.go @@ -0,0 +1,529 @@ +//go:build with_gvisor + +package derp + +import ( + "bufio" + "context" + stdTLS "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + boxScale "github.com/sagernet/sing-box/protocol/tailscale" + "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/badoption" + "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" + aTLS "github.com/sagernet/sing/common/tls" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" + "github.com/sagernet/tailscale/client/local" + "github.com/sagernet/tailscale/derp" + "github.com/sagernet/tailscale/derp/derphttp" + "github.com/sagernet/tailscale/derp/derpserver" + "github.com/sagernet/tailscale/net/netmon" + "github.com/sagernet/tailscale/net/stun" + "github.com/sagernet/tailscale/net/wsconn" + "github.com/sagernet/tailscale/tsweb" + "github.com/sagernet/tailscale/types/key" + + "github.com/coder/websocket" + "github.com/go-chi/render" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func Register(registry *boxService.Registry) { + boxService.Register[option.DERPServiceOptions](registry, C.TypeDERP, NewService) +} + +type Service struct { + boxService.Adapter + ctx context.Context + logger logger.ContextLogger + listener *listener.Listener + stunListener *listener.Listener + tlsConfig tls.ServerConfig + server *derpserver.Server + configPath string + verifyClientEndpoint []string + verifyClientURL []*option.DERPVerifyClientURLOptions + home string + meshKey string + meshKeyPath string + meshWith []*option.DERPMeshOptions +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) { + if options.TLS == nil || !options.TLS.Enabled { + return nil, E.New("TLS is required for DERP server") + } + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + + var configPath string + if options.ConfigPath != "" { + configPath = filemanager.BasePath(ctx, os.ExpandEnv(options.ConfigPath)) + } else { + return nil, E.New("missing config_path") + } + + if options.MeshPSK != "" { + err = checkMeshKey(options.MeshPSK) + if err != nil { + return nil, E.Cause(err, "invalid mesh_psk") + } + } + + var stunListener *listener.Listener + if options.STUN != nil && options.STUN.Enabled { + if options.STUN.Listen == nil { + options.STUN.Listen = (*badoption.Addr)(common.Ptr(netip.IPv6Unspecified())) + } + if options.STUN.ListenPort == 0 { + options.STUN.ListenPort = 3478 + } + stunListener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkUDP}, + Listen: options.STUN.ListenOptions, + }) + } + + return &Service{ + Adapter: boxService.NewAdapter(C.TypeDERP, tag), + ctx: ctx, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + stunListener: stunListener, + tlsConfig: tlsConfig, + configPath: configPath, + verifyClientEndpoint: options.VerifyClientEndpoint, + verifyClientURL: options.VerifyClientURL, + home: options.Home, + meshKey: options.MeshPSK, + meshKeyPath: options.MeshPSKFile, + meshWith: options.MeshWith, + }, nil +} + +func (d *Service) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + config, err := readDERPConfig(filemanager.BasePath(d.ctx, d.configPath)) + if err != nil { + return err + } + + server := derpserver.New(config.PrivateKey, func(format string, args ...any) { + d.logger.Debug(fmt.Sprintf(format, args...)) + }) + + if len(d.verifyClientURL) > 0 { + var httpClients []*http.Client + var urls []string + for index, options := range d.verifyClientURL { + verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{ + Context: d.ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + NewDialer: true, + }) + if createErr != nil { + return E.Cause(createErr, "verify_client_url[", index, "]") + } + httpClients = append(httpClients, &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSClientConfig: &stdTLS.Config{ + RootCAs: adapter.RootPoolFromContext(d.ctx), + Time: ntp.TimeFuncFromContext(d.ctx), + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + }) + urls = append(urls, options.URL) + } + server.SetVerifyClientHTTPClient(httpClients) + server.SetVerifyClientURL(urls) + } + + if d.meshKey != "" { + server.SetMeshKey(d.meshKey) + } else if d.meshKeyPath != "" { + var meshKeyContent []byte + meshKeyContent, err = os.ReadFile(d.meshKeyPath) + if err != nil { + return err + } + err = checkMeshKey(string(meshKeyContent)) + if err != nil { + return E.Cause(err, "invalid mesh_psk_path file") + } + server.SetMeshKey(string(meshKeyContent)) + } + d.server = server + + derpMux := http.NewServeMux() + derpHandler := derpserver.Handler(server) + derpHandler = addWebSocketSupport(server, derpHandler) + derpMux.Handle("/derp", derpHandler) + + homeHandler, ok := getHomeHandler(d.home) + if !ok { + return E.New("invalid home value: ", d.home) + } + + derpMux.HandleFunc("/derp/probe", derpserver.ProbeHandler) + derpMux.HandleFunc("/derp/latency-check", derpserver.ProbeHandler) + derpMux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS(d.ctx))) + derpMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tsweb.AddBrowserHeaders(w) + homeHandler.ServeHTTP(w, r) + })) + derpMux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tsweb.AddBrowserHeaders(w) + io.WriteString(w, "User-agent: *\nDisallow: /\n") + })) + derpMux.Handle("/generate_204", http.HandlerFunc(derpserver.ServeNoContent)) + + err = d.tlsConfig.Start() + if err != nil { + return err + } + + tcpListener, err := d.listener.ListenTCP() + if err != nil { + return err + } + if len(d.tlsConfig.NextProtos()) == 0 { + d.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) + } else if !common.Contains(d.tlsConfig.NextProtos(), http2.NextProtoTLS) { + d.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, d.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, d.tlsConfig) + httpServer := &http.Server{ + Handler: h2c.NewHandler(derpMux, &http2.Server{}), + } + go httpServer.Serve(tcpListener) + + if d.stunListener != nil { + stunConn, err := d.stunListener.ListenUDP() + if err != nil { + return err + } + go d.loopSTUNPacket(stunConn.(*net.UDPConn)) + } + case adapter.StartStatePostStart: + if len(d.verifyClientEndpoint) > 0 { + var endpoints []*local.Client + endpointManager := service.FromContext[adapter.EndpointManager](d.ctx) + for _, endpointTag := range d.verifyClientEndpoint { + endpoint, loaded := endpointManager.Get(endpointTag) + if !loaded { + return E.New("verify_client_endpoint: endpoint not found: ", endpointTag) + } + tsEndpoint, isTailscale := endpoint.(*boxScale.Endpoint) + if !isTailscale { + return E.New("verify_client_endpoint: endpoint is not Tailscale: ", endpointTag) + } + localClient, err := tsEndpoint.Server().LocalClient() + if err != nil { + return err + } + endpoints = append(endpoints, localClient) + } + d.server.SetVerifyClientLocalClient(endpoints) + } + if len(d.meshWith) > 0 { + if !d.server.HasMeshKey() { + return E.New("missing mesh psk") + } + for _, options := range d.meshWith { + err := d.startMeshWithHost(d.server, options) + if err != nil { + return err + } + } + } + } + return nil +} + +func checkMeshKey(meshKey string) error { + checkRegex, err := regexp.Compile(`^[0-9a-f]{64}$`) + if err != nil { + return err + } + if !checkRegex.MatchString(meshKey) { + return E.New("key must contain exactly 64 hex digits") + } + return nil +} + +func (d *Service) startMeshWithHost(derpServer *derpserver.Server, server *option.DERPMeshOptions) error { + meshDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: d.ctx, + Options: server.DialerOptions, + RemoteIsDomain: server.ServerIsDomain(), + NewDialer: true, + }) + if err != nil { + return err + } + var hostname string + if server.Host != "" { + hostname = server.Host + } else { + hostname = server.Server + } + var stdConfig *tls.STDConfig + if server.TLS != nil && server.TLS.Enabled { + tlsConfig, err := tls.NewClient(d.ctx, d.logger, hostname, common.PtrValueOrDefault(server.TLS)) + if err != nil { + return err + } + stdConfig, err = tlsConfig.STDConfig() + if err != nil { + return err + } + } + logf := func(format string, args ...any) { + d.logger.Debug(F.ToString("mesh(", hostname, "): ", fmt.Sprintf(format, args...))) + } + var meshHost string + if server.ServerPort == 0 || server.ServerPort == 443 { + meshHost = hostname + } else { + meshHost = M.ParseSocksaddrHostPort(hostname, server.ServerPort).String() + } + var serverURL string + if stdConfig != nil { + serverURL = "https://" + meshHost + "/derp" + } else { + serverURL = "http://" + meshHost + "/derp" + } + meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), serverURL, logf, netmon.NewStatic()) + if err != nil { + return err + } + meshClient.TLSConfig = stdConfig + meshClient.MeshKey = derpServer.MeshKey() + meshClient.WatchConnectionChanges = true + meshClient.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) { + return meshDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }) + add := func(m derp.PeerPresentMessage) { derpServer.AddPacketForwarder(m.Key, meshClient) } + remove := func(m derp.PeerGoneMessage) { derpServer.RemovePacketForwarder(m.Peer, meshClient) } + notifyError := func(err error) { d.logger.Error(err) } + go meshClient.RunWatchConnectionLoop(context.Background(), derpServer.PublicKey(), logf, add, remove, notifyError) + return nil +} + +func (d *Service) Close() error { + return common.Close( + common.PtrOrNil(d.listener), + d.tlsConfig, + ) +} + +var homePage = ` +

DERP

+

+ This is a Tailscale DERP server. +

+ +

+ It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic + for Tailscale clients. +

+ +

+ Documentation: +

+ +
    + +
  • About DERP
  • +
  • Protocol & Go docs
  • +
  • How to run a DERP server
  • + + + +` + +func getHomeHandler(val string) (_ http.Handler, ok bool) { + if val == "" { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(200) + w.Write([]byte(homePage)) + }), true + } + if val == "blank" { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(200) + }), true + } + if strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://") { + return http.RedirectHandler(val, http.StatusFound), true + } + return nil, false +} + +func addWebSocketSupport(s *derpserver.Server, base http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + up := strings.ToLower(r.Header.Get("Upgrade")) + + // Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually + // speak WebSockets (they still assumed DERP's binary framing). So to distinguish + // clients that actually want WebSockets, look for an explicit "derp" subprotocol. + if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") { + base.ServeHTTP(w, r) + return + } + + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"derp"}, + OriginPatterns: []string{"*"}, + // Disable compression because we transmit WireGuard messages that + // are not compressible. + // Additionally, Safari has a broken implementation of compression + // (see https://github.com/nhooyr/websocket/issues/218) that makes + // enabling it actively harmful. + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + return + } + defer c.Close(websocket.StatusInternalError, "closing") + if c.Subprotocol() != "derp" { + c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol") + return + } + wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary, r.RemoteAddr) + brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc)) + s.Accept(r.Context(), wc, brw, r.RemoteAddr) + }) +} + +func handleBootstrapDNS(ctx context.Context) http.HandlerFunc { + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Connection", "close") + if queryDomain := r.URL.Query().Get("q"); queryDomain != "" { + addresses, err := dnsRouter.Lookup(ctx, queryDomain, adapter.DNSQueryOptions{}) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + render.JSON(w, r, render.M{ + queryDomain: addresses, + }) + return + } + w.Write([]byte("{}")) + } +} + +type derpConfig struct { + PrivateKey key.NodePrivate +} + +func readDERPConfig(path string) (*derpConfig, error) { + content, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return writeNewDERPConfig(path) + } + return nil, err + } + var config derpConfig + err = json.Unmarshal(content, &config) + if err != nil { + return nil, err + } + return &config, nil +} + +func writeNewDERPConfig(path string) (*derpConfig, error) { + newKey := key.NewNode() + err := os.MkdirAll(filepath.Dir(path), 0o777) + if err != nil { + return nil, err + } + config := derpConfig{ + PrivateKey: newKey, + } + content, err := json.Marshal(config) + if err != nil { + return nil, err + } + err = os.WriteFile(path, content, 0o644) + if err != nil { + return nil, err + } + return &config, nil +} + +func (d *Service) loopSTUNPacket(packetConn *net.UDPConn) { + buffer := make([]byte, 65535) + oob := make([]byte, 1024) + var ( + n int + oobN int + addrPort netip.AddrPort + err error + ) + for { + n, oobN, _, addrPort, err = packetConn.ReadMsgUDPAddrPort(buffer, oob) + if err != nil { + if E.IsClosedOrCanceled(err) { + return + } + time.Sleep(time.Second) + continue + } + if !stun.Is(buffer[:n]) { + continue + } + txid, err := stun.ParseBindingRequest(buffer[:n]) + if err != nil { + continue + } + packetConn.WriteMsgUDPAddrPort(stun.Response(txid, addrPort), oob[:oobN], addrPort) + } +} diff --git a/service/ocm/credential.go b/service/ocm/credential.go new file mode 100644 index 00000000..76651a8e --- /dev/null +++ b/service/ocm/credential.go @@ -0,0 +1,173 @@ +package ocm + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "os/user" + "path/filepath" + "time" + + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + oauth2ClientID = "app_EMoamEEZ73f0CkXaXp7hrann" + oauth2TokenURL = "https://auth.openai.com/oauth/token" + openaiAPIBaseURL = "https://api.openai.com" + chatGPTBackendURL = "https://chatgpt.com/backend-api/codex" + tokenRefreshIntervalDays = 8 +) + +func getRealUser() (*user.User, error) { + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + sudoUserInfo, err := user.Lookup(sudoUser) + if err == nil { + return sudoUserInfo, nil + } + } + return user.Current() +} + +func getDefaultCredentialsPath() (string, error) { + if codexHome := os.Getenv("CODEX_HOME"); codexHome != "" { + return filepath.Join(codexHome, "auth.json"), nil + } + userInfo, err := getRealUser() + if err != nil { + return "", err + } + return filepath.Join(userInfo.HomeDir, ".codex", "auth.json"), nil +} + +func readCredentialsFromFile(path string) (*oauthCredentials, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var credentials oauthCredentials + err = json.Unmarshal(data, &credentials) + if err != nil { + return nil, err + } + return &credentials, nil +} + +func writeCredentialsToFile(credentials *oauthCredentials, path string) error { + data, err := json.MarshalIndent(credentials, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o600) +} + +type oauthCredentials struct { + APIKey string `json:"OPENAI_API_KEY,omitempty"` + Tokens *tokenData `json:"tokens,omitempty"` + LastRefresh *time.Time `json:"last_refresh,omitempty"` +} + +type tokenData struct { + IDToken string `json:"id_token,omitempty"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + AccountID string `json:"account_id,omitempty"` +} + +func (c *oauthCredentials) isAPIKeyMode() bool { + return c.APIKey != "" +} + +func (c *oauthCredentials) getAccessToken() string { + if c.APIKey != "" { + return c.APIKey + } + if c.Tokens != nil { + return c.Tokens.AccessToken + } + return "" +} + +func (c *oauthCredentials) getAccountID() string { + if c.Tokens != nil { + return c.Tokens.AccountID + } + return "" +} + +func (c *oauthCredentials) needsRefresh() bool { + if c.APIKey != "" { + return false + } + if c.Tokens == nil || c.Tokens.RefreshToken == "" { + return false + } + if c.LastRefresh == nil { + return true + } + return time.Since(*c.LastRefresh) >= time.Duration(tokenRefreshIntervalDays)*24*time.Hour +} + +func refreshToken(httpClient *http.Client, credentials *oauthCredentials) (*oauthCredentials, error) { + if credentials.Tokens == nil || credentials.Tokens.RefreshToken == "" { + return nil, E.New("refresh token is empty") + } + + requestBody, err := json.Marshal(map[string]string{ + "grant_type": "refresh_token", + "refresh_token": credentials.Tokens.RefreshToken, + "client_id": oauth2ClientID, + "scope": "openid profile email", + }) + if err != nil { + return nil, E.Cause(err, "marshal request") + } + + request, err := http.NewRequest("POST", oauth2TokenURL, bytes.NewReader(requestBody)) + if err != nil { + return nil, err + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + + response, err := httpClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return nil, E.New("refresh failed: ", response.Status, " ", string(body)) + } + + var tokenResponse struct { + IDToken string `json:"id_token"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + } + err = json.NewDecoder(response.Body).Decode(&tokenResponse) + if err != nil { + return nil, E.Cause(err, "decode response") + } + + newCredentials := *credentials + if newCredentials.Tokens == nil { + newCredentials.Tokens = &tokenData{} + } + if tokenResponse.IDToken != "" { + newCredentials.Tokens.IDToken = tokenResponse.IDToken + } + if tokenResponse.AccessToken != "" { + newCredentials.Tokens.AccessToken = tokenResponse.AccessToken + } + if tokenResponse.RefreshToken != "" { + newCredentials.Tokens.RefreshToken = tokenResponse.RefreshToken + } + now := time.Now() + newCredentials.LastRefresh = &now + + return &newCredentials, nil +} diff --git a/service/ocm/credential_darwin.go b/service/ocm/credential_darwin.go new file mode 100644 index 00000000..f3da2a63 --- /dev/null +++ b/service/ocm/credential_darwin.go @@ -0,0 +1,25 @@ +//go:build darwin + +package ocm + +func platformReadCredentials(customPath string) (*oauthCredentials, error) { + if customPath == "" { + var err error + customPath, err = getDefaultCredentialsPath() + if err != nil { + return nil, err + } + } + return readCredentialsFromFile(customPath) +} + +func platformWriteCredentials(credentials *oauthCredentials, customPath string) error { + if customPath == "" { + var err error + customPath, err = getDefaultCredentialsPath() + if err != nil { + return err + } + } + return writeCredentialsToFile(credentials, customPath) +} diff --git a/service/ocm/credential_other.go b/service/ocm/credential_other.go new file mode 100644 index 00000000..22dfd033 --- /dev/null +++ b/service/ocm/credential_other.go @@ -0,0 +1,25 @@ +//go:build !darwin + +package ocm + +func platformReadCredentials(customPath string) (*oauthCredentials, error) { + if customPath == "" { + var err error + customPath, err = getDefaultCredentialsPath() + if err != nil { + return nil, err + } + } + return readCredentialsFromFile(customPath) +} + +func platformWriteCredentials(credentials *oauthCredentials, customPath string) error { + if customPath == "" { + var err error + customPath, err = getDefaultCredentialsPath() + if err != nil { + return err + } + } + return writeCredentialsToFile(credentials, customPath) +} diff --git a/service/ocm/service.go b/service/ocm/service.go new file mode 100644 index 00000000..8b66964a --- /dev/null +++ b/service/ocm/service.go @@ -0,0 +1,707 @@ +package ocm + +import ( + "bytes" + "context" + stdTLS "crypto/tls" + "encoding/json" + "errors" + "io" + "mime" + "net" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" + aTLS "github.com/sagernet/sing/common/tls" + + "github.com/go-chi/chi/v5" + "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/responses" + "golang.org/x/net/http2" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.OCMServiceOptions](registry, C.TypeOCM, NewService) +} + +type errorResponse struct { + Error errorDetails `json:"error"` +} + +type errorDetails struct { + Type string `json:"type"` + Code string `json:"code,omitempty"` + Message string `json:"message"` +} + +func writeJSONError(w http.ResponseWriter, r *http.Request, statusCode int, errorType string, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + + json.NewEncoder(w).Encode(errorResponse{ + Error: errorDetails{ + Type: errorType, + Message: message, + }, + }) +} + +func isHopByHopHeader(header string) bool { + switch strings.ToLower(header) { + case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host": + return true + default: + return false + } +} + +func normalizeRateLimitIdentifier(limitIdentifier string) string { + trimmedIdentifier := strings.TrimSpace(strings.ToLower(limitIdentifier)) + if trimmedIdentifier == "" { + return "" + } + return strings.ReplaceAll(trimmedIdentifier, "_", "-") +} + +func parseInt64Header(headers http.Header, headerName string) (int64, bool) { + headerValue := strings.TrimSpace(headers.Get(headerName)) + if headerValue == "" { + return 0, false + } + parsedValue, parseError := strconv.ParseInt(headerValue, 10, 64) + if parseError != nil { + return 0, false + } + return parsedValue, true +} + +func weeklyCycleHintForLimit(headers http.Header, limitIdentifier string) *WeeklyCycleHint { + normalizedLimitIdentifier := normalizeRateLimitIdentifier(limitIdentifier) + if normalizedLimitIdentifier == "" { + return nil + } + + windowHeader := "x-" + normalizedLimitIdentifier + "-secondary-window-minutes" + resetHeader := "x-" + normalizedLimitIdentifier + "-secondary-reset-at" + + windowMinutes, hasWindowMinutes := parseInt64Header(headers, windowHeader) + resetAtUnix, hasResetAt := parseInt64Header(headers, resetHeader) + if !hasWindowMinutes || !hasResetAt || windowMinutes <= 0 || resetAtUnix <= 0 { + return nil + } + + return &WeeklyCycleHint{ + WindowMinutes: windowMinutes, + ResetAt: time.Unix(resetAtUnix, 0).UTC(), + } +} + +func extractWeeklyCycleHint(headers http.Header) *WeeklyCycleHint { + activeLimitIdentifier := normalizeRateLimitIdentifier(headers.Get("x-codex-active-limit")) + if activeLimitIdentifier != "" { + if activeHint := weeklyCycleHintForLimit(headers, activeLimitIdentifier); activeHint != nil { + return activeHint + } + } + return weeklyCycleHintForLimit(headers, "codex") +} + +type Service struct { + boxService.Adapter + ctx context.Context + logger log.ContextLogger + credentialPath string + credentials *oauthCredentials + users []option.OCMUser + dialer N.Dialer + httpClient *http.Client + httpHeaders http.Header + listener *listener.Listener + tlsConfig tls.ServerConfig + httpServer *http.Server + userManager *UserManager + accessMutex sync.RWMutex + usageTracker *AggregatedUsage + webSocketMutex sync.Mutex + webSocketGroup sync.WaitGroup + webSocketConns map[*webSocketSession]struct{} + shuttingDown bool +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OCMServiceOptions) (adapter.Service, error) { + serviceDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{ + Detour: options.Detour, + }, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "create dialer") + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSClientConfig: &stdTLS.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: ntp.TimeFuncFromContext(ctx), + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + + userManager := &UserManager{ + tokenMap: make(map[string]string), + } + + var usageTracker *AggregatedUsage + if options.UsagesPath != "" { + usageTracker = &AggregatedUsage{ + LastUpdated: time.Now(), + Combinations: make([]CostCombination, 0), + filePath: options.UsagesPath, + logger: logger, + } + } + + service := &Service{ + Adapter: boxService.NewAdapter(C.TypeOCM, tag), + ctx: ctx, + logger: logger, + credentialPath: options.CredentialPath, + users: options.Users, + dialer: serviceDialer, + httpClient: httpClient, + httpHeaders: options.Headers.Build(), + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + userManager: userManager, + usageTracker: usageTracker, + webSocketConns: make(map[*webSocketSession]struct{}), + } + + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + service.tlsConfig = tlsConfig + } + + return service, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + + s.userManager.UpdateUsers(s.users) + + credentials, err := platformReadCredentials(s.credentialPath) + if err != nil { + return E.Cause(err, "read credentials") + } + s.credentials = credentials + + if s.usageTracker != nil { + err = s.usageTracker.Load() + if err != nil { + s.logger.Warn("load usage statistics: ", err) + } + } + + router := chi.NewRouter() + router.Mount("/", s) + + s.httpServer = &http.Server{Handler: router} + + if s.tlsConfig != nil { + err = s.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + + tcpListener, err := s.listener.ListenTCP() + if err != nil { + return err + } + + if s.tlsConfig != nil { + if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig) + } + + go func() { + serveErr := s.httpServer.Serve(tcpListener) + if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { + s.logger.Error("serve error: ", serveErr) + } + }() + + return nil +} + +func (s *Service) getAccessToken() (string, error) { + s.accessMutex.RLock() + if !s.credentials.needsRefresh() { + token := s.credentials.getAccessToken() + s.accessMutex.RUnlock() + return token, nil + } + s.accessMutex.RUnlock() + + s.accessMutex.Lock() + defer s.accessMutex.Unlock() + + if !s.credentials.needsRefresh() { + return s.credentials.getAccessToken(), nil + } + + newCredentials, err := refreshToken(s.httpClient, s.credentials) + if err != nil { + return "", err + } + + s.credentials = newCredentials + + err = platformWriteCredentials(newCredentials, s.credentialPath) + if err != nil { + s.logger.Warn("persist refreshed token: ", err) + } + + return newCredentials.getAccessToken(), nil +} + +func (s *Service) getAccountID() string { + s.accessMutex.RLock() + defer s.accessMutex.RUnlock() + return s.credentials.getAccountID() +} + +func (s *Service) isAPIKeyMode() bool { + s.accessMutex.RLock() + defer s.accessMutex.RUnlock() + return s.credentials.isAPIKeyMode() +} + +func (s *Service) getBaseURL() string { + if s.isAPIKeyMode() { + return openaiAPIBaseURL + } + return chatGPTBackendURL +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if !strings.HasPrefix(path, "/v1/") { + writeJSONError(w, r, http.StatusNotFound, "invalid_request_error", "path must start with /v1/") + return + } + + var proxyPath string + if s.isAPIKeyMode() { + proxyPath = path + } else { + if path == "/v1/chat/completions" { + writeJSONError(w, r, http.StatusBadRequest, "invalid_request_error", + "chat completions endpoint is only available in API key mode") + return + } + proxyPath = strings.TrimPrefix(path, "/v1") + } + + var username string + if len(s.users) > 0 { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": missing Authorization header") + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "missing api key") + return + } + clientToken := strings.TrimPrefix(authHeader, "Bearer ") + if clientToken == authHeader { + s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": invalid Authorization format") + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key format") + return + } + var ok bool + username, ok = s.userManager.Authenticate(clientToken) + if !ok { + s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": unknown key: ", clientToken) + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key") + return + } + } + + if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && strings.HasPrefix(path, "/v1/responses") { + s.handleWebSocket(w, r, proxyPath, username) + return + } + + var requestModel string + + if s.usageTracker != nil && r.Body != nil { + bodyBytes, err := io.ReadAll(r.Body) + if err == nil { + var request struct { + Model string `json:"model"` + } + err := json.Unmarshal(bodyBytes, &request) + if err == nil { + requestModel = request.Model + } + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + } + + accessToken, err := s.getAccessToken() + if err != nil { + s.logger.Error("get access token: ", err) + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "Authentication failed") + return + } + + proxyURL := s.getBaseURL() + proxyPath + if r.URL.RawQuery != "" { + proxyURL += "?" + r.URL.RawQuery + } + proxyRequest, err := http.NewRequestWithContext(r.Context(), r.Method, proxyURL, r.Body) + if err != nil { + s.logger.Error("create proxy request: ", err) + writeJSONError(w, r, http.StatusInternalServerError, "api_error", "Internal server error") + return + } + + for key, values := range r.Header { + if !isHopByHopHeader(key) && key != "Authorization" { + proxyRequest.Header[key] = values + } + } + + for key, values := range s.httpHeaders { + proxyRequest.Header.Del(key) + proxyRequest.Header[key] = values + } + + proxyRequest.Header.Set("Authorization", "Bearer "+accessToken) + + if accountID := s.getAccountID(); accountID != "" { + proxyRequest.Header.Set("ChatGPT-Account-Id", accountID) + } + + response, err := s.httpClient.Do(proxyRequest) + if err != nil { + writeJSONError(w, r, http.StatusBadGateway, "api_error", err.Error()) + return + } + defer response.Body.Close() + + for key, values := range response.Header { + if !isHopByHopHeader(key) { + w.Header()[key] = values + } + } + w.WriteHeader(response.StatusCode) + + trackUsage := s.usageTracker != nil && response.StatusCode == http.StatusOK && + (path == "/v1/chat/completions" || strings.HasPrefix(path, "/v1/responses")) + if trackUsage { + s.handleResponseWithTracking(w, response, path, requestModel, username) + } else { + mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) + if err == nil && mediaType != "text/event-stream" { + _, _ = io.Copy(w, response.Body) + return + } + flusher, ok := w.(http.Flusher) + if !ok { + s.logger.Error("streaming not supported") + return + } + buffer := make([]byte, buf.BufferSize) + for { + n, err := response.Body.Read(buffer) + if n > 0 { + _, writeError := w.Write(buffer[:n]) + if writeError != nil { + s.logger.Error("write streaming response: ", writeError) + return + } + flusher.Flush() + } + if err != nil { + return + } + } + } +} + +func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, response *http.Response, path string, requestModel string, username string) { + isChatCompletions := path == "/v1/chat/completions" + weeklyCycleHint := extractWeeklyCycleHint(response.Header) + mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) + isStreaming := err == nil && mediaType == "text/event-stream" + if !isStreaming && !isChatCompletions && response.Header.Get("Content-Type") == "" { + isStreaming = true + } + if !isStreaming { + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + s.logger.Error("read response body: ", err) + return + } + + var responseModel, serviceTier string + var inputTokens, outputTokens, cachedTokens int64 + + if isChatCompletions { + var chatCompletion openai.ChatCompletion + if json.Unmarshal(bodyBytes, &chatCompletion) == nil { + responseModel = chatCompletion.Model + serviceTier = string(chatCompletion.ServiceTier) + inputTokens = chatCompletion.Usage.PromptTokens + outputTokens = chatCompletion.Usage.CompletionTokens + cachedTokens = chatCompletion.Usage.PromptTokensDetails.CachedTokens + } + } else { + var responsesResponse responses.Response + if json.Unmarshal(bodyBytes, &responsesResponse) == nil { + responseModel = string(responsesResponse.Model) + serviceTier = string(responsesResponse.ServiceTier) + inputTokens = responsesResponse.Usage.InputTokens + outputTokens = responsesResponse.Usage.OutputTokens + cachedTokens = responsesResponse.Usage.InputTokensDetails.CachedTokens + } + } + + if inputTokens > 0 || outputTokens > 0 { + if responseModel == "" { + responseModel = requestModel + } + if responseModel != "" { + contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + contextWindow, + inputTokens, + outputTokens, + cachedTokens, + serviceTier, + username, + time.Now(), + weeklyCycleHint, + ) + } + } + + _, _ = writer.Write(bodyBytes) + return + } + + flusher, ok := writer.(http.Flusher) + if !ok { + s.logger.Error("streaming not supported") + return + } + + var inputTokens, outputTokens, cachedTokens int64 + var responseModel, serviceTier string + buffer := make([]byte, buf.BufferSize) + var leftover []byte + + for { + n, err := response.Body.Read(buffer) + if n > 0 { + data := append(leftover, buffer[:n]...) + lines := bytes.Split(data, []byte("\n")) + + if err == nil { + leftover = lines[len(lines)-1] + lines = lines[:len(lines)-1] + } else { + leftover = nil + } + + for _, line := range lines { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + + if bytes.HasPrefix(line, []byte("data: ")) { + eventData := bytes.TrimPrefix(line, []byte("data: ")) + if bytes.Equal(eventData, []byte("[DONE]")) { + continue + } + + if isChatCompletions { + var chatChunk openai.ChatCompletionChunk + if json.Unmarshal(eventData, &chatChunk) == nil { + if chatChunk.Model != "" { + responseModel = chatChunk.Model + } + if chatChunk.ServiceTier != "" { + serviceTier = string(chatChunk.ServiceTier) + } + if chatChunk.Usage.PromptTokens > 0 { + inputTokens = chatChunk.Usage.PromptTokens + cachedTokens = chatChunk.Usage.PromptTokensDetails.CachedTokens + } + if chatChunk.Usage.CompletionTokens > 0 { + outputTokens = chatChunk.Usage.CompletionTokens + } + } + } else { + var streamEvent responses.ResponseStreamEventUnion + if json.Unmarshal(eventData, &streamEvent) == nil { + if streamEvent.Type == "response.completed" { + completedEvent := streamEvent.AsResponseCompleted() + if string(completedEvent.Response.Model) != "" { + responseModel = string(completedEvent.Response.Model) + } + if completedEvent.Response.ServiceTier != "" { + serviceTier = string(completedEvent.Response.ServiceTier) + } + if completedEvent.Response.Usage.InputTokens > 0 { + inputTokens = completedEvent.Response.Usage.InputTokens + cachedTokens = completedEvent.Response.Usage.InputTokensDetails.CachedTokens + } + if completedEvent.Response.Usage.OutputTokens > 0 { + outputTokens = completedEvent.Response.Usage.OutputTokens + } + } + } + } + } + } + + _, writeError := writer.Write(buffer[:n]) + if writeError != nil { + s.logger.Error("write streaming response: ", writeError) + return + } + flusher.Flush() + } + + if err != nil { + if responseModel == "" { + responseModel = requestModel + } + + if inputTokens > 0 || outputTokens > 0 { + if responseModel != "" { + contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + contextWindow, + inputTokens, + outputTokens, + cachedTokens, + serviceTier, + username, + time.Now(), + weeklyCycleHint, + ) + } + } + return + } + } +} + +func (s *Service) Close() error { + webSocketSessions := s.startWebSocketShutdown() + + err := common.Close( + common.PtrOrNil(s.httpServer), + common.PtrOrNil(s.listener), + s.tlsConfig, + ) + for _, session := range webSocketSessions { + session.Close() + } + s.webSocketGroup.Wait() + + if s.usageTracker != nil { + s.usageTracker.cancelPendingSave() + saveErr := s.usageTracker.Save() + if saveErr != nil { + s.logger.Error("save usage statistics: ", saveErr) + } + } + + return err +} + +func (s *Service) registerWebSocketSession(session *webSocketSession) bool { + s.webSocketMutex.Lock() + defer s.webSocketMutex.Unlock() + + if s.shuttingDown { + return false + } + + s.webSocketConns[session] = struct{}{} + s.webSocketGroup.Add(1) + return true +} + +func (s *Service) unregisterWebSocketSession(session *webSocketSession) { + s.webSocketMutex.Lock() + _, loaded := s.webSocketConns[session] + if loaded { + delete(s.webSocketConns, session) + } + s.webSocketMutex.Unlock() + + if loaded { + s.webSocketGroup.Done() + } +} + +func (s *Service) isShuttingDown() bool { + s.webSocketMutex.Lock() + defer s.webSocketMutex.Unlock() + return s.shuttingDown +} + +func (s *Service) startWebSocketShutdown() []*webSocketSession { + s.webSocketMutex.Lock() + defer s.webSocketMutex.Unlock() + + s.shuttingDown = true + + webSocketSessions := make([]*webSocketSession, 0, len(s.webSocketConns)) + for session := range s.webSocketConns { + webSocketSessions = append(webSocketSessions, session) + } + return webSocketSessions +} diff --git a/service/ocm/service_usage.go b/service/ocm/service_usage.go new file mode 100644 index 00000000..589fd093 --- /dev/null +++ b/service/ocm/service_usage.go @@ -0,0 +1,1202 @@ +package ocm + +import ( + "encoding/json" + "fmt" + "math" + "os" + "regexp" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" +) + +type UsageStats struct { + RequestCount int `json:"request_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CachedTokens int64 `json:"cached_tokens"` +} + +func (u *UsageStats) UnmarshalJSON(data []byte) error { + type Alias UsageStats + aux := &struct { + *Alias + PromptTokens int64 `json:"prompt_tokens"` + CompletionTokens int64 `json:"completion_tokens"` + }{ + Alias: (*Alias)(u), + } + err := json.Unmarshal(data, aux) + if err != nil { + return err + } + if u.InputTokens == 0 && aux.PromptTokens > 0 { + u.InputTokens = aux.PromptTokens + } + if u.OutputTokens == 0 && aux.CompletionTokens > 0 { + u.OutputTokens = aux.CompletionTokens + } + return nil +} + +type CostCombination struct { + Model string `json:"model"` + ServiceTier string `json:"service_tier,omitempty"` + ContextWindow int `json:"context_window"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` + Total UsageStats `json:"total"` + ByUser map[string]UsageStats `json:"by_user"` +} + +type AggregatedUsage struct { + LastUpdated time.Time `json:"last_updated"` + Combinations []CostCombination `json:"combinations"` + mutex sync.Mutex + filePath string + logger log.ContextLogger + lastSaveTime time.Time + pendingSave bool + saveTimer *time.Timer + saveMutex sync.Mutex +} + +type UsageStatsJSON struct { + RequestCount int `json:"request_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CachedTokens int64 `json:"cached_tokens"` + CostUSD float64 `json:"cost_usd"` +} + +type CostCombinationJSON struct { + Model string `json:"model"` + ServiceTier string `json:"service_tier,omitempty"` + ContextWindow int `json:"context_window"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` + Total UsageStatsJSON `json:"total"` + ByUser map[string]UsageStatsJSON `json:"by_user"` +} + +type CostsSummaryJSON struct { + TotalUSD float64 `json:"total_usd"` + ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` + ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` +} + +type AggregatedUsageJSON struct { + LastUpdated time.Time `json:"last_updated"` + Costs CostsSummaryJSON `json:"costs"` + Combinations []CostCombinationJSON `json:"combinations"` +} + +type WeeklyCycleHint struct { + WindowMinutes int64 + ResetAt time.Time +} + +type ModelPricing struct { + InputPrice float64 + OutputPrice float64 + CachedInputPrice float64 +} + +type modelFamily struct { + pattern *regexp.Regexp + pricing ModelPricing + premiumPricing *ModelPricing +} + +const ( + serviceTierAuto = "auto" + serviceTierDefault = "default" + serviceTierFlex = "flex" + serviceTierPriority = "priority" + serviceTierScale = "scale" +) + +const ( + contextWindowStandard = 272000 + contextWindowPremium = 1050000 + premiumContextThreshold = 272000 +) + +var ( + gpt52Pricing = ModelPricing{ + InputPrice: 1.75, + OutputPrice: 14.0, + CachedInputPrice: 0.175, + } + + gpt5Pricing = ModelPricing{ + InputPrice: 1.25, + OutputPrice: 10.0, + CachedInputPrice: 0.125, + } + + gpt5MiniPricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 2.0, + CachedInputPrice: 0.025, + } + + gpt5NanoPricing = ModelPricing{ + InputPrice: 0.05, + OutputPrice: 0.4, + CachedInputPrice: 0.005, + } + + gpt52CodexPricing = ModelPricing{ + InputPrice: 1.75, + OutputPrice: 14.0, + CachedInputPrice: 0.175, + } + + gpt51CodexPricing = ModelPricing{ + InputPrice: 1.25, + OutputPrice: 10.0, + CachedInputPrice: 0.125, + } + + gpt51CodexMiniPricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 2.0, + CachedInputPrice: 0.025, + } + + gpt54StandardPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 15.0, + CachedInputPrice: 0.25, + } + + gpt54PremiumPricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 22.5, + CachedInputPrice: 0.5, + } + + gpt54ProPricing = ModelPricing{ + InputPrice: 30.0, + OutputPrice: 180.0, + CachedInputPrice: 30.0, + } + + gpt54ProPremiumPricing = ModelPricing{ + InputPrice: 60.0, + OutputPrice: 270.0, + CachedInputPrice: 60.0, + } + + gpt52ProPricing = ModelPricing{ + InputPrice: 21.0, + OutputPrice: 168.0, + CachedInputPrice: 21.0, + } + + gpt5ProPricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 120.0, + CachedInputPrice: 15.0, + } + + gpt54FlexPricing = ModelPricing{ + InputPrice: 1.25, + OutputPrice: 7.5, + CachedInputPrice: 0.125, + } + + gpt54PremiumFlexPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 11.25, + CachedInputPrice: 0.25, + } + + gpt54ProFlexPricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 90.0, + CachedInputPrice: 15.0, + } + + gpt54ProPremiumFlexPricing = ModelPricing{ + InputPrice: 30.0, + OutputPrice: 135.0, + CachedInputPrice: 30.0, + } + + gpt52FlexPricing = ModelPricing{ + InputPrice: 0.875, + OutputPrice: 7.0, + CachedInputPrice: 0.0875, + } + + gpt5FlexPricing = ModelPricing{ + InputPrice: 0.625, + OutputPrice: 5.0, + CachedInputPrice: 0.0625, + } + + gpt5MiniFlexPricing = ModelPricing{ + InputPrice: 0.125, + OutputPrice: 1.0, + CachedInputPrice: 0.0125, + } + + gpt5NanoFlexPricing = ModelPricing{ + InputPrice: 0.025, + OutputPrice: 0.2, + CachedInputPrice: 0.0025, + } + + gpt54PriorityPricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 30.0, + CachedInputPrice: 0.5, + } + + gpt54PremiumPriorityPricing = ModelPricing{ + InputPrice: 10.0, + OutputPrice: 45.0, + CachedInputPrice: 1.0, + } + + gpt52PriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 28.0, + CachedInputPrice: 0.35, + } + + gpt5PriorityPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 20.0, + CachedInputPrice: 0.25, + } + + gpt5MiniPriorityPricing = ModelPricing{ + InputPrice: 0.45, + OutputPrice: 3.6, + CachedInputPrice: 0.045, + } + + gpt52CodexPriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 28.0, + CachedInputPrice: 0.35, + } + + gpt51CodexPriorityPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 20.0, + CachedInputPrice: 0.25, + } + + gpt4oPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 10.0, + CachedInputPrice: 1.25, + } + + gpt4oMiniPricing = ModelPricing{ + InputPrice: 0.15, + OutputPrice: 0.6, + CachedInputPrice: 0.075, + } + + gpt4oAudioPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 10.0, + CachedInputPrice: 2.5, + } + + gpt4oMiniAudioPricing = ModelPricing{ + InputPrice: 0.15, + OutputPrice: 0.6, + CachedInputPrice: 0.15, + } + + gptAudioMiniPricing = ModelPricing{ + InputPrice: 0.6, + OutputPrice: 2.4, + CachedInputPrice: 0.6, + } + + o1Pricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 60.0, + CachedInputPrice: 7.5, + } + + o1ProPricing = ModelPricing{ + InputPrice: 150.0, + OutputPrice: 600.0, + CachedInputPrice: 150.0, + } + + o1MiniPricing = ModelPricing{ + InputPrice: 1.1, + OutputPrice: 4.4, + CachedInputPrice: 0.55, + } + + o3MiniPricing = ModelPricing{ + InputPrice: 1.1, + OutputPrice: 4.4, + CachedInputPrice: 0.55, + } + + o3Pricing = ModelPricing{ + InputPrice: 2.0, + OutputPrice: 8.0, + CachedInputPrice: 0.5, + } + + o3ProPricing = ModelPricing{ + InputPrice: 20.0, + OutputPrice: 80.0, + CachedInputPrice: 20.0, + } + + o3DeepResearchPricing = ModelPricing{ + InputPrice: 10.0, + OutputPrice: 40.0, + CachedInputPrice: 2.5, + } + + o4MiniPricing = ModelPricing{ + InputPrice: 1.1, + OutputPrice: 4.4, + CachedInputPrice: 0.275, + } + + o4MiniDeepResearchPricing = ModelPricing{ + InputPrice: 2.0, + OutputPrice: 8.0, + CachedInputPrice: 0.5, + } + + o3FlexPricing = ModelPricing{ + InputPrice: 1.0, + OutputPrice: 4.0, + CachedInputPrice: 0.25, + } + + o4MiniFlexPricing = ModelPricing{ + InputPrice: 0.55, + OutputPrice: 2.2, + CachedInputPrice: 0.138, + } + + o3PriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 14.0, + CachedInputPrice: 0.875, + } + + o4MiniPriorityPricing = ModelPricing{ + InputPrice: 2.0, + OutputPrice: 8.0, + CachedInputPrice: 0.5, + } + + gpt41Pricing = ModelPricing{ + InputPrice: 2.0, + OutputPrice: 8.0, + CachedInputPrice: 0.5, + } + + gpt41MiniPricing = ModelPricing{ + InputPrice: 0.4, + OutputPrice: 1.6, + CachedInputPrice: 0.1, + } + + gpt41NanoPricing = ModelPricing{ + InputPrice: 0.1, + OutputPrice: 0.4, + CachedInputPrice: 0.025, + } + + gpt41PriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 14.0, + CachedInputPrice: 0.875, + } + + gpt41MiniPriorityPricing = ModelPricing{ + InputPrice: 0.7, + OutputPrice: 2.8, + CachedInputPrice: 0.175, + } + + gpt41NanoPriorityPricing = ModelPricing{ + InputPrice: 0.2, + OutputPrice: 0.8, + CachedInputPrice: 0.05, + } + + gpt4oPriorityPricing = ModelPricing{ + InputPrice: 4.25, + OutputPrice: 17.0, + CachedInputPrice: 2.125, + } + + gpt4oMiniPriorityPricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 1.0, + CachedInputPrice: 0.125, + } + + standardModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`), + pricing: gpt54ProPricing, + premiumPricing: &gpt54ProPremiumPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), + pricing: gpt54StandardPricing, + premiumPricing: &gpt54PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), + pricing: gpt52CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`), + pricing: gpt52CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`), + pricing: gpt51CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`), + pricing: gpt51CodexMiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`), + pricing: gpt51CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`), + pricing: gpt51CodexMiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`), + pricing: gpt51CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`), + pricing: gpt52Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-chat-latest$`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`), + pricing: gpt52ProPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`), + pricing: gpt5ProPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), + pricing: gpt5MiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`), + pricing: gpt5NanoPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), + pricing: gpt52Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`), + pricing: o4MiniDeepResearchPricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), + pricing: o4MiniPricing, + }, + { + pattern: regexp.MustCompile(`^o3-pro(?:$|-)`), + pricing: o3ProPricing, + }, + { + pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`), + pricing: o3DeepResearchPricing, + }, + { + pattern: regexp.MustCompile(`^o3-mini(?:$|-)`), + pricing: o3MiniPricing, + }, + { + pattern: regexp.MustCompile(`^o3(?:$|-)`), + pricing: o3Pricing, + }, + { + pattern: regexp.MustCompile(`^o1-pro(?:$|-)`), + pricing: o1ProPricing, + }, + { + pattern: regexp.MustCompile(`^o1-mini(?:$|-)`), + pricing: o1MiniPricing, + }, + { + pattern: regexp.MustCompile(`^o1(?:$|-)`), + pricing: o1Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`), + pricing: gpt4oMiniAudioPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`), + pricing: gptAudioMiniPricing, + }, + { + pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`), + pricing: gpt4oAudioPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`), + pricing: gpt41NanoPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`), + pricing: gpt41MiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`), + pricing: gpt41Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`), + pricing: gpt4oMiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`), + pricing: gpt4oPricing, + }, + { + pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`), + pricing: gpt4oPricing, + }, + } + + flexModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`), + pricing: gpt54ProFlexPricing, + premiumPricing: &gpt54ProPremiumFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), + pricing: gpt54FlexPricing, + premiumPricing: &gpt54PremiumFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), + pricing: gpt5MiniFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`), + pricing: gpt5NanoFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), + pricing: gpt52FlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), + pricing: gpt5FlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), + pricing: gpt5FlexPricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), + pricing: o4MiniFlexPricing, + }, + { + pattern: regexp.MustCompile(`^o3(?:$|-)`), + pricing: o3FlexPricing, + }, + } + + priorityModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), + pricing: gpt54PriorityPricing, + premiumPricing: &gpt54PremiumPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), + pricing: gpt52CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`), + pricing: gpt52CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`), + pricing: gpt51CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`), + pricing: gpt51CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`), + pricing: gpt5MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`), + pricing: gpt51CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), + pricing: gpt5MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), + pricing: gpt52PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), + pricing: gpt5PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), + pricing: gpt5PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), + pricing: o4MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^o3(?:$|-)`), + pricing: o3PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`), + pricing: gpt41NanoPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`), + pricing: gpt41MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`), + pricing: gpt41PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`), + pricing: gpt4oMiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`), + pricing: gpt4oPriorityPricing, + }, + } +) + +func modelFamiliesForTier(serviceTier string) []modelFamily { + switch serviceTier { + case serviceTierFlex: + return flexModelFamilies + case serviceTierPriority: + return priorityModelFamilies + default: + return standardModelFamilies + } +} + +func findPricingInFamilies(model string, contextWindow int, modelFamilies []modelFamily) (ModelPricing, bool) { + isPremium := contextWindow >= contextWindowPremium + for _, family := range modelFamilies { + if family.pattern.MatchString(model) { + if isPremium && family.premiumPricing != nil { + return *family.premiumPricing, true + } + return family.pricing, true + } + } + return ModelPricing{}, false +} + +func hasPremiumPricingInFamilies(model string, modelFamilies []modelFamily) bool { + for _, family := range modelFamilies { + if family.pattern.MatchString(model) { + return family.premiumPricing != nil + } + } + return false +} + +func normalizeServiceTier(serviceTier string) string { + switch strings.ToLower(strings.TrimSpace(serviceTier)) { + case "", serviceTierAuto, serviceTierDefault: + return serviceTierDefault + case serviceTierFlex: + return serviceTierFlex + case serviceTierPriority: + return serviceTierPriority + case serviceTierScale: + // Scale-tier requests are prepaid differently and not listed in this usage file. + return serviceTierDefault + default: + return serviceTierDefault + } +} + +func getPricing(model string, serviceTier string, contextWindow int) ModelPricing { + normalizedServiceTier := normalizeServiceTier(serviceTier) + families := modelFamiliesForTier(normalizedServiceTier) + + if pricing, found := findPricingInFamilies(model, contextWindow, families); found { + return pricing + } + + normalizedModel := normalizeGPT5Model(model) + if normalizedModel != model { + if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, families); found { + return pricing + } + } + + if normalizedServiceTier != serviceTierDefault { + if pricing, found := findPricingInFamilies(model, contextWindow, standardModelFamilies); found { + return pricing + } + if normalizedModel != model { + if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, standardModelFamilies); found { + return pricing + } + } + } + + return gpt4oPricing +} + +func detectContextWindow(model string, serviceTier string, inputTokens int64) int { + if inputTokens <= premiumContextThreshold { + return contextWindowStandard + } + normalizedServiceTier := normalizeServiceTier(serviceTier) + families := modelFamiliesForTier(normalizedServiceTier) + if hasPremiumPricingInFamilies(model, families) { + return contextWindowPremium + } + normalizedModel := normalizeGPT5Model(model) + if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, families) { + return contextWindowPremium + } + if normalizedServiceTier != serviceTierDefault { + if hasPremiumPricingInFamilies(model, standardModelFamilies) { + return contextWindowPremium + } + if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, standardModelFamilies) { + return contextWindowPremium + } + } + return contextWindowStandard +} + +func normalizeGPT5Model(model string) string { + if !strings.HasPrefix(model, "gpt-5.") { + return model + } + + switch { + case strings.Contains(model, "-codex-mini"): + return "gpt-5.1-codex-mini" + case strings.Contains(model, "-codex-max"): + return "gpt-5.1-codex-max" + case strings.Contains(model, "-codex"): + return "gpt-5.3-codex" + case strings.Contains(model, "-chat-latest"): + return "gpt-5.2-chat-latest" + case strings.Contains(model, "-pro"): + return "gpt-5.4-pro" + case strings.Contains(model, "-mini"): + return "gpt-5-mini" + case strings.Contains(model, "-nano"): + return "gpt-5-nano" + default: + return "gpt-5.4" + } +} + +func calculateCost(stats UsageStats, model string, serviceTier string, contextWindow int) float64 { + pricing := getPricing(model, serviceTier, contextWindow) + + regularInputTokens := stats.InputTokens - stats.CachedTokens + if regularInputTokens < 0 { + regularInputTokens = 0 + } + + cost := (float64(regularInputTokens)*pricing.InputPrice + + float64(stats.OutputTokens)*pricing.OutputPrice + + float64(stats.CachedTokens)*pricing.CachedInputPrice) / 1_000_000 + + return math.Round(cost*100) / 100 +} + +func roundCost(cost float64) float64 { + return math.Round(cost*100) / 100 +} + +func normalizeCombinations(combinations []CostCombination) { + for index := range combinations { + combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier) + if combinations[index].ContextWindow <= 0 { + combinations[index].ContextWindow = contextWindowStandard + } + if combinations[index].ByUser == nil { + combinations[index].ByUser = make(map[string]UsageStats) + } + } +} + +func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, contextWindow int, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) { + var matchedCombination *CostCombination + for index := range *combinations { + combination := &(*combinations)[index] + combinationServiceTier := normalizeServiceTier(combination.ServiceTier) + if combination.ServiceTier != combinationServiceTier { + combination.ServiceTier = combinationServiceTier + } + if combination.Model == model && combinationServiceTier == serviceTier && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix { + matchedCombination = combination + break + } + } + + if matchedCombination == nil { + newCombination := CostCombination{ + Model: model, + ServiceTier: serviceTier, + ContextWindow: contextWindow, + WeekStartUnix: weekStartUnix, + Total: UsageStats{}, + ByUser: make(map[string]UsageStats), + } + *combinations = append(*combinations, newCombination) + matchedCombination = &(*combinations)[len(*combinations)-1] + } + + matchedCombination.Total.RequestCount++ + matchedCombination.Total.InputTokens += inputTokens + matchedCombination.Total.OutputTokens += outputTokens + matchedCombination.Total.CachedTokens += cachedTokens + + if user != "" { + userStats := matchedCombination.ByUser[user] + userStats.RequestCount++ + userStats.InputTokens += inputTokens + userStats.OutputTokens += outputTokens + userStats.CachedTokens += cachedTokens + matchedCombination.ByUser[user] = userStats + } +} + +func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) { + result := make([]CostCombinationJSON, len(combinations)) + var totalCost float64 + + for index, combination := range combinations { + combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow) + totalCost += combinationTotalCost + + combinationJSON := CostCombinationJSON{ + Model: combination.Model, + ServiceTier: combination.ServiceTier, + ContextWindow: combination.ContextWindow, + WeekStartUnix: combination.WeekStartUnix, + Total: UsageStatsJSON{ + RequestCount: combination.Total.RequestCount, + InputTokens: combination.Total.InputTokens, + OutputTokens: combination.Total.OutputTokens, + CachedTokens: combination.Total.CachedTokens, + CostUSD: combinationTotalCost, + }, + ByUser: make(map[string]UsageStatsJSON), + } + + for user, userStats := range combination.ByUser { + userCost := calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow) + if aggregateUserCosts != nil { + aggregateUserCosts[user] += userCost + } + + combinationJSON.ByUser[user] = UsageStatsJSON{ + RequestCount: userStats.RequestCount, + InputTokens: userStats.InputTokens, + OutputTokens: userStats.OutputTokens, + CachedTokens: userStats.CachedTokens, + CostUSD: userCost, + } + } + + result[index] = combinationJSON + } + + return result, roundCost(totalCost) +} + +func formatUTCOffsetLabel(timestamp time.Time) string { + _, offsetSeconds := timestamp.Zone() + sign := "+" + if offsetSeconds < 0 { + sign = "-" + offsetSeconds = -offsetSeconds + } + offsetHours := offsetSeconds / 3600 + offsetMinutes := (offsetSeconds % 3600) / 60 + if offsetMinutes == 0 { + return fmt.Sprintf("UTC%s%d", sign, offsetHours) + } + return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes) +} + +func formatWeekStartKey(cycleStartAt time.Time) string { + localCycleStart := cycleStartAt.In(time.Local) + return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart)) +} + +func buildByWeekCost(combinations []CostCombination) map[string]float64 { + byWeek := make(map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow) + } + for weekKey, weekCost := range byWeek { + byWeek[weekKey] = roundCost(weekCost) + } + return byWeek +} + +func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { + byUserAndWeek := make(map[string]map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + for user, userStats := range combination.ByUser { + userWeeks, exists := byUserAndWeek[user] + if !exists { + userWeeks = make(map[string]float64) + byUserAndWeek[user] = userWeeks + } + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow) + } + } + for _, weekCosts := range byUserAndWeek { + for weekKey, cost := range weekCosts { + weekCosts[weekKey] = roundCost(cost) + } + } + return byUserAndWeek +} + +func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { + if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { + return 0 + } + windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute + return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix() +} + +func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { + u.mutex.Lock() + defer u.mutex.Unlock() + + result := &AggregatedUsageJSON{ + LastUpdated: u.LastUpdated, + Costs: CostsSummaryJSON{ + TotalUSD: 0, + ByUser: make(map[string]float64), + ByWeek: make(map[string]float64), + }, + } + + globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser) + result.Combinations = globalCombinationsJSON + result.Costs.TotalUSD = totalCost + result.Costs.ByWeek = buildByWeekCost(u.Combinations) + + if len(result.Costs.ByWeek) == 0 { + result.Costs.ByWeek = nil + } + + result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) + if len(result.Costs.ByUserAndWeek) == 0 { + result.Costs.ByUserAndWeek = nil + } + + for user, cost := range result.Costs.ByUser { + result.Costs.ByUser[user] = roundCost(cost) + } + + return result +} + +func (u *AggregatedUsage) Load() error { + u.mutex.Lock() + defer u.mutex.Unlock() + + u.LastUpdated = time.Time{} + u.Combinations = nil + + data, err := os.ReadFile(u.filePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + var temp struct { + LastUpdated time.Time `json:"last_updated"` + Combinations []CostCombination `json:"combinations"` + } + + err = json.Unmarshal(data, &temp) + if err != nil { + return err + } + + u.LastUpdated = temp.LastUpdated + u.Combinations = temp.Combinations + normalizeCombinations(u.Combinations) + + return nil +} + +func (u *AggregatedUsage) Save() error { + jsonData := u.ToJSON() + + data, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + return err + } + + tmpFile := u.filePath + ".tmp" + err = os.WriteFile(tmpFile, data, 0o644) + if err != nil { + return err + } + defer os.Remove(tmpFile) + err = os.Rename(tmpFile, u.filePath) + if err == nil { + u.saveMutex.Lock() + u.lastSaveTime = time.Now() + u.saveMutex.Unlock() + } + return err +} + +func (u *AggregatedUsage) AddUsage(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error { + return u.AddUsageWithCycleHint(model, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil) +} + +func (u *AggregatedUsage) AddUsageWithCycleHint(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error { + if model == "" { + return E.New("model cannot be empty") + } + if contextWindow <= 0 { + return E.New("contextWindow must be positive") + } + + normalizedServiceTier := normalizeServiceTier(serviceTier) + if observedAt.IsZero() { + observedAt = time.Now() + } + + u.mutex.Lock() + defer u.mutex.Unlock() + + u.LastUpdated = observedAt + weekStartUnix := deriveWeekStartUnix(cycleHint) + + addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens) + + go u.scheduleSave() + + return nil +} + +func (u *AggregatedUsage) scheduleSave() { + const saveInterval = time.Minute + + u.saveMutex.Lock() + defer u.saveMutex.Unlock() + + timeSinceLastSave := time.Since(u.lastSaveTime) + + if timeSinceLastSave >= saveInterval { + go u.saveAsync() + return + } + + if u.pendingSave { + return + } + + u.pendingSave = true + remainingTime := saveInterval - timeSinceLastSave + + u.saveTimer = time.AfterFunc(remainingTime, func() { + u.saveMutex.Lock() + u.pendingSave = false + u.saveMutex.Unlock() + u.saveAsync() + }) +} + +func (u *AggregatedUsage) saveAsync() { + err := u.Save() + if err != nil { + if u.logger != nil { + u.logger.Error("save usage statistics: ", err) + } + } +} + +func (u *AggregatedUsage) cancelPendingSave() { + u.saveMutex.Lock() + defer u.saveMutex.Unlock() + + if u.saveTimer != nil { + u.saveTimer.Stop() + u.saveTimer = nil + } + u.pendingSave = false +} diff --git a/service/ocm/service_user.go b/service/ocm/service_user.go new file mode 100644 index 00000000..494b981b --- /dev/null +++ b/service/ocm/service_user.go @@ -0,0 +1,29 @@ +package ocm + +import ( + "sync" + + "github.com/sagernet/sing-box/option" +) + +type UserManager struct { + accessMutex sync.RWMutex + tokenMap map[string]string +} + +func (m *UserManager) UpdateUsers(users []option.OCMUser) { + m.accessMutex.Lock() + defer m.accessMutex.Unlock() + tokenMap := make(map[string]string, len(users)) + for _, user := range users { + tokenMap[user.Token] = user.Name + } + m.tokenMap = tokenMap +} + +func (m *UserManager) Authenticate(token string) (string, bool) { + m.accessMutex.RLock() + username, found := m.tokenMap[token] + m.accessMutex.RUnlock() + return username, found +} diff --git a/service/ocm/service_websocket.go b/service/ocm/service_websocket.go new file mode 100644 index 00000000..d19f2df8 --- /dev/null +++ b/service/ocm/service_websocket.go @@ -0,0 +1,285 @@ +package ocm + +import ( + "context" + stdTLS "crypto/tls" + "encoding/json" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" + + "github.com/openai/openai-go/v3/responses" +) + +type webSocketSession struct { + clientConn net.Conn + upstreamConn net.Conn + closeOnce sync.Once +} + +func (s *webSocketSession) Close() { + s.closeOnce.Do(func() { + s.clientConn.Close() + s.upstreamConn.Close() + }) +} + +func buildUpstreamWebSocketURL(baseURL string, proxyPath string) string { + upstreamURL := baseURL + if strings.HasPrefix(upstreamURL, "https://") { + upstreamURL = "wss://" + upstreamURL[len("https://"):] + } else if strings.HasPrefix(upstreamURL, "http://") { + upstreamURL = "ws://" + upstreamURL[len("http://"):] + } + return upstreamURL + proxyPath +} + +func isForwardableResponseHeader(key string) bool { + lowerKey := strings.ToLower(key) + switch { + case strings.HasPrefix(lowerKey, "x-codex-"): + return true + case strings.HasPrefix(lowerKey, "x-reasoning"): + return true + case lowerKey == "openai-model": + return true + case strings.Contains(lowerKey, "-secondary-"): + return true + default: + return false + } +} + +func isForwardableWebSocketRequestHeader(key string) bool { + if isHopByHopHeader(key) { + return false + } + + lowerKey := strings.ToLower(key) + switch { + case lowerKey == "authorization": + return false + case strings.HasPrefix(lowerKey, "sec-websocket-"): + return false + default: + return true + } +} + +func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyPath string, username string) { + accessToken, err := s.getAccessToken() + if err != nil { + s.logger.Error("get access token for websocket: ", err) + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "authentication failed") + return + } + + upstreamURL := buildUpstreamWebSocketURL(s.getBaseURL(), proxyPath) + if r.URL.RawQuery != "" { + upstreamURL += "?" + r.URL.RawQuery + } + + upstreamHeaders := make(http.Header) + for key, values := range r.Header { + if isForwardableWebSocketRequestHeader(key) { + upstreamHeaders[key] = values + } + } + for key, values := range s.httpHeaders { + upstreamHeaders.Del(key) + upstreamHeaders[key] = values + } + upstreamHeaders.Set("Authorization", "Bearer "+accessToken) + if accountID := s.getAccountID(); accountID != "" { + upstreamHeaders.Set("ChatGPT-Account-Id", accountID) + } + + upstreamResponseHeaders := make(http.Header) + upstreamDialer := ws.Dialer{ + NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSConfig: &stdTLS.Config{ + RootCAs: adapter.RootPoolFromContext(s.ctx), + Time: ntp.TimeFuncFromContext(s.ctx), + }, + Header: ws.HandshakeHeaderHTTP(upstreamHeaders), + OnHeader: func(key, value []byte) error { + upstreamResponseHeaders.Add(string(key), string(value)) + return nil + }, + } + + upstreamConn, upstreamBufferedReader, _, err := upstreamDialer.Dial(r.Context(), upstreamURL) + if err != nil { + s.logger.Error("dial upstream websocket: ", err) + writeJSONError(w, r, http.StatusBadGateway, "api_error", "upstream websocket connection failed") + return + } + + weeklyCycleHint := extractWeeklyCycleHint(upstreamResponseHeaders) + + clientResponseHeaders := make(http.Header) + for key, values := range upstreamResponseHeaders { + if isForwardableResponseHeader(key) { + clientResponseHeaders[key] = values + } + } + + clientUpgrader := ws.HTTPUpgrader{ + Header: clientResponseHeaders, + } + if s.isShuttingDown() { + upstreamConn.Close() + writeJSONError(w, r, http.StatusServiceUnavailable, "api_error", "service is shutting down") + return + } + clientConn, _, _, err := clientUpgrader.Upgrade(r, w) + if err != nil { + s.logger.Error("upgrade client websocket: ", err) + upstreamConn.Close() + return + } + session := &webSocketSession{ + clientConn: clientConn, + upstreamConn: upstreamConn, + } + if !s.registerWebSocketSession(session) { + session.Close() + return + } + defer s.unregisterWebSocketSession(session) + + var upstreamReadWriter io.ReadWriter + if upstreamBufferedReader != nil { + upstreamReadWriter = struct { + io.Reader + io.Writer + }{upstreamBufferedReader, upstreamConn} + } else { + upstreamReadWriter = upstreamConn + } + + modelChannel := make(chan string, 1) + var waitGroup sync.WaitGroup + + waitGroup.Add(2) + go func() { + defer waitGroup.Done() + defer session.Close() + s.proxyWebSocketClientToUpstream(clientConn, upstreamConn, modelChannel) + }() + go func() { + defer waitGroup.Done() + defer session.Close() + s.proxyWebSocketUpstreamToClient(upstreamReadWriter, clientConn, modelChannel, username, weeklyCycleHint) + }() + waitGroup.Wait() +} + +func (s *Service) proxyWebSocketClientToUpstream(clientConn net.Conn, upstreamConn net.Conn, modelChannel chan<- string) { + for { + data, opCode, err := wsutil.ReadClientData(clientConn) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("read client websocket: ", err) + } + return + } + + if opCode == ws.OpText && s.usageTracker != nil { + var request struct { + Type string `json:"type"` + Model string `json:"model"` + } + if json.Unmarshal(data, &request) == nil && request.Type == "response.create" && request.Model != "" { + select { + case modelChannel <- request.Model: + default: + } + } + } + + err = wsutil.WriteClientMessage(upstreamConn, opCode, data) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("write upstream websocket: ", err) + } + return + } + } +} + +func (s *Service) proxyWebSocketUpstreamToClient(upstreamReadWriter io.ReadWriter, clientConn net.Conn, modelChannel <-chan string, username string, weeklyCycleHint *WeeklyCycleHint) { + var requestModel string + for { + data, opCode, err := wsutil.ReadServerData(upstreamReadWriter) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("read upstream websocket: ", err) + } + return + } + + if opCode == ws.OpText && s.usageTracker != nil { + select { + case model := <-modelChannel: + requestModel = model + default: + } + + var event struct { + Type string `json:"type"` + } + if json.Unmarshal(data, &event) == nil && event.Type == "response.completed" { + var streamEvent responses.ResponseStreamEventUnion + if json.Unmarshal(data, &streamEvent) == nil { + completedEvent := streamEvent.AsResponseCompleted() + responseModel := string(completedEvent.Response.Model) + serviceTier := string(completedEvent.Response.ServiceTier) + inputTokens := completedEvent.Response.Usage.InputTokens + outputTokens := completedEvent.Response.Usage.OutputTokens + cachedTokens := completedEvent.Response.Usage.InputTokensDetails.CachedTokens + + if inputTokens > 0 || outputTokens > 0 { + if responseModel == "" { + responseModel = requestModel + } + if responseModel != "" { + contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + contextWindow, + inputTokens, + outputTokens, + cachedTokens, + serviceTier, + username, + time.Now(), + weeklyCycleHint, + ) + } + } + } + } + } + + err = wsutil.WriteServerMessage(clientConn, opCode, data) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("write client websocket: ", err) + } + return + } + } +} diff --git a/service/oomkiller/policy.go b/service/oomkiller/policy.go new file mode 100644 index 00000000..aa744301 --- /dev/null +++ b/service/oomkiller/policy.go @@ -0,0 +1,46 @@ +package oomkiller + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/memory" + "github.com/sagernet/sing/service" +) + +const DefaultAppleNetworkExtensionMemoryLimit = 50 * 1024 * 1024 + +type policyMode uint8 + +const ( + policyModeNone policyMode = iota + policyModeMemoryLimit + policyModeAvailable + policyModeNetworkExtension +) + +func (m policyMode) hasTimerMode() bool { + return m != policyModeNone +} + +func resolvePolicyMode(ctx context.Context, options option.OOMKillerServiceOptions) (uint64, policyMode) { + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + if C.IsIos && platformInterface != nil && platformInterface.UnderNetworkExtension() { + return DefaultAppleNetworkExtensionMemoryLimit, policyModeNetworkExtension + } + if options.MemoryLimitOverride > 0 { + return options.MemoryLimitOverride, policyModeMemoryLimit + } + if options.MemoryLimit != nil { + memoryLimit := options.MemoryLimit.Value() + if memoryLimit > 0 { + return memoryLimit, policyModeMemoryLimit + } + } + if memory.AvailableAvailable() { + return 0, policyModeAvailable + } + return 0, policyModeNone +} diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go new file mode 100644 index 00000000..ec3838d2 --- /dev/null +++ b/service/oomkiller/service.go @@ -0,0 +1,83 @@ +package oomkiller + +import ( + "context" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + boxConstant "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/service" +) + +type OOMReporter interface { + WriteReport(memoryUsage uint64) error +} + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) +} + +type Service struct { + boxService.Adapter + ctx context.Context + logger log.ContextLogger + router adapter.Router + timerConfig timerConfig + adaptiveTimer *adaptiveTimer + lastReportTime atomic.Int64 +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { + memoryLimit, mode := resolvePolicyMode(ctx, options) + config, err := buildTimerConfig(options, memoryLimit, mode, options.KillerDisabled) + if err != nil { + return nil, err + } + return &Service{ + Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), + ctx: ctx, + logger: logger, + router: service.FromContext[adapter.Router](ctx), + timerConfig: config, + }, nil +} + +func (s *Service) createTimer() { + s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig, s.writeOOMReport) +} + +func (s *Service) startTimer() { + s.createTimer() + s.adaptiveTimer.start() +} + +func (s *Service) stopTimer() { + if s.adaptiveTimer != nil { + s.adaptiveTimer.stop() + } +} + +func (s *Service) writeOOMReport(memoryUsage uint64) { + now := time.Now().Unix() + lastReport := s.lastReportTime.Load() + if now-lastReport < 3600 { + return + } + if !s.lastReportTime.CompareAndSwap(lastReport, now) { + return + } + reporter := service.FromContext[OOMReporter](s.ctx) + if reporter == nil { + return + } + err := reporter.WriteReport(memoryUsage) + if err != nil { + s.logger.Warn("failed to write OOM report: ", err) + } else { + s.logger.Info("OOM report saved") + } +} diff --git a/service/oomkiller/service_darwin.go b/service/oomkiller/service_darwin.go new file mode 100644 index 00000000..1d51c1b4 --- /dev/null +++ b/service/oomkiller/service_darwin.go @@ -0,0 +1,105 @@ +//go:build darwin && cgo + +package oomkiller + +/* +#include + +static dispatch_source_t memoryPressureSource; + +extern void goMemoryPressureCallback(unsigned long status); + +static void startMemoryPressureMonitor() { + memoryPressureSource = dispatch_source_create( + DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, + 0, + DISPATCH_MEMORYPRESSURE_CRITICAL, + dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + ); + dispatch_source_set_event_handler(memoryPressureSource, ^{ + unsigned long status = dispatch_source_get_data(memoryPressureSource); + goMemoryPressureCallback(status); + }); + dispatch_activate(memoryPressureSource); +} + +static void stopMemoryPressureMonitor() { + if (memoryPressureSource) { + dispatch_source_cancel(memoryPressureSource); + memoryPressureSource = NULL; + } +} +*/ +import "C" + +import ( + runtimeDebug "runtime/debug" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" +) + +var ( + globalAccess sync.Mutex + globalServices []*Service +) + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if s.timerConfig.policyMode == policyModeNetworkExtension { + s.createTimer() + globalAccess.Lock() + isFirst := len(globalServices) == 0 + globalServices = append(globalServices, s) + globalAccess.Unlock() + if isFirst { + C.startMemoryPressureMonitor() + } + return nil + } + if !s.timerConfig.policyMode.hasTimerMode() { + return E.New("memory pressure monitoring is not available on this platform without memory_limit") + } + s.startTimer() + return nil +} + +func (s *Service) Close() error { + s.stopTimer() + if s.timerConfig.policyMode == policyModeNetworkExtension { + globalAccess.Lock() + for i, svc := range globalServices { + if svc == s { + globalServices = append(globalServices[:i], globalServices[i+1:]...) + break + } + } + isLast := len(globalServices) == 0 + globalAccess.Unlock() + if isLast { + C.stopMemoryPressureMonitor() + } + } + return nil +} + +//export goMemoryPressureCallback +func goMemoryPressureCallback(status C.ulong) { + runtimeDebug.FreeOSMemory() + globalAccess.Lock() + services := make([]*Service, len(globalServices)) + copy(services, globalServices) + globalAccess.Unlock() + if len(services) == 0 { + return + } + sample := readMemorySample(policyModeNetworkExtension) + for _, s := range services { + s.logger.Warn("memory pressure: critical, usage: ", byteformats.FormatMemoryBytes(sample.usage)) + s.adaptiveTimer.notifyPressure() + } +} diff --git a/service/oomkiller/service_stub.go b/service/oomkiller/service_stub.go new file mode 100644 index 00000000..5eaf8204 --- /dev/null +++ b/service/oomkiller/service_stub.go @@ -0,0 +1,24 @@ +//go:build !darwin || !cgo + +package oomkiller + +import ( + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" +) + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if !s.timerConfig.policyMode.hasTimerMode() { + return E.New("memory pressure monitoring is not available on this platform without memory_limit") + } + s.startTimer() + return nil +} + +func (s *Service) Close() error { + s.stopTimer() + return nil +} diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go new file mode 100644 index 00000000..a5bef3a7 --- /dev/null +++ b/service/oomkiller/timer.go @@ -0,0 +1,338 @@ +package oomkiller + +import ( + runtimeDebug "runtime/debug" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/memory" +) + +const ( + defaultMinInterval = 100 * time.Millisecond + defaultArmedInterval = time.Second + defaultMaxInterval = 10 * time.Second + defaultSafetyMargin = 5 * 1024 * 1024 + defaultAvailableTriggerMarginMin = 32 * 1024 * 1024 + defaultAvailableTriggerMarginMax = 128 * 1024 * 1024 +) + +type pressureState uint8 + +const ( + pressureStateNormal pressureState = iota + pressureStateArmed + pressureStateTriggered +) + +type memorySample struct { + usage uint64 + available uint64 + availableKnown bool +} + +type pressureThresholds struct { + trigger uint64 + armed uint64 + resume uint64 +} + +type timerConfig struct { + memoryLimit uint64 + safetyMargin uint64 + hasSafetyMargin bool + minInterval time.Duration + armedInterval time.Duration + maxInterval time.Duration + policyMode policyMode + killerDisabled bool +} + +func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, policyMode policyMode, killerDisabled bool) (timerConfig, error) { + minInterval := defaultMinInterval + if options.MinInterval != 0 { + minInterval = time.Duration(options.MinInterval.Build()) + if minInterval <= 0 { + return timerConfig{}, E.New("min_interval must be greater than 0") + } + } + + maxInterval := defaultMaxInterval + if options.MaxInterval != 0 { + maxInterval = time.Duration(options.MaxInterval.Build()) + if maxInterval <= 0 { + return timerConfig{}, E.New("max_interval must be greater than 0") + } + } + if maxInterval < minInterval { + return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval") + } + + var ( + safetyMargin uint64 + hasSafetyMargin bool + ) + if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 { + safetyMargin = options.SafetyMargin.Value() + hasSafetyMargin = true + } else if memoryLimit > 0 { + safetyMargin = defaultSafetyMargin + hasSafetyMargin = true + } + + return timerConfig{ + memoryLimit: memoryLimit, + safetyMargin: safetyMargin, + hasSafetyMargin: hasSafetyMargin, + minInterval: minInterval, + armedInterval: max(min(defaultArmedInterval, maxInterval), minInterval), + maxInterval: maxInterval, + policyMode: policyMode, + killerDisabled: killerDisabled, + }, nil +} + +type adaptiveTimer struct { + timerConfig + logger log.ContextLogger + router adapter.Router + onTriggered func(uint64) + limitThresholds pressureThresholds + + access sync.Mutex + timer *time.Timer + state pressureState + currentInterval time.Duration + forceMinInterval bool + pendingPressureBaseline bool + pressureBaseline memorySample + pressureBaselineTime time.Time +} + +func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig, onTriggered func(uint64)) *adaptiveTimer { + t := &adaptiveTimer{ + timerConfig: config, + logger: logger, + router: router, + onTriggered: onTriggered, + } + if config.policyMode == policyModeMemoryLimit || config.policyMode == policyModeNetworkExtension { + t.limitThresholds = computeLimitThresholds(config.memoryLimit, config.safetyMargin) + } + return t +} + +func (t *adaptiveTimer) start() { + t.access.Lock() + defer t.access.Unlock() + t.startLocked() +} + +func (t *adaptiveTimer) notifyPressure() { + t.access.Lock() + t.startLocked() + t.forceMinInterval = true + t.pendingPressureBaseline = true + t.access.Unlock() + t.poll() +} + +func (t *adaptiveTimer) startLocked() { + if t.timer != nil { + return + } + t.state = pressureStateNormal + t.forceMinInterval = false + t.timer = time.AfterFunc(t.minInterval, t.poll) +} + +func (t *adaptiveTimer) stop() { + t.access.Lock() + defer t.access.Unlock() + if t.timer != nil { + t.timer.Stop() + t.timer = nil + } +} + +func (t *adaptiveTimer) poll() { + if t.timerConfig.policyMode == policyModeNetworkExtension { + runtimeDebug.FreeOSMemory() + } + + var triggered bool + var rateTriggered bool + sample := readMemorySample(t.policyMode) + + t.access.Lock() + if t.timer == nil { + t.access.Unlock() + return + } + if t.pendingPressureBaseline { + t.pressureBaseline = sample + t.pressureBaselineTime = time.Now() + t.pendingPressureBaseline = false + } + previousState := t.state + t.state = t.nextState(sample) + if t.state == pressureStateNormal { + t.forceMinInterval = false + if !t.pressureBaselineTime.IsZero() && time.Since(t.pressureBaselineTime) > t.maxInterval { + t.pressureBaselineTime = time.Time{} + } + } + t.timer.Reset(t.intervalForState()) + triggered = previousState != pressureStateTriggered && t.state == pressureStateTriggered + if !triggered && !t.pressureBaselineTime.IsZero() && t.memoryLimit > 0 && + sample.usage > t.pressureBaseline.usage && sample.usage < t.memoryLimit { + elapsed := time.Since(t.pressureBaselineTime) + if elapsed >= t.minInterval/2 { + growth := sample.usage - t.pressureBaseline.usage + ratePerSecond := float64(growth) / elapsed.Seconds() + headroom := t.memoryLimit - sample.usage + timeToLimit := time.Duration(float64(headroom)/ratePerSecond) * time.Second + if timeToLimit < t.minInterval { + triggered = true + rateTriggered = true + t.state = pressureStateTriggered + } + } + } + t.access.Unlock() + + if !triggered { + return + } + t.onTriggered(sample.usage) + if rateTriggered { + if t.killerDisabled { + t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) + } else { + t.logger.Error("memory growth rate critical, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") + t.router.ResetNetwork() + } + } else { + if t.killerDisabled { + t.logger.Warn("memory threshold reached (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) + } else { + t.logger.Error("memory threshold reached, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") + t.router.ResetNetwork() + } + } + runtimeDebug.FreeOSMemory() +} + +func (t *adaptiveTimer) nextState(sample memorySample) pressureState { + switch t.policyMode { + case policyModeMemoryLimit, policyModeNetworkExtension: + return nextPressureState(t.state, + sample.usage >= t.limitThresholds.trigger, + sample.usage >= t.limitThresholds.armed, + sample.usage >= t.limitThresholds.resume, + ) + case policyModeAvailable: + if !sample.availableKnown { + return pressureStateNormal + } + thresholds := t.availableThresholds(sample) + return nextPressureState(t.state, + sample.available <= thresholds.trigger, + sample.available <= thresholds.armed, + sample.available <= thresholds.resume, + ) + default: + return pressureStateNormal + } +} + +func computeLimitThresholds(memoryLimit uint64, safetyMargin uint64) pressureThresholds { + triggerMargin := min(safetyMargin, memoryLimit) + armedMargin := min(triggerMargin*2, memoryLimit) + resumeMargin := min(triggerMargin*4, memoryLimit) + return pressureThresholds{ + trigger: memoryLimit - triggerMargin, + armed: memoryLimit - armedMargin, + resume: memoryLimit - resumeMargin, + } +} + +func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresholds { + var triggerMargin uint64 + if t.hasSafetyMargin { + triggerMargin = t.safetyMargin + } else if sample.usage == 0 { + triggerMargin = defaultAvailableTriggerMarginMin + } else { + triggerMargin = max(defaultAvailableTriggerMarginMin, min(sample.usage/4, defaultAvailableTriggerMarginMax)) + } + return pressureThresholds{ + trigger: triggerMargin, + armed: triggerMargin * 2, + resume: triggerMargin * 4, + } +} + +func (t *adaptiveTimer) intervalForState() time.Duration { + switch { + case t.forceMinInterval || t.state == pressureStateTriggered: + t.currentInterval = t.minInterval + case t.state == pressureStateArmed: + t.currentInterval = t.armedInterval + default: + if t.currentInterval == 0 { + t.currentInterval = t.maxInterval + } else { + t.currentInterval = min(t.currentInterval*2, t.maxInterval) + } + } + return t.currentInterval +} + +func (t *adaptiveTimer) logDetails(sample memorySample) string { + switch t.policyMode { + case policyModeMemoryLimit, policyModeNetworkExtension: + headroom := uint64(0) + if sample.usage < t.memoryLimit { + headroom = t.memoryLimit - sample.usage + } + return ", limit: " + byteformats.FormatMemoryBytes(t.memoryLimit) + ", headroom: " + byteformats.FormatMemoryBytes(headroom) + case policyModeAvailable: + if sample.availableKnown { + return ", available: " + byteformats.FormatMemoryBytes(sample.available) + } + } + return "" +} + +func nextPressureState(current pressureState, shouldTrigger, shouldArm, shouldStayTriggered bool) pressureState { + if current == pressureStateTriggered { + if shouldStayTriggered { + return pressureStateTriggered + } + return pressureStateNormal + } + if shouldTrigger { + return pressureStateTriggered + } + if shouldArm { + return pressureStateArmed + } + return pressureStateNormal +} + +func readMemorySample(mode policyMode) memorySample { + sample := memorySample{ + usage: memory.Total(), + } + if mode == policyModeAvailable { + sample.availableKnown = true + sample.available = memory.Available() + } + return sample +} diff --git a/service/origin_ca/service.go b/service/origin_ca/service.go new file mode 100644 index 00000000..85588c37 --- /dev/null +++ b/service/origin_ca/service.go @@ -0,0 +1,618 @@ +package originca + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "io" + "io/fs" + "net" + "net/http" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/common/dialer" + 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" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + + "github.com/caddyserver/certmagic" +) + +const ( + cloudflareOriginCAEndpoint = "https://api.cloudflare.com/client/v4/certificates" + defaultRequestedValidity = option.CloudflareOriginCARequestValidity5475 + // min of 30 days and certmagic's 1/3 lifetime ratio (maintain.go) + defaultRenewBefore = 30 * 24 * time.Hour + // from certmagic retry backoff range (async.go) + minimumRenewRetryDelay = time.Minute + maximumRenewRetryDelay = time.Hour + storageLockPrefix = "cloudflare-origin-ca" +) + +func RegisterCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.CloudflareOriginCACertificateProviderOptions](registry, C.TypeCloudflareOriginCA, NewCertificateProvider) +} + +var _ adapter.CertificateProviderService = (*Service)(nil) + +type Service struct { + certificate.Adapter + logger log.ContextLogger + ctx context.Context + cancel context.CancelFunc + done chan struct{} + timeFunc func() time.Time + httpClient *http.Client + storage certmagic.Storage + storageIssuerKey string + storageNamesKey string + storageLockKey string + apiToken string + originCAKey string + domain []string + requestType option.CloudflareOriginCARequestType + requestedValidity option.CloudflareOriginCARequestValidity + + access sync.RWMutex + currentCertificate *tls.Certificate + currentLeaf *x509.Certificate +} + +func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.CloudflareOriginCACertificateProviderOptions) (adapter.CertificateProviderService, error) { + domain, err := normalizeHostnames(options.Domain) + if err != nil { + return nil, err + } + if len(domain) == 0 { + return nil, E.New("missing domain") + } + apiToken := strings.TrimSpace(options.APIToken) + originCAKey := strings.TrimSpace(options.OriginCAKey) + switch { + case apiToken == "" && originCAKey == "": + return nil, E.New("api_token or origin_ca_key is required") + case apiToken != "" && originCAKey != "": + return nil, E.New("api_token and origin_ca_key are mutually exclusive") + } + requestType := options.RequestType + if requestType == "" { + requestType = option.CloudflareOriginCARequestTypeOriginRSA + } + requestedValidity := options.RequestedValidity + if requestedValidity == 0 { + requestedValidity = defaultRequestedValidity + } + ctx, cancel := context.WithCancel(ctx) + serviceDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{ + Detour: options.Detour, + }, + RemoteIsDomain: true, + }) + if err != nil { + cancel() + return nil, E.Cause(err, "create Cloudflare Origin CA dialer") + } + var storage certmagic.Storage + if options.DataDirectory != "" { + storage = &certmagic.FileStorage{Path: options.DataDirectory} + } else { + storage = certmagic.Default.Storage + } + timeFunc := ntp.TimeFuncFromContext(ctx) + if timeFunc == nil { + timeFunc = time.Now + } + storageIssuerKey := C.TypeCloudflareOriginCA + "-" + string(requestType) + storageNamesKey := (&certmagic.CertificateResource{SANs: slices.Clone(domain)}).NamesKey() + storageLockKey := strings.Join([]string{ + storageLockPrefix, + certmagic.StorageKeys.Safe(storageIssuerKey), + certmagic.StorageKeys.Safe(storageNamesKey), + }, "/") + return &Service{ + Adapter: certificate.NewAdapter(C.TypeCloudflareOriginCA, tag), + logger: logger, + ctx: ctx, + cancel: cancel, + timeFunc: timeFunc, + httpClient: &http.Client{Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: timeFunc, + }, + ForceAttemptHTTP2: true, + }}, + storage: storage, + storageIssuerKey: storageIssuerKey, + storageNamesKey: storageNamesKey, + storageLockKey: storageLockKey, + apiToken: apiToken, + originCAKey: originCAKey, + domain: domain, + requestType: requestType, + requestedValidity: requestedValidity, + }, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + cachedCertificate, cachedLeaf, err := s.loadCachedCertificate() + if err != nil { + s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate")) + } else if cachedCertificate != nil { + s.setCurrentCertificate(cachedCertificate, cachedLeaf) + } + if cachedCertificate == nil { + err = s.issueAndStoreCertificate() + if err != nil { + return err + } + } else if s.shouldRenew(cachedLeaf, s.timeFunc()) { + err = s.issueAndStoreCertificate() + if err != nil { + s.logger.Warn(E.Cause(err, "renew cached Cloudflare Origin CA certificate")) + } + } + s.done = make(chan struct{}) + go s.refreshLoop() + return nil +} + +func (s *Service) Close() error { + s.cancel() + if done := s.done; done != nil { + <-done + } + if transport, loaded := s.httpClient.Transport.(*http.Transport); loaded { + transport.CloseIdleConnections() + } + return nil +} + +func (s *Service) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { + s.access.RLock() + certificate := s.currentCertificate + s.access.RUnlock() + if certificate == nil { + return nil, E.New("Cloudflare Origin CA certificate is unavailable") + } + return certificate, nil +} + +func (s *Service) refreshLoop() { + defer close(s.done) + var retryDelay time.Duration + for { + waitDuration := retryDelay + if waitDuration == 0 { + s.access.RLock() + leaf := s.currentLeaf + s.access.RUnlock() + if leaf == nil { + waitDuration = minimumRenewRetryDelay + } else { + refreshAt := leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf)) + waitDuration = refreshAt.Sub(s.timeFunc()) + if waitDuration < minimumRenewRetryDelay { + waitDuration = minimumRenewRetryDelay + } + } + } + timer := time.NewTimer(waitDuration) + select { + case <-s.ctx.Done(): + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + return + case <-timer.C: + } + err := s.issueAndStoreCertificate() + if err != nil { + s.logger.Error(E.Cause(err, "renew Cloudflare Origin CA certificate")) + s.access.RLock() + leaf := s.currentLeaf + s.access.RUnlock() + if leaf == nil { + retryDelay = minimumRenewRetryDelay + } else { + remaining := leaf.NotAfter.Sub(s.timeFunc()) + switch { + case remaining <= minimumRenewRetryDelay: + retryDelay = minimumRenewRetryDelay + case remaining < maximumRenewRetryDelay: + retryDelay = max(remaining/2, minimumRenewRetryDelay) + default: + retryDelay = maximumRenewRetryDelay + } + } + continue + } + retryDelay = 0 + } +} + +func (s *Service) shouldRenew(leaf *x509.Certificate, now time.Time) bool { + return !now.Before(leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf))) +} + +func (s *Service) effectiveRenewBefore(leaf *x509.Certificate) time.Duration { + lifetime := leaf.NotAfter.Sub(leaf.NotBefore) + if lifetime <= 0 { + return 0 + } + return min(lifetime/3, defaultRenewBefore) +} + +func (s *Service) issueAndStoreCertificate() error { + err := s.storage.Lock(s.ctx, s.storageLockKey) + if err != nil { + return E.Cause(err, "lock Cloudflare Origin CA certificate storage") + } + defer func() { + err = s.storage.Unlock(context.WithoutCancel(s.ctx), s.storageLockKey) + if err != nil { + s.logger.Warn(E.Cause(err, "unlock Cloudflare Origin CA certificate storage")) + } + }() + cachedCertificate, cachedLeaf, err := s.loadCachedCertificate() + if err != nil { + s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate")) + } else if cachedCertificate != nil && !s.shouldRenew(cachedLeaf, s.timeFunc()) { + s.setCurrentCertificate(cachedCertificate, cachedLeaf) + return nil + } + certificatePEM, privateKeyPEM, tlsCertificate, leaf, err := s.requestCertificate(s.ctx) + if err != nil { + return err + } + issuerData, err := json.Marshal(originCAIssuerData{ + RequestType: s.requestType, + RequestedValidity: s.requestedValidity, + }) + if err != nil { + return E.Cause(err, "encode Cloudflare Origin CA certificate metadata") + } + err = storeCertificateResource(s.ctx, s.storage, s.storageIssuerKey, certmagic.CertificateResource{ + SANs: slices.Clone(s.domain), + CertificatePEM: certificatePEM, + PrivateKeyPEM: privateKeyPEM, + IssuerData: issuerData, + }) + if err != nil { + return E.Cause(err, "store Cloudflare Origin CA certificate") + } + s.setCurrentCertificate(tlsCertificate, leaf) + s.logger.Info("updated Cloudflare Origin CA certificate, expires at ", leaf.NotAfter.Format(time.RFC3339)) + return nil +} + +func (s *Service) requestCertificate(ctx context.Context) ([]byte, []byte, *tls.Certificate, *x509.Certificate, error) { + var privateKey crypto.Signer + switch s.requestType { + case option.CloudflareOriginCARequestTypeOriginRSA: + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, nil, err + } + privateKey = rsaKey + case option.CloudflareOriginCARequestTypeOriginECC: + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, nil, nil, err + } + privateKey = ecKey + default: + return nil, nil, nil, nil, E.New("unsupported Cloudflare Origin CA request type: ", s.requestType) + } + privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "encode private key") + } + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: privateKeyDER, + }) + certificateRequestDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: s.domain[0]}, + DNSNames: s.domain, + }, privateKey) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "create certificate request") + } + certificateRequestPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: certificateRequestDER, + }) + requestBody, err := json.Marshal(originCARequest{ + CSR: string(certificateRequestPEM), + Hostnames: s.domain, + RequestType: string(s.requestType), + RequestedValidity: uint16(s.requestedValidity), + }) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "marshal request") + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, cloudflareOriginCAEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "create request") + } + request.Header.Set("Accept", "application/json") + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "sing-box/"+C.Version) + if s.apiToken != "" { + request.Header.Set("Authorization", "Bearer "+s.apiToken) + } else { + request.Header.Set("X-Auth-User-Service-Key", s.originCAKey) + } + response, err := s.httpClient.Do(request) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "request certificate from Cloudflare") + } + defer response.Body.Close() + responseBody, err := io.ReadAll(response.Body) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "read Cloudflare response") + } + var responseEnvelope originCAResponse + err = json.Unmarshal(responseBody, &responseEnvelope) + if err != nil && response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices { + return nil, nil, nil, nil, E.Cause(err, "decode Cloudflare response") + } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody) + } + if !responseEnvelope.Success { + return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody) + } + if responseEnvelope.Result.Certificate == "" { + return nil, nil, nil, nil, E.New("Cloudflare Origin CA response is missing certificate data") + } + certificatePEM := []byte(responseEnvelope.Result.Certificate) + tlsCertificate, leaf, err := parseKeyPair(certificatePEM, privateKeyPEM) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "parse issued certificate") + } + if !s.matchesCertificate(leaf) { + return nil, nil, nil, nil, E.New("issued Cloudflare Origin CA certificate does not match requested hostnames or key type") + } + return certificatePEM, privateKeyPEM, tlsCertificate, leaf, nil +} + +func (s *Service) loadCachedCertificate() (*tls.Certificate, *x509.Certificate, error) { + certificateResource, err := loadCertificateResource(s.ctx, s.storage, s.storageIssuerKey, s.storageNamesKey) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, nil, nil + } + return nil, nil, err + } + tlsCertificate, leaf, err := parseKeyPair(certificateResource.CertificatePEM, certificateResource.PrivateKeyPEM) + if err != nil { + return nil, nil, E.Cause(err, "parse cached key pair") + } + if s.timeFunc().After(leaf.NotAfter) { + return nil, nil, nil + } + if !s.matchesCertificate(leaf) { + return nil, nil, nil + } + return tlsCertificate, leaf, nil +} + +func (s *Service) matchesCertificate(leaf *x509.Certificate) bool { + if leaf == nil { + return false + } + leafHostnames := leaf.DNSNames + if len(leafHostnames) == 0 && leaf.Subject.CommonName != "" { + leafHostnames = []string{leaf.Subject.CommonName} + } + normalizedLeafHostnames, err := normalizeHostnames(leafHostnames) + if err != nil { + return false + } + if !slices.Equal(normalizedLeafHostnames, s.domain) { + return false + } + switch s.requestType { + case option.CloudflareOriginCARequestTypeOriginRSA: + return leaf.PublicKeyAlgorithm == x509.RSA + case option.CloudflareOriginCARequestTypeOriginECC: + return leaf.PublicKeyAlgorithm == x509.ECDSA + default: + return false + } +} + +func (s *Service) setCurrentCertificate(certificate *tls.Certificate, leaf *x509.Certificate) { + s.access.Lock() + s.currentCertificate = certificate + s.currentLeaf = leaf + s.access.Unlock() +} + +func normalizeHostnames(hostnames []string) ([]string, error) { + normalizedHostnames := make([]string, 0, len(hostnames)) + seen := make(map[string]struct{}, len(hostnames)) + for _, hostname := range hostnames { + normalizedHostname := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(hostname, "."))) + if normalizedHostname == "" { + return nil, E.New("hostname is empty") + } + if net.ParseIP(normalizedHostname) != nil { + return nil, E.New("hostname cannot be an IP address: ", normalizedHostname) + } + if strings.Contains(normalizedHostname, "*") { + if !strings.HasPrefix(normalizedHostname, "*.") || strings.Count(normalizedHostname, "*") != 1 { + return nil, E.New("invalid wildcard hostname: ", normalizedHostname) + } + suffix := strings.TrimPrefix(normalizedHostname, "*.") + if strings.Count(suffix, ".") == 0 { + return nil, E.New("wildcard hostname must cover a multi-label domain: ", normalizedHostname) + } + normalizedHostname = "*." + suffix + } + if _, loaded := seen[normalizedHostname]; loaded { + continue + } + seen[normalizedHostname] = struct{}{} + normalizedHostnames = append(normalizedHostnames, normalizedHostname) + } + slices.Sort(normalizedHostnames) + return normalizedHostnames, nil +} + +func parseKeyPair(certificatePEM []byte, privateKeyPEM []byte) (*tls.Certificate, *x509.Certificate, error) { + keyPair, err := tls.X509KeyPair(certificatePEM, privateKeyPEM) + if err != nil { + return nil, nil, err + } + if len(keyPair.Certificate) == 0 { + return nil, nil, E.New("certificate chain is empty") + } + leaf, err := x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + return nil, nil, err + } + keyPair.Leaf = leaf + return &keyPair, leaf, nil +} + +func storeCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, certificateResource certmagic.CertificateResource) error { + metaBytes, err := json.MarshalIndent(certificateResource, "", "\t") + if err != nil { + return err + } + namesKey := certificateResource.NamesKey() + keyValueList := []struct { + key string + value []byte + }{ + { + key: certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey), + value: certificateResource.PrivateKeyPEM, + }, + { + key: certmagic.StorageKeys.SiteCert(issuerKey, namesKey), + value: certificateResource.CertificatePEM, + }, + { + key: certmagic.StorageKeys.SiteMeta(issuerKey, namesKey), + value: metaBytes, + }, + } + for i, item := range keyValueList { + err = storage.Store(ctx, item.key, item.value) + if err != nil { + for j := i - 1; j >= 0; j-- { + storage.Delete(ctx, keyValueList[j].key) + } + return err + } + } + return nil +} + +func loadCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, namesKey string) (certmagic.CertificateResource, error) { + privateKeyPEM, err := storage.Load(ctx, certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey)) + if err != nil { + return certmagic.CertificateResource{}, err + } + certificatePEM, err := storage.Load(ctx, certmagic.StorageKeys.SiteCert(issuerKey, namesKey)) + if err != nil { + return certmagic.CertificateResource{}, err + } + metaBytes, err := storage.Load(ctx, certmagic.StorageKeys.SiteMeta(issuerKey, namesKey)) + if err != nil { + return certmagic.CertificateResource{}, err + } + var certificateResource certmagic.CertificateResource + err = json.Unmarshal(metaBytes, &certificateResource) + if err != nil { + return certmagic.CertificateResource{}, E.Cause(err, "decode Cloudflare Origin CA certificate metadata") + } + certificateResource.PrivateKeyPEM = privateKeyPEM + certificateResource.CertificatePEM = certificatePEM + return certificateResource, nil +} + +func buildOriginCAError(statusCode int, responseErrors []originCAResponseError, responseBody []byte) error { + if len(responseErrors) > 0 { + messageList := make([]string, 0, len(responseErrors)) + for _, responseError := range responseErrors { + if responseError.Message == "" { + continue + } + if responseError.Code != 0 { + messageList = append(messageList, responseError.Message+" (code "+strconv.Itoa(responseError.Code)+")") + } else { + messageList = append(messageList, responseError.Message) + } + } + if len(messageList) > 0 { + return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", strings.Join(messageList, ", ")) + } + } + responseText := strings.TrimSpace(string(responseBody)) + if responseText == "" { + return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode) + } + return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", responseText) +} + +type originCARequest struct { + CSR string `json:"csr"` + Hostnames []string `json:"hostnames"` + RequestType string `json:"request_type"` + RequestedValidity uint16 `json:"requested_validity"` +} + +type originCAResponse struct { + Success bool `json:"success"` + Errors []originCAResponseError `json:"errors"` + Result originCAResponseResult `json:"result"` +} + +type originCAResponseError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type originCAResponseResult struct { + Certificate string `json:"certificate"` +} + +type originCAIssuerData struct { + RequestType option.CloudflareOriginCARequestType `json:"request_type,omitempty"` + RequestedValidity option.CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"` +} diff --git a/service/resolved/resolve1.go b/service/resolved/resolve1.go new file mode 100644 index 00000000..ed1ee41a --- /dev/null +++ b/service/resolved/resolve1.go @@ -0,0 +1,656 @@ +//go:build linux + +package resolved + +import ( + "context" + "errors" + "fmt" + "net/netip" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + + "github.com/godbus/dbus/v5" + mDNS "github.com/miekg/dns" +) + +type resolve1Manager Service + +type Address struct { + IfIndex int32 + Family int32 + Address []byte +} + +type Name struct { + IfIndex int32 + Hostname string +} + +type ResourceRecord struct { + IfIndex int32 + Type uint16 + Class uint16 + Data []byte +} + +type SRVRecord struct { + Priority uint16 + Weight uint16 + Port uint16 + Hostname string + Addresses []Address + CNAME string +} + +type TXTRecord []byte + +type LinkDNS struct { + Family int32 + Address []byte +} + +type LinkDNSEx struct { + Family int32 + Address []byte + Port uint16 + Name string +} + +type LinkDomain struct { + Domain string + RoutingOnly bool +} + +func (t *resolve1Manager) getLink(ifIndex int32) (*TransportLink, *dbus.Error) { + link, loaded := t.links[ifIndex] + if !loaded { + link = &TransportLink{} + t.links[ifIndex] = link + iif, err := t.network.InterfaceFinder().ByIndex(int(ifIndex)) + if err != nil { + return nil, wrapError(err) + } + link.iif = iif + } + return link, nil +} + +func (t *resolve1Manager) getSenderProcess(sender dbus.Sender) (int32, error) { + var senderPid int32 + dbusObject := t.systemBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus") + if dbusObject == nil { + return 0, E.New("missing dbus object") + } + err := dbusObject.Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, string(sender)).Store(&senderPid) + if err != nil { + return 0, E.Cause(err, "GetConnectionUnixProcessID") + } + return senderPid, nil +} + +func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundContext { + var metadata adapter.InboundContext + metadata.Inbound = t.Tag() + metadata.InboundType = C.TypeResolved + senderPid, err := t.getSenderProcess(sender) + if err != nil { + return metadata + } + var processInfo adapter.ConnectionOwner + metadata.ProcessInfo = &processInfo + processInfo.ProcessID = uint32(senderPid) + + processPath, err := os.Readlink(F.ToString("/proc/", senderPid, "/exe")) + if err == nil { + processInfo.ProcessPath = processPath + } else { + processPath, err = os.Readlink(F.ToString("/proc/", senderPid, "/comm")) + if err == nil { + processInfo.ProcessPath = processPath + } + } + + var uidFound bool + statusContent, err := os.ReadFile(F.ToString("/proc/", senderPid, "/status")) + if err == nil { + for _, line := range strings.Split(string(statusContent), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "Uid:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + uid, parseErr := strconv.ParseUint(fields[1], 10, 32) + if parseErr != nil { + break + } + processInfo.UserId = int32(uid) + uidFound = true + if osUser, _ := user.LookupId(F.ToString(uid)); osUser != nil { + processInfo.UserName = osUser.Username + } + break + } + } + } + } + if !uidFound { + metadata.ProcessInfo.UserId = -1 + } + return metadata +} + +func (t *resolve1Manager) log(sender dbus.Sender, message ...any) { + metadata := t.createMetadata(sender) + if metadata.ProcessInfo != nil { + var prefix string + if metadata.ProcessInfo.ProcessPath != "" { + prefix = filepath.Base(metadata.ProcessInfo.ProcessPath) + } else if metadata.ProcessInfo.UserName != "" { + prefix = F.ToString("user:", metadata.ProcessInfo.UserName) + } else if metadata.ProcessInfo.UserId != 0 { + prefix = F.ToString("uid:", metadata.ProcessInfo.UserId) + } + t.logger.Info("(", prefix, ") ", F.ToString(message...)) + } else { + t.logger.Info(F.ToString(message...)) + } +} + +func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context.Context { + ctx := log.ContextWithNewID(t.ctx) + metadata := t.createMetadata(sender) + if metadata.ProcessInfo != nil { + var prefix string + if metadata.ProcessInfo.ProcessPath != "" { + prefix = filepath.Base(metadata.ProcessInfo.ProcessPath) + } else if metadata.ProcessInfo.UserName != "" { + prefix = F.ToString("user:", metadata.ProcessInfo.UserName) + } else if metadata.ProcessInfo.UserId != 0 { + prefix = F.ToString("uid:", metadata.ProcessInfo.UserId) + } + t.logger.InfoContext(ctx, "(", prefix, ") ", strings.Join(F.MapToString(message), " ")) + } else { + t.logger.InfoContext(ctx, strings.Join(F.MapToString(message), " ")) + } + return adapter.WithContext(ctx, &metadata) +} + +func familyToString(family int32) string { + switch family { + case syscall.AF_UNSPEC: + return "AF_UNSPEC" + case syscall.AF_INET: + return "AF_INET" + case syscall.AF_INET6: + return "AF_INET6" + default: + return F.ToString(family) + } +} + +func (t *resolve1Manager) ResolveHostname(sender dbus.Sender, ifIndex int32, hostname string, family int32, flags uint64) (addresses []Address, canonical string, outflags uint64, err *dbus.Error) { + t.linkAccess.Lock() + link, err := t.getLink(ifIndex) + if err != nil { + return + } + t.linkAccess.Unlock() + var strategy C.DomainStrategy + switch family { + case syscall.AF_UNSPEC: + strategy = C.DomainStrategyAsIS + case syscall.AF_INET: + strategy = C.DomainStrategyIPv4Only + case syscall.AF_INET6: + strategy = C.DomainStrategyIPv6Only + } + ctx := t.logRequest(sender, "ResolveHostname ", link.iif.Name, " ", hostname, " ", familyToString(family), " ", flags) + responseAddresses, lookupErr := t.dnsRouter.Lookup(ctx, hostname, adapter.DNSQueryOptions{ + LookupStrategy: strategy, + }) + if lookupErr != nil { + err = wrapError(err) + return + } + addresses = common.Map(responseAddresses, func(it netip.Addr) Address { + var addrFamily int32 + if it.Is4() { + addrFamily = syscall.AF_INET + } else { + addrFamily = syscall.AF_INET6 + } + return Address{ + IfIndex: ifIndex, + Family: addrFamily, + Address: it.AsSlice(), + } + }) + canonical = mDNS.CanonicalName(hostname) + return +} + +func (t *resolve1Manager) ResolveAddress(sender dbus.Sender, ifIndex int32, family int32, address []byte, flags uint64) (names []Name, outflags uint64, err *dbus.Error) { + t.linkAccess.Lock() + link, err := t.getLink(ifIndex) + if err != nil { + return + } + t.linkAccess.Unlock() + addr, ok := netip.AddrFromSlice(address) + if !ok { + err = wrapError(E.New("invalid address")) + return + } + var nibbles []string + for i := len(address) - 1; i >= 0; i-- { + b := address[i] + nibbles = append(nibbles, fmt.Sprintf("%x", b&0x0F)) + nibbles = append(nibbles, fmt.Sprintf("%x", b>>4)) + } + var ptrDomain string + if addr.Is4() { + ptrDomain = strings.Join(nibbles, ".") + ".in-addr.arpa." + } else { + ptrDomain = strings.Join(nibbles, ".") + ".ip6.arpa." + } + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{ + { + Name: mDNS.Fqdn(ptrDomain), + Qtype: mDNS.TypePTR, + Qclass: mDNS.ClassINET, + }, + }, + } + ctx := t.logRequest(sender, "ResolveAddress ", link.iif.Name, familyToString(family), addr, flags) + var metadata adapter.InboundContext + metadata.InboundType = t.Type() + metadata.Inbound = t.Tag() + response, lookupErr := t.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), request, adapter.DNSQueryOptions{}) + if lookupErr != nil { + err = wrapError(err) + return + } + if response.Rcode != mDNS.RcodeSuccess { + err = rcodeError(response.Rcode) + return + } + for _, rawRR := range response.Answer { + switch rr := rawRR.(type) { + case *mDNS.PTR: + names = append(names, Name{ + IfIndex: ifIndex, + Hostname: rr.Ptr, + }) + } + } + return +} + +func (t *resolve1Manager) ResolveRecord(sender dbus.Sender, ifIndex int32, hostname string, qClass uint16, qType uint16, flags uint64) (records []ResourceRecord, outflags uint64, err *dbus.Error) { + t.linkAccess.Lock() + link, err := t.getLink(ifIndex) + if err != nil { + return + } + t.linkAccess.Unlock() + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{ + { + Name: mDNS.Fqdn(hostname), + Qtype: qType, + Qclass: qClass, + }, + }, + } + ctx := t.logRequest(sender, "ResolveRecord", link.iif.Name, hostname, mDNS.Class(qClass), mDNS.Type(qType), flags) + var metadata adapter.InboundContext + metadata.InboundType = t.Type() + metadata.Inbound = t.Tag() + response, exchangeErr := t.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), request, adapter.DNSQueryOptions{}) + if exchangeErr != nil { + err = wrapError(exchangeErr) + return + } + if response.Rcode != mDNS.RcodeSuccess { + err = rcodeError(response.Rcode) + return + } + for _, rr := range response.Answer { + var record ResourceRecord + record.IfIndex = ifIndex + record.Type = rr.Header().Rrtype + record.Class = rr.Header().Class + data := make([]byte, mDNS.Len(rr)) + _, unpackErr := mDNS.PackRR(rr, data, 0, nil, false) + if unpackErr != nil { + err = wrapError(unpackErr) + } + record.Data = data + records = append(records, record) + } + return +} + +func (t *resolve1Manager) ResolveService(sender dbus.Sender, ifIndex int32, hostname string, sType string, domain string, family int32, flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err *dbus.Error) { + t.linkAccess.Lock() + link, err := t.getLink(ifIndex) + if err != nil { + return + } + t.linkAccess.Unlock() + + serviceName := hostname + if hostname != "" && !strings.HasSuffix(hostname, ".") { + serviceName += "." + } + serviceName += sType + if !strings.HasSuffix(serviceName, ".") { + serviceName += "." + } + serviceName += domain + if !strings.HasSuffix(serviceName, ".") { + serviceName += "." + } + + ctx := t.logRequest(sender, "ResolveService ", link.iif.Name, " ", hostname, " ", sType, " ", domain, " ", familyToString(family), " ", flags) + + srvRequest := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{ + { + Name: serviceName, + Qtype: mDNS.TypeSRV, + Qclass: mDNS.ClassINET, + }, + }, + } + var metadata adapter.InboundContext + metadata.InboundType = t.Type() + metadata.Inbound = t.Tag() + srvResponse, exchangeErr := t.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), srvRequest, adapter.DNSQueryOptions{}) + if exchangeErr != nil { + err = wrapError(exchangeErr) + return + } + if srvResponse.Rcode != mDNS.RcodeSuccess { + err = rcodeError(srvResponse.Rcode) + return + } + + txtRequest := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{ + { + Name: serviceName, + Qtype: mDNS.TypeTXT, + Qclass: mDNS.ClassINET, + }, + }, + } + + txtResponse, exchangeErr := t.dnsRouter.Exchange(ctx, txtRequest, adapter.DNSQueryOptions{}) + if exchangeErr != nil { + err = wrapError(exchangeErr) + return + } + + for _, rawRR := range srvResponse.Answer { + switch rr := rawRR.(type) { + case *mDNS.SRV: + var srvRecord SRVRecord + srvRecord.Priority = rr.Priority + srvRecord.Weight = rr.Weight + srvRecord.Port = rr.Port + srvRecord.Hostname = rr.Target + + var strategy C.DomainStrategy + switch family { + case syscall.AF_UNSPEC: + strategy = C.DomainStrategyAsIS + case syscall.AF_INET: + strategy = C.DomainStrategyIPv4Only + case syscall.AF_INET6: + strategy = C.DomainStrategyIPv6Only + } + + addrs, lookupErr := t.dnsRouter.Lookup(ctx, rr.Target, adapter.DNSQueryOptions{ + LookupStrategy: strategy, + }) + if lookupErr == nil { + srvRecord.Addresses = common.Map(addrs, func(it netip.Addr) Address { + var addrFamily int32 + if it.Is4() { + addrFamily = syscall.AF_INET + } else { + addrFamily = syscall.AF_INET6 + } + return Address{ + IfIndex: ifIndex, + Family: addrFamily, + Address: it.AsSlice(), + } + }) + } + for _, a := range srvResponse.Answer { + if cname, ok := a.(*mDNS.CNAME); ok && cname.Header().Name == rr.Target { + srvRecord.CNAME = cname.Target + break + } + } + srvData = append(srvData, srvRecord) + } + } + for _, rawRR := range txtResponse.Answer { + switch rr := rawRR.(type) { + case *mDNS.TXT: + data := make([]byte, mDNS.Len(rr)) + _, packErr := mDNS.PackRR(rr, data, 0, nil, false) + if packErr == nil { + txtData = append(txtData, data) + } + } + } + canonicalName = mDNS.CanonicalName(hostname) + canonicalType = mDNS.CanonicalName(sType) + canonicalDomain = mDNS.CanonicalName(domain) + return +} + +func (t *resolve1Manager) SetLinkDNS(sender dbus.Sender, ifIndex int32, addresses []LinkDNS) *dbus.Error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + link, err := t.getLink(ifIndex) + if err != nil { + return wrapError(err) + } + link.address = addresses + if len(addresses) > 0 { + t.log(sender, "SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNS) string { + return M.AddrFromIP(it.Address).String() + }), ", ")) + } else { + t.log(sender, "SetLinkDNS ", link.iif.Name, " (empty)") + } + return t.postUpdate(link) +} + +func (t *resolve1Manager) SetLinkDNSEx(sender dbus.Sender, ifIndex int32, addresses []LinkDNSEx) *dbus.Error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + link, err := t.getLink(ifIndex) + if err != nil { + return wrapError(err) + } + link.addressEx = addresses + if len(addresses) > 0 { + t.log(sender, "SetLinkDNSEx ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNSEx) string { + return M.SocksaddrFrom(M.AddrFromIP(it.Address), it.Port).String() + }), ", ")) + } else { + t.log(sender, "SetLinkDNSEx ", link.iif.Name, " (empty)") + } + return t.postUpdate(link) +} + +func (t *resolve1Manager) SetLinkDomains(sender dbus.Sender, ifIndex int32, domains []LinkDomain) *dbus.Error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + link, err := t.getLink(ifIndex) + if err != nil { + return wrapError(err) + } + link.domain = domains + if len(domains) > 0 { + t.log(sender, "SetLinkDomains ", link.iif.Name, " ", strings.Join(common.Map(domains, func(domain LinkDomain) string { + if !domain.RoutingOnly { + return domain.Domain + } else { + return "~" + domain.Domain + } + }), ", ")) + } else { + t.log(sender, "SetLinkDomains ", link.iif.Name, " (empty)") + } + return t.postUpdate(link) +} + +func (t *resolve1Manager) SetLinkDefaultRoute(sender dbus.Sender, ifIndex int32, defaultRoute bool) *dbus.Error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + link, err := t.getLink(ifIndex) + if err != nil { + return err + } + link.defaultRoute = defaultRoute + if defaultRoute { + t.defaultRouteSequence = append(common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }), ifIndex) + } else { + t.defaultRouteSequence = common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }) + } + var defaultRouteString string + if defaultRoute { + defaultRouteString = "yes" + } else { + defaultRouteString = "no" + } + t.log(sender, "SetLinkDefaultRoute ", link.iif.Name, " ", defaultRouteString) + return t.postUpdate(link) +} + +func (t *resolve1Manager) SetLinkLLMNR(ifIndex int32, llmnrMode string) *dbus.Error { + return nil +} + +func (t *resolve1Manager) SetLinkMulticastDNS(ifIndex int32, mdnsMode string) *dbus.Error { + return nil +} + +func (t *resolve1Manager) SetLinkDNSOverTLS(sender dbus.Sender, ifIndex int32, dotMode string) *dbus.Error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + link, err := t.getLink(ifIndex) + if err != nil { + return wrapError(err) + } + switch dotMode { + case "yes": + link.dnsOverTLS = true + case "": + dotMode = "no" + fallthrough + case "opportunistic", "no": + link.dnsOverTLS = false + } + t.log(sender, "SetLinkDNSOverTLS ", link.iif.Name, " ", dotMode) + return t.postUpdate(link) +} + +func (t *resolve1Manager) SetLinkDNSSEC(ifIndex int32, dnssecMode string) *dbus.Error { + return nil +} + +func (t *resolve1Manager) SetLinkDNSSECNegativeTrustAnchors(ifIndex int32, domains []string) *dbus.Error { + return nil +} + +func (t *resolve1Manager) RevertLink(sender dbus.Sender, ifIndex int32) *dbus.Error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + link, err := t.getLink(ifIndex) + if err != nil { + return wrapError(err) + } + delete(t.links, ifIndex) + t.log(sender, "RevertLink ", link.iif.Name) + return t.postUpdate(link) +} + +// TODO: implement RegisterService, UnregisterService + +func (t *resolve1Manager) RegisterService(sender dbus.Sender, identifier string, nameTemplate string, serviceType string, port uint16, priority uint16, weight uint16, txtRecords []TXTRecord) (objectPath dbus.ObjectPath, dbusErr *dbus.Error) { + return "", wrapError(E.New("not implemented")) +} + +func (t *resolve1Manager) UnregisterService(sender dbus.Sender, servicePath dbus.ObjectPath) error { + return wrapError(E.New("not implemented")) +} + +func (t *resolve1Manager) ResetStatistics() *dbus.Error { + return nil +} + +func (t *resolve1Manager) FlushCaches(sender dbus.Sender) *dbus.Error { + t.dnsRouter.ClearCache() + t.log(sender, "FlushCaches") + return nil +} + +func (t *resolve1Manager) ResetServerFeatures() *dbus.Error { + return nil +} + +func (t *resolve1Manager) postUpdate(link *TransportLink) *dbus.Error { + if t.updateCallback != nil { + return wrapError(t.updateCallback(link)) + } + return nil +} + +func rcodeError(rcode int) *dbus.Error { + return dbus.NewError("org.freedesktop.resolve1.DnsError."+mDNS.RcodeToString[rcode], []any{mDNS.RcodeToString[rcode]}) +} + +func wrapError(err error) *dbus.Error { + if err == nil { + return nil + } + var rcode dns.RcodeError + if errors.As(err, &rcode) { + return rcodeError(int(rcode)) + } + return dbus.MakeFailedError(err) +} diff --git a/service/resolved/service.go b/service/resolved/service.go new file mode 100644 index 00000000..eaedc09d --- /dev/null +++ b/service/resolved/service.go @@ -0,0 +1,254 @@ +//go:build linux + +package resolved + +import ( + "context" + "net" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/listener" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + dnsOutbound "github.com/sagernet/sing-box/protocol/dns" + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + 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/x/list" + "github.com/sagernet/sing/service" + + "github.com/godbus/dbus/v5" + mDNS "github.com/miekg/dns" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, NewService) +} + +type Service struct { + boxService.Adapter + ctx context.Context + logger log.ContextLogger + network adapter.NetworkManager + dnsRouter adapter.DNSRouter + listener *listener.Listener + systemBus *dbus.Conn + linkAccess sync.RWMutex + links map[int32]*TransportLink + defaultRouteSequence []int32 + networkUpdateCallback *list.Element[tun.NetworkUpdateCallback] + updateCallback func(*TransportLink) error + deleteCallback func(*TransportLink) +} + +type TransportLink struct { + iif *control.Interface + address []LinkDNS + addressEx []LinkDNSEx + domain []LinkDomain + defaultRoute bool + dnsOverTLS bool + // dnsOverTLSFallback bool +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) { + inbound := &Service{ + Adapter: boxService.NewAdapter(C.TypeResolved, tag), + ctx: ctx, + logger: logger, + network: service.FromContext[adapter.NetworkManager](ctx), + dnsRouter: service.FromContext[adapter.DNSRouter](ctx), + links: make(map[int32]*TransportLink), + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP, N.NetworkUDP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + OOBPacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) + return inbound, nil +} + +func (i *Service) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateInitialize: + inboundManager := service.FromContext[adapter.ServiceManager](i.ctx) + for _, transport := range inboundManager.Services() { + if transport.Type() == C.TypeResolved && transport != i { + return E.New("multiple resolved service are not supported") + } + } + systemBus, err := dbus.SystemBus() + if err != nil { + return err + } + i.systemBus = systemBus + err = systemBus.Export((*resolve1Manager)(i), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager") + if err != nil { + return err + } + reply, err := systemBus.RequestName("org.freedesktop.resolve1", dbus.NameFlagDoNotQueue) + if err != nil { + return err + } + switch reply { + case dbus.RequestNameReplyPrimaryOwner: + case dbus.RequestNameReplyExists: + return E.New("D-Bus object already exists, maybe real resolved is running") + default: + return E.New("unknown request name reply: ", reply) + } + i.networkUpdateCallback = i.network.NetworkMonitor().RegisterCallback(i.onNetworkUpdate) + case adapter.StartStateStart: + err := i.listener.Start() + if err != nil { + return err + } + } + return nil +} + +func (i *Service) Close() error { + if i.networkUpdateCallback != nil { + i.network.NetworkMonitor().UnregisterCallback(i.networkUpdateCallback) + } + if i.systemBus != nil { + i.systemBus.ReleaseName("org.freedesktop.resolve1") + i.systemBus.Close() + } + return i.listener.Close() +} + +func (i *Service) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + metadata.Destination = M.Socksaddr{} + for { + conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) + err := dnsOutbound.HandleStreamDNSRequest(ctx, i.dnsRouter, conn, metadata) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + return + } + } +} + +func (i *Service) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { + go i.exchangePacket(buffer, oob, source) +} + +func (i *Service) exchangePacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { + ctx := log.ContextWithNewID(i.ctx) + err := i.exchangePacket0(ctx, buffer, oob, source) + if err != nil { + i.logger.ErrorContext(ctx, "process DNS packet: ", err) + } +} + +func (i *Service) exchangePacket0(ctx context.Context, buffer *buf.Buffer, oob []byte, source M.Socksaddr) error { + var message mDNS.Msg + err := message.Unpack(buffer.Bytes()) + buffer.Release() + if err != nil { + return E.Cause(err, "unpack request") + } + var metadata adapter.InboundContext + metadata.Source = source + metadata.InboundType = i.Type() + metadata.Inbound = i.Tag() + response, err := i.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{}) + if err != nil { + return err + } + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 0) + if err != nil { + return err + } + defer responseBuffer.Release() + _, _, err = i.listener.UDPConn().WriteMsgUDPAddrPort(responseBuffer.Bytes(), oob, source.AddrPort()) + return err +} + +func (i *Service) onNetworkUpdate() { + i.linkAccess.Lock() + defer i.linkAccess.Unlock() + var deleteIfIndex []int + for ifIndex, link := range i.links { + iif, err := i.network.InterfaceFinder().ByIndex(int(ifIndex)) + if err != nil || iif != link.iif { + deleteIfIndex = append(deleteIfIndex, int(ifIndex)) + } + i.defaultRouteSequence = common.Filter(i.defaultRouteSequence, func(it int32) bool { + return it != ifIndex + }) + if i.deleteCallback != nil { + i.deleteCallback(link) + } + } + for _, ifIndex := range deleteIfIndex { + delete(i.links, int32(ifIndex)) + } +} + +func (conf *TransportLink) nameList(ndots int, name string) []string { + search := common.Map(common.Filter(conf.domain, func(it LinkDomain) bool { + return !it.RoutingOnly + }), func(it LinkDomain) string { + return it.Domain + }) + + l := len(name) + rooted := l > 0 && name[l-1] == '.' + if l > 254 || l == 254 && !rooted { + return nil + } + + if rooted { + if avoidDNS(name) { + return nil + } + return []string{name} + } + + hasNdots := strings.Count(name, ".") >= ndots + name += "." + // l++ + + names := make([]string, 0, 1+len(search)) + if hasNdots && !avoidDNS(name) { + names = append(names, name) + } + for _, suffix := range search { + fqdn := name + suffix + if !avoidDNS(fqdn) && len(fqdn) <= 254 { + names = append(names, fqdn) + } + } + if !hasNdots && !avoidDNS(name) { + names = append(names, name) + } + return names +} + +func avoidDNS(name string) bool { + if name == "" { + return true + } + if name[len(name)-1] == '.' { + name = name[:len(name)-1] + } + return strings.HasSuffix(name, ".onion") +} diff --git a/service/resolved/stub.go b/service/resolved/stub.go new file mode 100644 index 00000000..ede64691 --- /dev/null +++ b/service/resolved/stub.go @@ -0,0 +1,27 @@ +//go:build !linux + +package resolved + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + boxService "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" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) { + return nil, E.New("resolved service is only supported on Linux") + }) +} + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) { + return nil, E.New("resolved DNS server is only supported on Linux") + }) +} diff --git a/service/resolved/transport.go b/service/resolved/transport.go new file mode 100644 index 00000000..ac20663a --- /dev/null +++ b/service/resolved/transport.go @@ -0,0 +1,307 @@ +//go:build linux + +package resolved + +import ( + "context" + "net/netip" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, NewTransport) +} + +var _ adapter.DNSTransport = (*Transport)(nil) + +type Transport struct { + dns.TransportAdapter + ctx context.Context + logger logger.ContextLogger + serviceTag string + acceptDefaultResolvers bool + ndots int + timeout time.Duration + attempts int + rotate bool + service *Service + linkAccess sync.RWMutex + linkServers map[*TransportLink]*LinkServers +} + +type LinkServers struct { + Link *TransportLink + Servers []adapter.DNSTransport + serverOffset uint32 +} + +func (c *LinkServers) ServerOffset(rotate bool) uint32 { + if rotate { + return atomic.AddUint32(&c.serverOffset, 1) - 1 + } + return 0 +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) { + return &Transport{ + TransportAdapter: dns.NewTransportAdapter(C.DNSTypeDHCP, tag, nil), + ctx: ctx, + logger: logger, + serviceTag: options.Service, + acceptDefaultResolvers: options.AcceptDefaultResolvers, + // ndots: options.NDots, + // timeout: time.Duration(options.Timeout), + // attempts: options.Attempts, + // rotate: options.Rotate, + ndots: 1, + timeout: 5 * time.Second, + attempts: 2, + linkServers: make(map[*TransportLink]*LinkServers), + }, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateInitialize { + return nil + } + serviceManager := service.FromContext[adapter.ServiceManager](t.ctx) + service, loaded := serviceManager.Get(t.serviceTag) + if !loaded { + return E.New("service not found: ", t.serviceTag) + } + resolvedInbound, isResolved := service.(*Service) + if !isResolved { + return E.New("service is not resolved: ", t.serviceTag) + } + resolvedInbound.updateCallback = t.updateTransports + resolvedInbound.deleteCallback = t.deleteTransport + t.service = resolvedInbound + return nil +} + +func (t *Transport) Close() error { + t.linkAccess.RLock() + defer t.linkAccess.RUnlock() + for _, servers := range t.linkServers { + for _, server := range servers.Servers { + server.Close() + } + } + return nil +} + +func (t *Transport) Reset() { + t.linkAccess.RLock() + defer t.linkAccess.RUnlock() + for _, servers := range t.linkServers { + for _, server := range servers.Servers { + server.Reset() + } + } +} + +func (t *Transport) updateTransports(link *TransportLink) error { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + if servers, loaded := t.linkServers[link]; loaded { + for _, server := range servers.Servers { + server.Close() + } + } + serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{ + BindInterface: link.iif.Name, + UDPFragmentDefault: true, + })) + var transports []adapter.DNSTransport + for _, address := range link.address { + serverAddr, ok := netip.AddrFromSlice(address.Address) + if !ok { + return os.ErrInvalid + } + if link.dnsOverTLS { + tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.String(), option.OutboundTLSOptions{ + Enabled: true, + ServerName: serverAddr.String(), + })) + transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53), tlsConfig)) + + } else { + transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53))) + } + } + for _, address := range link.addressEx { + serverAddr, ok := netip.AddrFromSlice(address.Address) + if !ok { + return os.ErrInvalid + } + if link.dnsOverTLS { + var serverName string + if address.Name != "" { + serverName = address.Name + } else { + serverName = serverAddr.String() + } + tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.String(), option.OutboundTLSOptions{ + Enabled: true, + ServerName: serverName, + })) + transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port), tlsConfig)) + + } else { + transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port))) + } + } + t.linkServers[link] = &LinkServers{ + Link: link, + Servers: transports, + } + return nil +} + +func (t *Transport) deleteTransport(link *TransportLink) { + t.linkAccess.Lock() + defer t.linkAccess.Unlock() + servers, loaded := t.linkServers[link] + if !loaded { + return + } + for _, server := range servers.Servers { + server.Close() + } + delete(t.linkServers, link) +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + var selectedLink *TransportLink + t.service.linkAccess.RLock() + for _, link := range t.service.links { + for _, domain := range link.domain { + if domain.Domain == "." && domain.RoutingOnly && !t.acceptDefaultResolvers { + continue + } + if strings.HasSuffix(question.Name, domain.Domain) { + selectedLink = link + } + } + } + if selectedLink == nil && t.acceptDefaultResolvers { + for l := len(t.service.defaultRouteSequence); l > 0; l-- { + selectedLink = t.service.links[t.service.defaultRouteSequence[l-1]] + if len(selectedLink.address) > 0 || len(selectedLink.addressEx) > 0 { + break + } + } + } + t.service.linkAccess.RUnlock() + if selectedLink == nil { + return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil + } + t.linkAccess.RLock() + servers := t.linkServers[selectedLink] + t.linkAccess.RUnlock() + if len(servers.Servers) == 0 { + return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil + } + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + return t.exchangeParallel(ctx, servers, message) + } else { + return t.exchangeSingleRequest(ctx, servers, message) + } +} + +func (t *Transport) exchangeSingleRequest(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) { + var lastErr error + for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) { + response, err := t.tryOneName(ctx, servers, message, fqdn) + if err != nil { + lastErr = err + continue + } + return response, nil + } + return nil, lastErr +} + +func (t *Transport) tryOneName(ctx context.Context, servers *LinkServers, message *mDNS.Msg, fqdn string) (*mDNS.Msg, error) { + serverOffset := servers.ServerOffset(t.rotate) + sLen := uint32(len(servers.Servers)) + var lastErr error + for i := 0; i < t.attempts; i++ { + for j := uint32(0); j < sLen; j++ { + server := servers.Servers[(serverOffset+j)%sLen] + question := message.Question[0] + question.Name = fqdn + exchangeMessage := *message + exchangeMessage.Question = []mDNS.Question{question} + exchangeCtx, cancel := context.WithTimeout(ctx, t.timeout) + response, err := server.Exchange(exchangeCtx, &exchangeMessage) + cancel() + if err != nil { + lastErr = err + continue + } + return response, nil + } + } + return nil, E.Cause(lastErr, fqdn) +} + +func (t *Transport) exchangeParallel(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) { + returned := make(chan struct{}) + defer close(returned) + type queryResult struct { + response *mDNS.Msg + err error + } + results := make(chan queryResult) + startRacer := func(ctx context.Context, fqdn string) { + response, err := t.tryOneName(ctx, servers, message, fqdn) + select { + case results <- queryResult{response, err}: + case <-returned: + } + } + queryCtx, queryCancel := context.WithCancel(ctx) + defer queryCancel() + var nameCount int + for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) { + nameCount++ + go startRacer(queryCtx, fqdn) + } + var errors []error + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case result := <-results: + if result.err == nil { + return result.response, nil + } + errors = append(errors, result.err) + if len(errors) == nameCount { + return nil, E.Errors(errors...) + } + } + } +} diff --git a/service/ssmapi/api.go b/service/ssmapi/api.go new file mode 100644 index 00000000..6a067c50 --- /dev/null +++ b/service/ssmapi/api.go @@ -0,0 +1,177 @@ +package ssmapi + +import ( + "net/http" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/logger" + sHTTP "github.com/sagernet/sing/protocol/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +type APIServer struct { + logger logger.Logger + traffic *TrafficManager + user *UserManager +} + +func NewAPIServer(logger logger.Logger, traffic *TrafficManager, user *UserManager) *APIServer { + return &APIServer{ + logger: logger, + traffic: traffic, + user: user, + } +} + +func (s *APIServer) Route(r chi.Router) { + r.Route("/server/v1", func(r chi.Router) { + r.Use(func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + s.logger.Debug(request.Method, " ", request.RequestURI, " ", sHTTP.SourceAddress(request)) + handler.ServeHTTP(writer, request) + }) + }) + r.Get("/", s.getServerInfo) + r.Get("/users", s.listUser) + r.Post("/users", s.addUser) + r.Get("/users/{username}", s.getUser) + r.Put("/users/{username}", s.updateUser) + r.Delete("/users/{username}", s.deleteUser) + r.Get("/stats", s.getStats) + }) +} + +func (s *APIServer) getServerInfo(writer http.ResponseWriter, request *http.Request) { + render.JSON(writer, request, render.M{ + "server": "sing-box " + C.Version, + "apiVersion": "v1", + }) +} + +type UserObject struct { + UserName string `json:"username"` + Password string `json:"uPSK,omitempty"` + DownlinkBytes int64 `json:"downlinkBytes"` + UplinkBytes int64 `json:"uplinkBytes"` + DownlinkPackets int64 `json:"downlinkPackets"` + UplinkPackets int64 `json:"uplinkPackets"` + TCPSessions int64 `json:"tcpSessions"` + UDPSessions int64 `json:"udpSessions"` +} + +func (s *APIServer) listUser(writer http.ResponseWriter, request *http.Request) { + render.JSON(writer, request, render.M{ + "users": s.user.List(), + }) +} + +func (s *APIServer) addUser(writer http.ResponseWriter, request *http.Request) { + var addRequest struct { + UserName string `json:"username"` + Password string `json:"uPSK"` + } + err := render.DecodeJSON(request.Body, &addRequest) + if err != nil { + render.Status(request, http.StatusBadRequest) + render.PlainText(writer, request, err.Error()) + return + } + err = s.user.Add(addRequest.UserName, addRequest.Password) + if err != nil { + render.Status(request, http.StatusBadRequest) + render.PlainText(writer, request, err.Error()) + return + } + writer.WriteHeader(http.StatusCreated) +} + +func (s *APIServer) getUser(writer http.ResponseWriter, request *http.Request) { + userName := chi.URLParam(request, "username") + if userName == "" { + writer.WriteHeader(http.StatusBadRequest) + return + } + uPSK, loaded := s.user.Get(userName) + if !loaded { + writer.WriteHeader(http.StatusNotFound) + return + } + user := UserObject{ + UserName: userName, + Password: uPSK, + } + s.traffic.ReadUser(&user) + render.JSON(writer, request, user) +} + +func (s *APIServer) updateUser(writer http.ResponseWriter, request *http.Request) { + userName := chi.URLParam(request, "username") + if userName == "" { + writer.WriteHeader(http.StatusBadRequest) + return + } + var updateRequest struct { + Password string `json:"uPSK"` + } + err := render.DecodeJSON(request.Body, &updateRequest) + if err != nil { + render.Status(request, http.StatusBadRequest) + render.PlainText(writer, request, err.Error()) + return + } + _, loaded := s.user.Get(userName) + if !loaded { + writer.WriteHeader(http.StatusNotFound) + return + } + err = s.user.Update(userName, updateRequest.Password) + if err != nil { + render.Status(request, http.StatusBadRequest) + render.PlainText(writer, request, err.Error()) + return + } + writer.WriteHeader(http.StatusNoContent) +} + +func (s *APIServer) deleteUser(writer http.ResponseWriter, request *http.Request) { + userName := chi.URLParam(request, "username") + if userName == "" { + writer.WriteHeader(http.StatusBadRequest) + return + } + _, loaded := s.user.Get(userName) + if !loaded { + writer.WriteHeader(http.StatusNotFound) + return + } + err := s.user.Delete(userName) + if err != nil { + render.Status(request, http.StatusBadRequest) + render.PlainText(writer, request, err.Error()) + return + } + writer.WriteHeader(http.StatusNoContent) +} + +func (s *APIServer) getStats(writer http.ResponseWriter, request *http.Request) { + requireClear := request.URL.Query().Get("clear") == "true" + + users := s.user.List() + s.traffic.ReadUsers(users, requireClear) + for i := range users { + users[i].Password = "" + } + uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.traffic.ReadGlobal(requireClear) + + render.JSON(writer, request, render.M{ + "uplinkBytes": uplinkBytes, + "downlinkBytes": downlinkBytes, + "uplinkPackets": uplinkPackets, + "downlinkPackets": downlinkPackets, + "tcpSessions": tcpSessions, + "udpSessions": udpSessions, + "users": users, + }) +} diff --git a/service/ssmapi/cache.go b/service/ssmapi/cache.go new file mode 100644 index 00000000..f942265d --- /dev/null +++ b/service/ssmapi/cache.go @@ -0,0 +1,239 @@ +package ssmapi + +import ( + "bytes" + "os" + "path/filepath" + "sort" + "sync/atomic" + + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/service/filemanager" +) + +type Cache struct { + Endpoints *badjson.TypedMap[string, *EndpointCache] `json:"endpoints"` +} + +type EndpointCache struct { + GlobalUplink int64 `json:"global_uplink"` + GlobalDownlink int64 `json:"global_downlink"` + GlobalUplinkPackets int64 `json:"global_uplink_packets"` + GlobalDownlinkPackets int64 `json:"global_downlink_packets"` + GlobalTCPSessions int64 `json:"global_tcp_sessions"` + GlobalUDPSessions int64 `json:"global_udp_sessions"` + UserUplink *badjson.TypedMap[string, int64] `json:"user_uplink"` + UserDownlink *badjson.TypedMap[string, int64] `json:"user_downlink"` + UserUplinkPackets *badjson.TypedMap[string, int64] `json:"user_uplink_packets"` + UserDownlinkPackets *badjson.TypedMap[string, int64] `json:"user_downlink_packets"` + UserTCPSessions *badjson.TypedMap[string, int64] `json:"user_tcp_sessions"` + UserUDPSessions *badjson.TypedMap[string, int64] `json:"user_udp_sessions"` + Users *badjson.TypedMap[string, string] `json:"users"` +} + +func (s *Service) loadCache() error { + if s.cachePath == "" { + return nil + } + basePath := filemanager.BasePath(s.ctx, s.cachePath) + cacheBinary, err := os.ReadFile(basePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + err = s.decodeCache(cacheBinary) + if err != nil { + os.RemoveAll(basePath) + return err + } + s.cacheMutex.Lock() + s.lastSavedCache = cacheBinary + s.cacheMutex.Unlock() + return nil +} + +func (s *Service) saveCache() error { + if s.cachePath == "" { + return nil + } + cacheBinary, err := s.encodeCache() + if err != nil { + return err + } + s.cacheMutex.Lock() + defer s.cacheMutex.Unlock() + if bytes.Equal(s.lastSavedCache, cacheBinary) { + return nil + } + return s.writeCache(cacheBinary) +} + +func (s *Service) writeCache(cacheBinary []byte) error { + basePath := filemanager.BasePath(s.ctx, s.cachePath) + err := os.MkdirAll(filepath.Dir(basePath), 0o777) + if err != nil { + return err + } + err = os.WriteFile(basePath, cacheBinary, 0o644) + if err != nil { + return err + } + s.lastSavedCache = cacheBinary + return nil +} + +func (s *Service) decodeCache(cacheBinary []byte) error { + if len(cacheBinary) == 0 { + return nil + } + cache, err := json.UnmarshalExtended[*Cache](cacheBinary) + if err != nil { + return err + } + if cache.Endpoints == nil || cache.Endpoints.Size() == 0 { + return nil + } + for _, entry := range cache.Endpoints.Entries() { + trafficManager, loaded := s.traffics[entry.Key] + if !loaded { + continue + } + trafficManager.globalUplink.Store(entry.Value.GlobalUplink) + trafficManager.globalDownlink.Store(entry.Value.GlobalDownlink) + trafficManager.globalUplinkPackets.Store(entry.Value.GlobalUplinkPackets) + trafficManager.globalDownlinkPackets.Store(entry.Value.GlobalDownlinkPackets) + trafficManager.globalTCPSessions.Store(entry.Value.GlobalTCPSessions) + trafficManager.globalUDPSessions.Store(entry.Value.GlobalUDPSessions) + trafficManager.userUplink = typedAtomicInt64Map(entry.Value.UserUplink) + trafficManager.userDownlink = typedAtomicInt64Map(entry.Value.UserDownlink) + trafficManager.userUplinkPackets = typedAtomicInt64Map(entry.Value.UserUplinkPackets) + trafficManager.userDownlinkPackets = typedAtomicInt64Map(entry.Value.UserDownlinkPackets) + trafficManager.userTCPSessions = typedAtomicInt64Map(entry.Value.UserTCPSessions) + trafficManager.userUDPSessions = typedAtomicInt64Map(entry.Value.UserUDPSessions) + userManager, loaded := s.users[entry.Key] + if !loaded { + continue + } + userManager.usersMap = typedMap(entry.Value.Users) + _ = userManager.postUpdate(false) + } + return nil +} + +func (s *Service) encodeCache() ([]byte, error) { + endpoints := new(badjson.TypedMap[string, *EndpointCache]) + for tag, traffic := range s.traffics { + var ( + userUplink = new(badjson.TypedMap[string, int64]) + userDownlink = new(badjson.TypedMap[string, int64]) + userUplinkPackets = new(badjson.TypedMap[string, int64]) + userDownlinkPackets = new(badjson.TypedMap[string, int64]) + userTCPSessions = new(badjson.TypedMap[string, int64]) + userUDPSessions = new(badjson.TypedMap[string, int64]) + userMap = new(badjson.TypedMap[string, string]) + ) + for user, uplink := range traffic.userUplink { + if uplink.Load() > 0 { + userUplink.Put(user, uplink.Load()) + } + } + for user, downlink := range traffic.userDownlink { + if downlink.Load() > 0 { + userDownlink.Put(user, downlink.Load()) + } + } + for user, uplinkPackets := range traffic.userUplinkPackets { + if uplinkPackets.Load() > 0 { + userUplinkPackets.Put(user, uplinkPackets.Load()) + } + } + for user, downlinkPackets := range traffic.userDownlinkPackets { + if downlinkPackets.Load() > 0 { + userDownlinkPackets.Put(user, downlinkPackets.Load()) + } + } + for user, tcpSessions := range traffic.userTCPSessions { + if tcpSessions.Load() > 0 { + userTCPSessions.Put(user, tcpSessions.Load()) + } + } + for user, udpSessions := range traffic.userUDPSessions { + if udpSessions.Load() > 0 { + userUDPSessions.Put(user, udpSessions.Load()) + } + } + userManager := s.users[tag] + if userManager != nil && len(userManager.usersMap) > 0 { + userMap = new(badjson.TypedMap[string, string]) + for username, password := range userManager.usersMap { + if username != "" && password != "" { + userMap.Put(username, password) + } + } + } + endpoints.Put(tag, &EndpointCache{ + GlobalUplink: traffic.globalUplink.Load(), + GlobalDownlink: traffic.globalDownlink.Load(), + GlobalUplinkPackets: traffic.globalUplinkPackets.Load(), + GlobalDownlinkPackets: traffic.globalDownlinkPackets.Load(), + GlobalTCPSessions: traffic.globalTCPSessions.Load(), + GlobalUDPSessions: traffic.globalUDPSessions.Load(), + UserUplink: sortTypedMap(userUplink), + UserDownlink: sortTypedMap(userDownlink), + UserUplinkPackets: sortTypedMap(userUplinkPackets), + UserDownlinkPackets: sortTypedMap(userDownlinkPackets), + UserTCPSessions: sortTypedMap(userTCPSessions), + UserUDPSessions: sortTypedMap(userUDPSessions), + Users: sortTypedMap(userMap), + }) + } + var buffer bytes.Buffer + encoder := json.NewEncoder(&buffer) + encoder.SetIndent("", " ") + err := encoder.Encode(&Cache{ + Endpoints: sortTypedMap(endpoints), + }) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func sortTypedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) *badjson.TypedMap[string, T] { + if trafficMap == nil { + return nil + } + keys := trafficMap.Keys() + sort.Strings(keys) + sortedMap := new(badjson.TypedMap[string, T]) + for _, key := range keys { + value, _ := trafficMap.Get(key) + sortedMap.Put(key, value) + } + return sortedMap +} + +func typedAtomicInt64Map(trafficMap *badjson.TypedMap[string, int64]) map[string]*atomic.Int64 { + result := make(map[string]*atomic.Int64) + if trafficMap != nil { + for _, entry := range trafficMap.Entries() { + counter := new(atomic.Int64) + counter.Store(entry.Value) + result[entry.Key] = counter + } + } + return result +} + +func typedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) map[string]T { + result := make(map[string]T) + if trafficMap != nil { + for _, entry := range trafficMap.Entries() { + result[entry.Key] = entry.Value + } + } + return result +} diff --git a/service/ssmapi/server.go b/service/ssmapi/server.go new file mode 100644 index 00000000..157ea150 --- /dev/null +++ b/service/ssmapi/server.go @@ -0,0 +1,163 @@ +package ssmapi + +import ( + "context" + "errors" + "net/http" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + "github.com/sagernet/sing/service" + + "github.com/go-chi/chi/v5" + "golang.org/x/net/http2" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.SSMAPIServiceOptions](registry, C.TypeSSMAPI, NewService) +} + +type Service struct { + boxService.Adapter + ctx context.Context + cancel context.CancelFunc + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + httpServer *http.Server + traffics map[string]*TrafficManager + users map[string]*UserManager + cachePath string + saveTicker *time.Ticker + lastSavedCache []byte + cacheMutex sync.Mutex +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) { + ctx, cancel := context.WithCancel(ctx) + chiRouter := chi.NewRouter() + s := &Service{ + Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag), + ctx: ctx, + cancel: cancel, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + httpServer: &http.Server{ + Handler: chiRouter, + }, + traffics: make(map[string]*TrafficManager), + users: make(map[string]*UserManager), + cachePath: options.CachePath, + } + inboundManager := service.FromContext[adapter.InboundManager](ctx) + if options.Servers.Size() == 0 { + return nil, E.New("missing servers") + } + for i, entry := range options.Servers.Entries() { + inbound, loaded := inboundManager.Get(entry.Value) + if !loaded { + return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, " not found") + } + managedServer, isManaged := inbound.(adapter.ManagedSSMServer) + if !isManaged { + return nil, E.New("parse SSM server[", i, "]: inbound/", inbound.Type(), "[", inbound.Tag(), "] is not a SSM server") + } + traffic := NewTrafficManager() + managedServer.SetTracker(traffic) + user := NewUserManager(managedServer, traffic) + chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route) + s.traffics[entry.Key] = traffic + s.users[entry.Key] = user + } + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + s.tlsConfig = tlsConfig + } + return s, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + err := s.loadCache() + if err != nil { + s.logger.Error(E.Cause(err, "load cache")) + } + s.saveTicker = time.NewTicker(1 * time.Minute) + go s.loopSaveCache() + if s.tlsConfig != nil { + err = s.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + tcpListener, err := s.listener.ListenTCP() + if err != nil { + return err + } + if s.tlsConfig != nil { + if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig) + } + go func() { + err = s.httpServer.Serve(tcpListener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("serve error: ", err) + } + }() + return nil +} + +func (s *Service) loopSaveCache() { + for { + select { + case <-s.ctx.Done(): + return + case <-s.saveTicker.C: + err := s.saveCache() + if err != nil { + s.logger.Error(E.Cause(err, "save cache")) + } + } + } +} + +func (s *Service) Close() error { + if s.cancel != nil { + s.cancel() + } + if s.saveTicker != nil { + s.saveTicker.Stop() + } + err := s.saveCache() + if err != nil { + s.logger.Error(E.Cause(err, "save cache")) + } + return common.Close( + common.PtrOrNil(s.httpServer), + common.PtrOrNil(s.listener), + s.tlsConfig, + ) +} diff --git a/service/ssmapi/traffic.go b/service/ssmapi/traffic.go new file mode 100644 index 00000000..4a669adb --- /dev/null +++ b/service/ssmapi/traffic.go @@ -0,0 +1,223 @@ +package ssmapi + +import ( + "net" + "sync" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.SSMTracker = (*TrafficManager)(nil) + +type TrafficManager struct { + globalUplink atomic.Int64 + globalDownlink atomic.Int64 + globalUplinkPackets atomic.Int64 + globalDownlinkPackets atomic.Int64 + globalTCPSessions atomic.Int64 + globalUDPSessions atomic.Int64 + userAccess sync.Mutex + userUplink map[string]*atomic.Int64 + userDownlink map[string]*atomic.Int64 + userUplinkPackets map[string]*atomic.Int64 + userDownlinkPackets map[string]*atomic.Int64 + userTCPSessions map[string]*atomic.Int64 + userUDPSessions map[string]*atomic.Int64 +} + +func NewTrafficManager() *TrafficManager { + manager := &TrafficManager{ + userUplink: make(map[string]*atomic.Int64), + userDownlink: make(map[string]*atomic.Int64), + userUplinkPackets: make(map[string]*atomic.Int64), + userDownlinkPackets: make(map[string]*atomic.Int64), + userTCPSessions: make(map[string]*atomic.Int64), + userUDPSessions: make(map[string]*atomic.Int64), + } + return manager +} + +func (s *TrafficManager) UpdateUsers(users []string) { + s.userAccess.Lock() + defer s.userAccess.Unlock() + newUserUplink := make(map[string]*atomic.Int64) + newUserDownlink := make(map[string]*atomic.Int64) + newUserUplinkPackets := make(map[string]*atomic.Int64) + newUserDownlinkPackets := make(map[string]*atomic.Int64) + newUserTCPSessions := make(map[string]*atomic.Int64) + newUserUDPSessions := make(map[string]*atomic.Int64) + for _, user := range users { + if counter, loaded := s.userUplink[user]; loaded { + newUserUplink[user] = counter + } + if counter, loaded := s.userDownlink[user]; loaded { + newUserDownlink[user] = counter + } + if counter, loaded := s.userUplinkPackets[user]; loaded { + newUserUplinkPackets[user] = counter + } + if counter, loaded := s.userDownlinkPackets[user]; loaded { + newUserDownlinkPackets[user] = counter + } + if counter, loaded := s.userTCPSessions[user]; loaded { + newUserTCPSessions[user] = counter + } + if counter, loaded := s.userUDPSessions[user]; loaded { + newUserUDPSessions[user] = counter + } + } + s.userUplink = newUserUplink + s.userDownlink = newUserDownlink + s.userUplinkPackets = newUserUplinkPackets + s.userDownlinkPackets = newUserDownlinkPackets + s.userTCPSessions = newUserTCPSessions + s.userUDPSessions = newUserUDPSessions +} + +func (s *TrafficManager) userCounter(user string) (*atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64) { + s.userAccess.Lock() + defer s.userAccess.Unlock() + upCounter, loaded := s.userUplink[user] + if !loaded { + upCounter = new(atomic.Int64) + s.userUplink[user] = upCounter + } + downCounter, loaded := s.userDownlink[user] + if !loaded { + downCounter = new(atomic.Int64) + s.userDownlink[user] = downCounter + } + upPacketsCounter, loaded := s.userUplinkPackets[user] + if !loaded { + upPacketsCounter = new(atomic.Int64) + s.userUplinkPackets[user] = upPacketsCounter + } + downPacketsCounter, loaded := s.userDownlinkPackets[user] + if !loaded { + downPacketsCounter = new(atomic.Int64) + s.userDownlinkPackets[user] = downPacketsCounter + } + tcpSessionsCounter, loaded := s.userTCPSessions[user] + if !loaded { + tcpSessionsCounter = new(atomic.Int64) + s.userTCPSessions[user] = tcpSessionsCounter + } + udpSessionsCounter, loaded := s.userUDPSessions[user] + if !loaded { + udpSessionsCounter = new(atomic.Int64) + s.userUDPSessions[user] = udpSessionsCounter + } + return upCounter, downCounter, upPacketsCounter, downPacketsCounter, tcpSessionsCounter, udpSessionsCounter +} + +func (s *TrafficManager) TrackConnection(conn net.Conn, metadata adapter.InboundContext) net.Conn { + s.globalTCPSessions.Add(1) + var readCounter []*atomic.Int64 + var writeCounter []*atomic.Int64 + readCounter = append(readCounter, &s.globalUplink) + writeCounter = append(writeCounter, &s.globalDownlink) + upCounter, downCounter, _, _, tcpSessionCounter, _ := s.userCounter(metadata.User) + readCounter = append(readCounter, upCounter) + writeCounter = append(writeCounter, downCounter) + tcpSessionCounter.Add(1) + return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) +} + +func (s *TrafficManager) TrackPacketConnection(conn N.PacketConn, metadata adapter.InboundContext) N.PacketConn { + s.globalUDPSessions.Add(1) + var readCounter []*atomic.Int64 + var readPacketCounter []*atomic.Int64 + var writeCounter []*atomic.Int64 + var writePacketCounter []*atomic.Int64 + readCounter = append(readCounter, &s.globalUplink) + writeCounter = append(writeCounter, &s.globalDownlink) + readPacketCounter = append(readPacketCounter, &s.globalUplinkPackets) + writePacketCounter = append(writePacketCounter, &s.globalDownlinkPackets) + upCounter, downCounter, upPacketsCounter, downPacketsCounter, _, udpSessionCounter := s.userCounter(metadata.User) + readCounter = append(readCounter, upCounter) + writeCounter = append(writeCounter, downCounter) + readPacketCounter = append(readPacketCounter, upPacketsCounter) + writePacketCounter = append(writePacketCounter, downPacketsCounter) + udpSessionCounter.Add(1) + return bufio.NewInt64CounterPacketConn(conn, readCounter, readPacketCounter, writeCounter, writePacketCounter) +} + +func (s *TrafficManager) ReadUser(user *UserObject) { + s.userAccess.Lock() + defer s.userAccess.Unlock() + s.readUser(user, false) +} + +func (s *TrafficManager) readUser(user *UserObject, swap bool) { + if counter, loaded := s.userUplink[user.UserName]; loaded { + if swap { + user.UplinkBytes = counter.Swap(0) + } else { + user.UplinkBytes = counter.Load() + } + } + if counter, loaded := s.userDownlink[user.UserName]; loaded { + if swap { + user.DownlinkBytes = counter.Swap(0) + } else { + user.DownlinkBytes = counter.Load() + } + } + if counter, loaded := s.userUplinkPackets[user.UserName]; loaded { + if swap { + user.UplinkPackets = counter.Swap(0) + } else { + user.UplinkPackets = counter.Load() + } + } + if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded { + if swap { + user.DownlinkPackets = counter.Swap(0) + } else { + user.DownlinkPackets = counter.Load() + } + } + if counter, loaded := s.userTCPSessions[user.UserName]; loaded { + if swap { + user.TCPSessions = counter.Swap(0) + } else { + user.TCPSessions = counter.Load() + } + } + if counter, loaded := s.userUDPSessions[user.UserName]; loaded { + if swap { + user.UDPSessions = counter.Swap(0) + } else { + user.UDPSessions = counter.Load() + } + } +} + +func (s *TrafficManager) ReadUsers(users []*UserObject, swap bool) { + s.userAccess.Lock() + defer s.userAccess.Unlock() + for _, user := range users { + s.readUser(user, swap) + } +} + +func (s *TrafficManager) ReadGlobal(swap bool) (uplinkBytes int64, downlinkBytes int64, uplinkPackets int64, downlinkPackets int64, tcpSessions int64, udpSessions int64) { + if swap { + return s.globalUplink.Swap(0), + s.globalDownlink.Swap(0), + s.globalUplinkPackets.Swap(0), + s.globalDownlinkPackets.Swap(0), + s.globalTCPSessions.Swap(0), + s.globalUDPSessions.Swap(0) + } else { + return s.globalUplink.Load(), + s.globalDownlink.Load(), + s.globalUplinkPackets.Load(), + s.globalDownlinkPackets.Load(), + s.globalTCPSessions.Load(), + s.globalUDPSessions.Load() + } +} diff --git a/service/ssmapi/user.go b/service/ssmapi/user.go new file mode 100644 index 00000000..26bc621a --- /dev/null +++ b/service/ssmapi/user.go @@ -0,0 +1,87 @@ +package ssmapi + +import ( + "sync" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" +) + +type UserManager struct { + access sync.Mutex + usersMap map[string]string + server adapter.ManagedSSMServer + trafficManager *TrafficManager +} + +func NewUserManager(inbound adapter.ManagedSSMServer, trafficManager *TrafficManager) *UserManager { + return &UserManager{ + usersMap: make(map[string]string), + server: inbound, + trafficManager: trafficManager, + } +} + +func (m *UserManager) postUpdate(updated bool) error { + users := make([]string, 0, len(m.usersMap)) + uPSKs := make([]string, 0, len(m.usersMap)) + for username, password := range m.usersMap { + users = append(users, username) + uPSKs = append(uPSKs, password) + } + err := m.server.UpdateUsers(users, uPSKs) + if err != nil { + return err + } + if updated { + m.trafficManager.UpdateUsers(users) + } + return nil +} + +func (m *UserManager) List() []*UserObject { + m.access.Lock() + defer m.access.Unlock() + + users := make([]*UserObject, 0, len(m.usersMap)) + for username, password := range m.usersMap { + users = append(users, &UserObject{ + UserName: username, + Password: password, + }) + } + return users +} + +func (m *UserManager) Add(username string, password string) error { + m.access.Lock() + defer m.access.Unlock() + if _, found := m.usersMap[username]; found { + return E.New("user ", username, " already exists") + } + m.usersMap[username] = password + return m.postUpdate(true) +} + +func (m *UserManager) Get(username string) (string, bool) { + m.access.Lock() + defer m.access.Unlock() + if password, found := m.usersMap[username]; found { + return password, true + } + return "", false +} + +func (m *UserManager) Update(username string, password string) error { + m.access.Lock() + defer m.access.Unlock() + m.usersMap[username] = password + return m.postUpdate(true) +} + +func (m *UserManager) Delete(username string) error { + m.access.Lock() + defer m.access.Unlock() + delete(m.usersMap, username) + return m.postUpdate(true) +} diff --git a/test/box_test.go b/test/box_test.go new file mode 100644 index 00000000..d7d9b9b0 --- /dev/null +++ b/test/box_test.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "crypto/tls" + "io" + "net" + "net/http" + "testing" + "time" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/debug" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" + + "github.com/stretchr/testify/require" + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +var globalCtx context.Context + +func init() { + globalCtx = include.Context(context.Background()) +} + +func startInstance(t *testing.T, options option.Options) *box.Box { + if debug.Enabled { + options.Log = &option.LogOptions{ + Level: "trace", + } + } else { + options.Log = &option.LogOptions{ + Level: "warning", + } + } + ctx, cancel := context.WithCancel(globalCtx) + var instance *box.Box + var err error + for retry := 0; retry < 3; retry++ { + instance, err = box.New(box.Options{ + Context: ctx, + Options: options, + }) + require.NoError(t, err) + err = instance.Start() + if err != nil { + time.Sleep(time.Second) + continue + } + break + } + require.NoError(t, err) + t.Cleanup(func() { + instance.Close() + cancel() + }) + return instance +} + +func testSuit(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + dialUDP := func() (net.PacketConn, error) { + return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) + require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) + require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) + + // require.NoError(t, testPacketConnTimeout(t, dialUDP)) +} + +func testQUIC(t *testing.T, clientPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + client := &http.Client{ + Transport: &http3.Transport{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { + destination := M.ParseSocksaddr(addr) + udpConn, err := dialer.DialContext(ctx, N.NetworkUDP, destination) + if err != nil { + return nil, err + } + return quic.DialEarly(ctx, udpConn.(net.PacketConn), destination, tlsCfg, cfg) + }, + }, + } + response, err := client.Get("https://cloudflare.com/cdn-cgi/trace") + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + content, err := io.ReadAll(response.Body) + require.NoError(t, err) + println(string(content)) +} + +func testSuitLargeUDP(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + dialUDP := func() (net.PacketConn, error) { + return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) + require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) + require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) + require.NoError(t, testLargeDataWithPacketConnSize(t, testPort, 4096, dialUDP)) +} + +func testTCP(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) +} + +func testSuitSimple(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + dialUDP := func() (net.PacketConn, error) { + return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) +} + +func testSuitSimple1(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + dialUDP := func() (net.PacketConn, error) { + return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + if !C.IsDarwin { + require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + if !C.IsDarwin { + require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) + } +} + +func testSuitWg(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("10.0.0.1", testPort)) + } + dialUDP := func() (net.PacketConn, error) { + conn, err := dialer.DialContext(context.Background(), "udp", M.ParseSocksaddrHostPort("10.0.0.1", testPort)) + if err != nil { + return nil, err + } + return bufio.NewUnbindPacketConn(conn), nil + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) + require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) + require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) + require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) +} diff --git a/test/brutal_test.go b/test/brutal_test.go new file mode 100644 index 00000000..d0841467 --- /dev/null +++ b/test/brutal_test.go @@ -0,0 +1,392 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +func TestBrutalShadowsocks(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Method: method, + Password: password, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "smux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestBrutalTrojan(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{{Password: password}}, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "ss-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: password, + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "yamux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestBrutalVMess(t *testing.T) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{{UUID: user.String()}}, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "ss-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "h2mux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestBrutalVLESS(t *testing.T) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVLESS, + Options: &option.VLESSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VLESSUser{{UUID: user.String()}}, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVLESS, + Tag: "ss-out", + Options: &option.VLESSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, + }, + }, + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "h2mux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/clash_darwin_test.go b/test/clash_darwin_test.go new file mode 100644 index 00000000..013d8b3f --- /dev/null +++ b/test/clash_darwin_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "fmt" + "net" + "net/netip" + "syscall" + + "golang.org/x/net/route" +) + +func defaultRouteIP() (netip.Addr, error) { + idx, err := defaultRouteInterfaceIndex() + if err != nil { + return netip.Addr{}, err + } + iface, err := net.InterfaceByIndex(idx) + if err != nil { + return netip.Addr{}, err + } + addrs, err := iface.Addrs() + if err != nil { + return netip.Addr{}, err + } + for _, addr := range addrs { + ip := addr.(*net.IPNet).IP + if ip.To4() != nil { + return netip.AddrFrom4([4]byte(ip)), nil + } + } + + return netip.Addr{}, errors.New("no ipv4 addr") +} + +func defaultRouteInterfaceIndex() (int, error) { + rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0) + if err != nil { + return 0, fmt.Errorf("route.FetchRIB: %w", err) + } + msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib) + if err != nil { + return 0, fmt.Errorf("route.ParseRIB: %w", err) + } + for _, message := range msgs { + routeMessage := message.(*route.RouteMessage) + if routeMessage.Flags&(syscall.RTF_UP|syscall.RTF_GATEWAY|syscall.RTF_STATIC) == 0 { + continue + } + + addresses := routeMessage.Addrs + + destination, ok := addresses[0].(*route.Inet4Addr) + if !ok { + continue + } + + if destination.IP != [4]byte{0, 0, 0, 0} { + continue + } + + switch addresses[1].(type) { + case *route.Inet4Addr: + return routeMessage.Index, nil + default: + continue + } + } + + return 0, fmt.Errorf("ambiguous gateway interfaces found") +} diff --git a/test/clash_other_test.go b/test/clash_other_test.go new file mode 100644 index 00000000..fc4a68af --- /dev/null +++ b/test/clash_other_test.go @@ -0,0 +1,12 @@ +//go:build !darwin + +package main + +import ( + "errors" + "net/netip" +) + +func defaultRouteIP() (netip.Addr, error) { + return netip.Addr{}, errors.New("not supported") +} diff --git a/test/clash_test.go b/test/clash_test.go new file mode 100644 index 00000000..bba7f3be --- /dev/null +++ b/test/clash_test.go @@ -0,0 +1,524 @@ +package main + +import ( + "context" + "crypto/md5" + "crypto/rand" + "errors" + "io" + "net" + _ "net/http/pprof" + "net/netip" + "sync" + "testing" + "time" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/control" + F "github.com/sagernet/sing/common/format" + + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// kanged from clash + +const ( + ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest" + ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest" + ImageV2RayCore = "v2fly/v2fly-core:latest" + ImageTrojan = "trojangfw/trojan:latest" + ImageNaive = "pocat/naiveproxy:client" + ImageBoringTun = "ghcr.io/ntkme/boringtun:edge" + ImageHysteria = "tobyxdd/hysteria:v1.3.5" + ImageHysteria2 = "tobyxdd/hysteria:v2" + ImageNginx = "nginx:stable" + ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest" + ImageXRayCore = "teddysun/xray:latest" + ImageShadowsocksLegacy = "mritd/shadowsocks:latest" + ImageTUICServer = "kilvn/tuic-server:latest" + ImageTUICClient = "kilvn/tuic-client:latest" +) + +var allImages = []string{ + ImageShadowsocksRustServer, + ImageShadowsocksRustClient, + ImageV2RayCore, + ImageTrojan, + ImageNaive, + ImageBoringTun, + ImageHysteria, + ImageHysteria2, + ImageNginx, + ImageShadowTLS, + ImageXRayCore, + ImageShadowsocksLegacy, + ImageTUICServer, + ImageTUICClient, +} + +var localIP = netip.MustParseAddr("127.0.0.1") + +func init() { + dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer dockerClient.Close() + + list, err := dockerClient.ImageList(context.Background(), image.ListOptions{All: true}) + if err != nil { + log.Warn(err) + return + } + + imageExist := func(image string) bool { + for _, item := range list { + for _, tag := range item.RepoTags { + if image == tag { + return true + } + } + } + return false + } + + for _, i := range allImages { + if imageExist(i) { + continue + } + + log.Info("pulling image: ", i) + imageStream, err := dockerClient.ImagePull(context.Background(), i, image.PullOptions{}) + if err != nil { + panic(err) + } + + io.Copy(io.Discard, imageStream) + } +} + +func newPingPongPair() (chan []byte, chan []byte, func(t *testing.T) error) { + pingCh := make(chan []byte) + pongCh := make(chan []byte) + test := func(t *testing.T) error { + defer close(pingCh) + defer close(pongCh) + pingOpen := false + pongOpen := false + var recv []byte + + for { + if pingOpen && pongOpen { + break + } + + select { + case recv, pingOpen = <-pingCh: + assert.True(t, pingOpen) + assert.Equal(t, []byte("ping"), recv) + case recv, pongOpen = <-pongCh: + assert.True(t, pongOpen) + assert.Equal(t, []byte("pong"), recv) + case <-time.After(10 * time.Second): + return errors.New("timeout") + } + } + return nil + } + + return pingCh, pongCh, test +} + +func newLargeDataPair() (chan hashPair, chan hashPair, func(t *testing.T) error) { + pingCh := make(chan hashPair) + pongCh := make(chan hashPair) + test := func(t *testing.T) error { + defer close(pingCh) + defer close(pongCh) + pingOpen := false + pongOpen := false + var serverPair hashPair + var clientPair hashPair + + for { + if pingOpen && pongOpen { + break + } + + select { + case serverPair, pingOpen = <-pingCh: + assert.True(t, pingOpen) + case clientPair, pongOpen = <-pongCh: + assert.True(t, pongOpen) + case <-time.After(10 * time.Second): + return errors.New("timeout") + } + } + + assert.Equal(t, serverPair.recvHash, clientPair.sendHash) + assert.Equal(t, serverPair.sendHash, clientPair.recvHash) + + return nil + } + + return pingCh, pongCh, test +} + +func testPingPongWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error { + l, err := listen("tcp", ":"+F.ToString(port)) + if err != nil { + return err + } + defer l.Close() + + c, err := cc() + if err != nil { + return err + } + defer c.Close() + + pingCh, pongCh, test := newPingPongPair() + go func() { + c, err := l.Accept() + if err != nil { + return + } + + buf := make([]byte, 4) + if _, err := io.ReadFull(c, buf); err != nil { + return + } + + pingCh <- buf + if _, err := c.Write([]byte("pong")); err != nil { + return + } + }() + + go func() { + if _, err := c.Write([]byte("ping")); err != nil { + return + } + + buf := make([]byte, 4) + if _, err := io.ReadFull(c, buf); err != nil { + return + } + + pongCh <- buf + }() + + return test(t) +} + +func testPingPongWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error { + l, err := listenPacket("udp", ":"+F.ToString(port)) + if err != nil { + return err + } + defer l.Close() + + rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} + + pingCh, pongCh, test := newPingPongPair() + go func() { + buf := make([]byte, 1024) + n, rAddr, err := l.ReadFrom(buf) + if err != nil { + return + } + + pingCh <- buf[:n] + if _, err := l.WriteTo([]byte("pong"), rAddr); err != nil { + return + } + }() + + pc, err := pcc() + if err != nil { + return err + } + defer pc.Close() + + go func() { + if _, err := pc.WriteTo([]byte("ping"), rAddr); err != nil { + return + } + + buf := make([]byte, 1024) + n, _, err := pc.ReadFrom(buf) + if err != nil { + return + } + + pongCh <- buf[:n] + }() + + return test(t) +} + +type hashPair struct { + sendHash map[int][]byte + recvHash map[int][]byte +} + +func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error { + l, err := listen("tcp", ":"+F.ToString(port)) + require.NoError(t, err) + defer l.Close() + + times := 100 + chunkSize := int64(64 * 1024) + + pingCh, pongCh, test := newLargeDataPair() + writeRandData := func(conn net.Conn) (map[int][]byte, error) { + buf := make([]byte, chunkSize) + hashMap := map[int][]byte{} + for i := 0; i < times; i++ { + if _, err := rand.Read(buf[1:]); err != nil { + return nil, err + } + buf[0] = byte(i) + + hash := md5.Sum(buf) + hashMap[i] = hash[:] + + if _, err := conn.Write(buf); err != nil { + return nil, err + } + } + + return hashMap, nil + } + + c, err := cc() + if err != nil { + return err + } + defer c.Close() + + go func() { + c, err := l.Accept() + if err != nil { + return + } + defer c.Close() + + hashMap := map[int][]byte{} + buf := make([]byte, chunkSize) + + for i := 0; i < times; i++ { + _, err := io.ReadFull(c, buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf) + hashMap[int(buf[0])] = hash[:] + } + + sendHash, err := writeRandData(c) + if err != nil { + t.Log(err.Error()) + return + } + + pingCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + go func() { + sendHash, err := writeRandData(c) + if err != nil { + t.Log(err.Error()) + return + } + + hashMap := map[int][]byte{} + buf := make([]byte, chunkSize) + + for i := 0; i < times; i++ { + _, err := io.ReadFull(c, buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf) + hashMap[int(buf[0])] = hash[:] + } + + pongCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + return test(t) +} + +func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error { + return testLargeDataWithPacketConnSize(t, port, 1500, pcc) +} + +func testLargeDataWithPacketConnSize(t *testing.T, port uint16, chunkSize int, pcc func() (net.PacketConn, error)) error { + l, err := listenPacket("udp", ":"+F.ToString(port)) + if err != nil { + return err + } + defer l.Close() + + rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} + + times := 50 + + pingCh, pongCh, test := newLargeDataPair() + writeRandData := func(pc net.PacketConn, addr net.Addr) (map[int][]byte, error) { + hashMap := map[int][]byte{} + mux := sync.Mutex{} + for i := 0; i < times; i++ { + buf := make([]byte, chunkSize) + if _, err := rand.Read(buf[1:]); err != nil { + t.Log(err.Error()) + continue + } + buf[0] = byte(i) + + hash := md5.Sum(buf) + mux.Lock() + hashMap[i] = hash[:] + mux.Unlock() + + if _, err := pc.WriteTo(buf, addr); err != nil { + t.Log(err.Error()) + } + + time.Sleep(10 * time.Millisecond) + } + + return hashMap, nil + } + + go func() { + var rAddr net.Addr + hashMap := map[int][]byte{} + buf := make([]byte, 64*1024) + + for i := 0; i < times; i++ { + _, rAddr, err = l.ReadFrom(buf) + if err != nil { + t.Log(err.Error()) + return + } + hash := md5.Sum(buf[:chunkSize]) + hashMap[int(buf[0])] = hash[:] + } + sendHash, err := writeRandData(l, rAddr) + if err != nil { + t.Log(err.Error()) + return + } + + pingCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + pc, err := pcc() + if err != nil { + return err + } + defer pc.Close() + + go func() { + sendHash, err := writeRandData(pc, rAddr) + if err != nil { + t.Log(err.Error()) + return + } + + hashMap := map[int][]byte{} + buf := make([]byte, 64*1024) + + for i := 0; i < times; i++ { + _, _, err := pc.ReadFrom(buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf[:chunkSize]) + hashMap[int(buf[0])] = hash[:] + } + + pongCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + return test(t) +} + +func testPacketConnTimeout(t *testing.T, pcc func() (net.PacketConn, error)) error { + pc, err := pcc() + if err != nil { + return err + } + + err = pc.SetReadDeadline(time.Now().Add(time.Millisecond * 300)) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + buf := make([]byte, 1024) + _, _, err := pc.ReadFrom(buf) + errCh <- err + }() + + select { + case <-errCh: + return nil + case <-time.After(time.Second * 10): + return errors.New("timeout") + } +} + +func listen(network, address string) (net.Listener, error) { + var lc net.ListenConfig + lc.Control = control.ReuseAddr() + var lastErr error + for i := 0; i < 5; i++ { + l, err := lc.Listen(context.Background(), network, address) + if err == nil { + return l, nil + } + + lastErr = err + time.Sleep(5 * time.Millisecond) + } + return nil, lastErr +} + +func listenPacket(network, address string) (net.PacketConn, error) { + var lc net.ListenConfig + lc.Control = control.ReuseAddr() + var lastErr error + for i := 0; i < 5; i++ { + l, err := lc.ListenPacket(context.Background(), network, address) + if err == nil { + return l, nil + } + + lastErr = err + time.Sleep(5 * time.Millisecond) + } + return nil, lastErr +} diff --git a/test/config/hysteria-client.json b/test/config/hysteria-client.json new file mode 100644 index 00000000..3328c510 --- /dev/null +++ b/test/config/hysteria-client.json @@ -0,0 +1,12 @@ +{ + "server": "127.0.0.1:10000", + "auth_str": "password", + "obfs": "fuck me till the daylight", + "up_mbps": 100, + "down_mbps": 100, + "socks5": { + "listen": "127.0.0.1:10001" + }, + "server_name": "example.org", + "ca": "/etc/hysteria/ca.pem" +} \ No newline at end of file diff --git a/test/config/hysteria-server.json b/test/config/hysteria-server.json new file mode 100644 index 00000000..e33624a2 --- /dev/null +++ b/test/config/hysteria-server.json @@ -0,0 +1,9 @@ +{ + "listen": ":10000", + "cert": "/etc/hysteria/cert.pem", + "key": "/etc/hysteria/key.pem", + "auth_str": "password", + "obfs": "fuck me till the daylight", + "up_mbps": 100, + "down_mbps": 100 +} \ No newline at end of file diff --git a/test/config/hysteria2-client.yml b/test/config/hysteria2-client.yml new file mode 100644 index 00000000..f9cc2ede --- /dev/null +++ b/test/config/hysteria2-client.yml @@ -0,0 +1,11 @@ +server: 127.0.0.1:10000 +auth: password +socks5: + listen: 127.0.0.1:10001 +tls: + sni: example.org + ca: /etc/hysteria/ca.pem +obfs: + type: salamander + salamander: + password: cry_me_a_r1ver \ No newline at end of file diff --git a/test/config/hysteria2-server.yml b/test/config/hysteria2-server.yml new file mode 100644 index 00000000..1d1ef1c3 --- /dev/null +++ b/test/config/hysteria2-server.yml @@ -0,0 +1,12 @@ +listen: 127.0.0.1:10000 +auth: + type: password + password: password +tls: + sni: example.org + cert: /etc/hysteria/cert.pem + key: /etc/hysteria/key.pem +obfs: + type: salamander + salamander: + password: cry_me_a_r1ver \ No newline at end of file diff --git a/test/config/naive-nginx.conf b/test/config/naive-nginx.conf new file mode 100644 index 00000000..bcd06c04 --- /dev/null +++ b/test/config/naive-nginx.conf @@ -0,0 +1,18 @@ +stream { + server { + listen 10000 ssl; + listen [::]:10000 ssl; + + ssl_certificate /etc/nginx/cert.pem; + ssl_certificate_key /etc/nginx/key.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # modern configuration + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + proxy_pass 127.0.0.1:10003; + } +} \ No newline at end of file diff --git a/test/config/naive-quic.json b/test/config/naive-quic.json new file mode 100644 index 00000000..ec891cd8 --- /dev/null +++ b/test/config/naive-quic.json @@ -0,0 +1,6 @@ +{ + "listen": "socks://127.0.0.1:10001", + "proxy": "quic://sekai:password@example.org:10000", + "host-resolver-rules": "MAP example.org 127.0.0.1", + "log": "" +} \ No newline at end of file diff --git a/test/config/naive.json b/test/config/naive.json new file mode 100644 index 00000000..c7b21101 --- /dev/null +++ b/test/config/naive.json @@ -0,0 +1,6 @@ +{ + "listen": "socks://127.0.0.1:10001", + "proxy": "https://sekai:password@example.org:10000", + "host-resolver-rules": "MAP example.org 127.0.0.1", + "log": "" +} \ No newline at end of file diff --git a/test/config/nginx.conf b/test/config/nginx.conf new file mode 100644 index 00000000..124218fd --- /dev/null +++ b/test/config/nginx.conf @@ -0,0 +1,29 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; +} + +include /etc/nginx/conf.d/naive.conf; \ No newline at end of file diff --git a/test/config/shadowsocksr.json b/test/config/shadowsocksr.json new file mode 100644 index 00000000..efae403c --- /dev/null +++ b/test/config/shadowsocksr.json @@ -0,0 +1,19 @@ +{ + "server": "0.0.0.0", + "server_ipv6": "::", + "server_port": 10000, + "local_address": "127.0.0.1", + "local_port": 1080, + "password": "password0", + "timeout": 120, + "method": "aes-256-cfb", + "protocol": "origin", + "protocol_param": "", + "obfs": "plain", + "obfs_param": "", + "redirect": "", + "dns_ipv6": false, + "fast_open": true, + "workers": 1, + "forbidden_ip": "" +} \ No newline at end of file diff --git a/test/config/trojan.json b/test/config/trojan.json new file mode 100644 index 00000000..d645fc7a --- /dev/null +++ b/test/config/trojan.json @@ -0,0 +1,40 @@ +{ + "run_type": "server", + "local_addr": "0.0.0.0", + "local_port": 10000, + "password": [ + "password" + ], + "log_level": 1, + "ssl": { + "cert": "/path/to/certificate.crt", + "key": "/path/to/private.key", + "key_password": "", + "cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384", + "cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384", + "prefer_server_cipher": true, + "alpn": [ + "http/1.1" + ], + "alpn_port_override": { + "h2": 81 + }, + "reuse_session": true, + "session_ticket": false, + "session_timeout": 600, + "plain_http_response": "", + "curves": "", + "dhparam": "" + }, + "tcp": { + "prefer_ipv4": false, + "no_delay": true, + "keep_alive": true, + "reuse_port": false, + "fast_open": false, + "fast_open_qlen": 20 + }, + "mysql": { + "enabled": false + } +} \ No newline at end of file diff --git a/test/config/tuic-client.json b/test/config/tuic-client.json new file mode 100644 index 00000000..0db1755c --- /dev/null +++ b/test/config/tuic-client.json @@ -0,0 +1,16 @@ +{ + "relay": { + "server": "example.org:10000", + "ip": "127.0.0.1", + "uuid": "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", + "password": "tuic", + "certificates": [ + "/etc/tuic/ca.pem" + ] + }, + "local": { + "server": "127.0.0.1:10001", + "max_packet_size": 65535 + }, + "log_level": "debug" +} \ No newline at end of file diff --git a/test/config/tuic-server.json b/test/config/tuic-server.json new file mode 100644 index 00000000..4907796b --- /dev/null +++ b/test/config/tuic-server.json @@ -0,0 +1,10 @@ +{ + "server": "[::]:10000", + "users": { + "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D": "tuic" + }, + "certificate": "/etc/tuic/cert.pem", + "private_key": "/etc/tuic/key.pem", + "max_external_packet_size": 65535, + "log_level": "debug" +} \ No newline at end of file diff --git a/test/config/vless-server.json b/test/config/vless-server.json new file mode 100644 index 00000000..b5a7bfd0 --- /dev/null +++ b/test/config/vless-server.json @@ -0,0 +1,25 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vless", + "settings": { + "decryption": "none", + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vless-tls-client.json b/test/config/vless-tls-client.json new file mode 100644 index 00000000..e0313547 --- /dev/null +++ b/test/config/vless-tls-client.json @@ -0,0 +1,51 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": "1080", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "host.docker.internal", + "port": 1234, + "users": [ + { + "id": "", + "encryption": "none", + "flow": "" + } + ] + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ], + "fingerprint": "chrome" + } + } + } + ] +} \ No newline at end of file diff --git a/test/config/vless-tls-server.json b/test/config/vless-tls-server.json new file mode 100644 index 00000000..1c0f3524 --- /dev/null +++ b/test/config/vless-tls-server.json @@ -0,0 +1,39 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vless", + "settings": { + "decryption": "none", + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "flow": "" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-client.json b/test/config/vmess-client.json new file mode 100644 index 00000000..72381a99 --- /dev/null +++ b/test/config/vmess-client.json @@ -0,0 +1,38 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "127.0.0.1", + "port": "1080", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 1234, + "users": [ + { + "id": "", + "alterId": 0, + "security": "none", + "experiments": "" + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-grpc-client.json b/test/config/vmess-grpc-client.json new file mode 100644 index 00000000..a5cd49a5 --- /dev/null +++ b/test/config/vmess-grpc-client.json @@ -0,0 +1,51 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "127.0.0.1", + "port": "1080", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 1234, + "users": [ + { + "id": "" + } + ] + } + ] + }, + "streamSettings": { + "network": "gun", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ] + }, + "grpcSettings": { + "serviceName": "TunService" + } + } + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-grpc-server.json b/test/config/vmess-grpc-server.json new file mode 100644 index 00000000..3867d447 --- /dev/null +++ b/test/config/vmess-grpc-server.json @@ -0,0 +1,40 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "gun", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ] + }, + "grpcSettings": { + "serviceName": "TunService" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-mux-client.json b/test/config/vmess-mux-client.json new file mode 100644 index 00000000..9cb6654b --- /dev/null +++ b/test/config/vmess-mux-client.json @@ -0,0 +1,41 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "127.0.0.1", + "port": "1080", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 1234, + "users": [ + { + "id": "", + "alterId": 0, + "security": "none", + "experiments": "" + } + ] + } + ] + }, + "mux": { + "enabled": true + } + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-server.json b/test/config/vmess-server.json new file mode 100644 index 00000000..3d971e8a --- /dev/null +++ b/test/config/vmess-server.json @@ -0,0 +1,25 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "alterId": 0 + } + ] + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-ws-client.json b/test/config/vmess-ws-client.json new file mode 100644 index 00000000..532ef134 --- /dev/null +++ b/test/config/vmess-ws-client.json @@ -0,0 +1,52 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "127.0.0.1", + "port": "1080", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 1234, + "users": [ + { + "id": "" + } + ] + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ] + }, + "wsSettings": { + "maxEarlyData": 2048, + "earlyDataHeaderName": "" + } + } + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-ws-server.json b/test/config/vmess-ws-server.json new file mode 100644 index 00000000..7eaef4ff --- /dev/null +++ b/test/config/vmess-ws-server.json @@ -0,0 +1,41 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "serverName": "example.org", + "certificates": [ + { + "certificateFile": "/path/to/certificate.crt", + "keyFile": "/path/to/private.key" + } + ] + }, + "wsSettings": { + "maxEarlyData": 2048, + "earlyDataHeaderName": "" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/wireguard.conf b/test/config/wireguard.conf new file mode 100644 index 00000000..b1140c31 --- /dev/null +++ b/test/config/wireguard.conf @@ -0,0 +1,8 @@ +[Interface] +PrivateKey = gHWUGzTh5YCEV6k8dneVP537XhVtoQJPIlFNs2zsxlE= +Address = 10.0.0.1/32 +ListenPort = 10000 + +[Peer] +PublicKey = LV2xr9tzxwbs0ZLUlFN9k/0Or9QWqIInvxc/Cu7/2hA= +AllowedIPs = 10.0.0.2/32 \ No newline at end of file diff --git a/test/direct_test.go b/test/direct_test.go new file mode 100644 index 00000000..d33defee --- /dev/null +++ b/test/direct_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +// Since this is a feature one-off added by outsiders, I won't address these anymore. +func _TestProxyProtocol(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeDirect, + Options: &option.DirectInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + ProxyProtocol: true, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeDirect, + Tag: "proxy-out", + Options: &option.DirectOutboundOptions{ + OverrideAddress: "127.0.0.1", + OverridePort: serverPort, + ProxyProtocol: 2, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "proxy-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/docker_test.go b/test/docker_test.go new file mode 100644 index 00000000..a85dd12c --- /dev/null +++ b/test/docker_test.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sagernet/sing/common/debug" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/rw" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" +) + +type DockerOptions struct { + Image string + EntryPoint string + Ports []uint16 + Cmd []string + Env []string + Bind map[string]string + Stdin []byte + Cap []string +} + +func startDockerContainer(t *testing.T, options DockerOptions) { + dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + require.NoError(t, err) + defer dockerClient.Close() + + writeStdin := len(options.Stdin) > 0 + + var containerOptions container.Config + + if writeStdin { + containerOptions.OpenStdin = true + containerOptions.StdinOnce = true + } + + containerOptions.Image = options.Image + if options.EntryPoint != "" { + containerOptions.Entrypoint = []string{options.EntryPoint} + } + containerOptions.Cmd = options.Cmd + containerOptions.Env = options.Env + containerOptions.ExposedPorts = make(nat.PortSet) + + var hostOptions container.HostConfig + hostOptions.NetworkMode = "host" + hostOptions.CapAdd = options.Cap + hostOptions.PortBindings = make(nat.PortMap) + + for _, port := range options.Ports { + containerOptions.ExposedPorts[nat.Port(F.ToString(port, "/tcp"))] = struct{}{} + containerOptions.ExposedPorts[nat.Port(F.ToString(port, "/udp"))] = struct{}{} + hostOptions.PortBindings[nat.Port(F.ToString(port, "/tcp"))] = []nat.PortBinding{ + {HostPort: F.ToString(port), HostIP: "0.0.0.0"}, + } + hostOptions.PortBindings[nat.Port(F.ToString(port, "/udp"))] = []nat.PortBinding{ + {HostPort: F.ToString(port), HostIP: "0.0.0.0"}, + } + } + + if len(options.Bind) > 0 { + hostOptions.Binds = []string{} + for path, internalPath := range options.Bind { + if !rw.FileExists(path) { + path = filepath.Join("config", path) + } + path, _ = filepath.Abs(path) + hostOptions.Binds = append(hostOptions.Binds, path+":"+internalPath) + } + } + + dockerContainer, err := dockerClient.ContainerCreate(context.Background(), &containerOptions, &hostOptions, nil, nil, "") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(dockerContainer.ID) + }) + + require.NoError(t, dockerClient.ContainerStart(context.Background(), dockerContainer.ID, container.StartOptions{})) + + if writeStdin { + stdinAttach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, container.AttachOptions{ + Stdin: writeStdin, + Stream: true, + }) + require.NoError(t, err) + _, err = stdinAttach.Conn.Write(options.Stdin) + require.NoError(t, err) + stdinAttach.Close() + } + if debug.Enabled { + attach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, container.AttachOptions{ + Stdout: true, + Stderr: true, + Logs: true, + Stream: true, + }) + require.NoError(t, err) + go func() { + stdcopy.StdCopy(os.Stderr, os.Stderr, attach.Reader) + }() + } + time.Sleep(time.Second) +} + +func cleanContainer(id string) error { + dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return err + } + defer dockerClient.Close() + return dockerClient.ContainerRemove(context.Background(), id, container.RemoveOptions{Force: true}) +} diff --git a/test/domain_inbound_test.go b/test/domain_inbound_test.go new file mode 100644 index 00000000..02354564 --- /dev/null +++ b/test/domain_inbound_test.go @@ -0,0 +1,94 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +func TestTUICDomainUDP(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTUIC, + Options: &option.TUICInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TUICUser{{ + UUID: uuid.Nil.String(), + }}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTUIC, + Tag: "tuic-out", + Options: &option.TUICOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: uuid.Nil.String(), + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "tuic-out", + }, + }, + }, + }, + }, + }, + }) + testQUIC(t, clientPort) +} diff --git a/test/ech_test.go b/test/ech_test.go new file mode 100644 index 00000000..91d13d3a --- /dev/null +++ b/test/ech_test.go @@ -0,0 +1,287 @@ +package main + +import ( + "net/netip" + "testing" + + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +func TestECH(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: "password", + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestECHQUIC(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTUIC, + Options: &option.TUICInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TUICUser{{ + UUID: uuid.Nil.String(), + }}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTUIC, + Tag: "tuic-out", + Options: &option.TUICOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: uuid.Nil.String(), + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "tuic-out", + }, + }, + }, + }, + }, + }, + }) + testSuitLargeUDP(t, clientPort, testPort) +} + +func TestECHHysteria2(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeHysteria2, + Options: &option.Hysteria2InboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.Hysteria2User{{ + Password: "password", + }}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeHysteria2, + Tag: "hy2-out", + Options: &option.Hysteria2OutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "hy2-out", + }, + }, + }, + }, + }, + }, + }) + testSuitLargeUDP(t, clientPort, testPort) +} diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 00000000..7d6d9edc --- /dev/null +++ b/test/go.mod @@ -0,0 +1,179 @@ +module test + +go 1.24.7 + +require github.com/sagernet/sing-box v0.0.0 + +replace github.com/sagernet/sing-box => ../ + +require ( + github.com/docker/docker v27.3.1+incompatible + github.com/docker/go-connections v0.5.0 + github.com/gofrs/uuid/v5 v5.4.0 + github.com/sagernet/quic-go v0.59.0-sing-box-mod.2 + github.com/sagernet/sing v0.8.0-beta.16 + github.com/sagernet/sing-quic v0.6.0-beta.11 + github.com/sagernet/sing-shadowsocks v0.2.8 + github.com/sagernet/sing-shadowsocks2 v0.2.1 + github.com/spyzhov/ajson v0.9.4 + github.com/stretchr/testify v1.11.1 + go.uber.org/goleak v1.3.0 + golang.org/x/net v0.48.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/anthropics/anthropic-sdk-go v1.19.0 // indirect + github.com/anytls/sing-anytls v0.0.11 // indirect + github.com/caddyserver/certmagic v0.25.0 // indirect + github.com/caddyserver/zerossl v0.1.3 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/cretz/bine v0.2.0 // indirect + github.com/database64128/netx-go v0.1.1 // indirect + github.com/database64128/tfo-go/v2 v2.3.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gaissmai/bart v0.18.0 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/go-chi/render v1.0.3 // indirect + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/godbus/dbus/v5 v5.2.1 // indirect + github.com/gogo/protobuf v1.3.2 // 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.7.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 // indirect + github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/keybase/go-keychain v0.0.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/libdns/acmedns v0.5.0 // indirect + github.com/libdns/alidns v1.0.6-beta.3 // indirect + github.com/libdns/cloudflare v0.2.2 // indirect + github.com/libdns/libdns v1.1.1 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/metacubex/utls v1.8.4 // indirect + github.com/mholt/acmez/v3 v3.1.4 // indirect + github.com/miekg/dns v1.1.69 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/openai/openai-go/v3 v3.15.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect + github.com/sagernet/cors v1.2.1 // indirect + github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287 // indirect + github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f // indirect + github.com/sagernet/fswatch v0.1.1 // indirect + github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect + github.com/sagernet/sing-mux v0.3.4 // indirect + github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect + github.com/sagernet/sing-tun v0.8.0-beta.17 // indirect + github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect + github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 // indirect + github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 // indirect + github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect + github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect + github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect + github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.40.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/windows v0.5.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect + lukechampine.com/blake3 v1.3.0 // indirect +) diff --git a/test/go.sum b/test/go.sum new file mode 100644 index 00000000..34f8d997 --- /dev/null +++ b/test/go.sum @@ -0,0 +1,429 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE= +github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= +github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= +github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= +github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= +github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= +github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= +github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= +github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= +github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= +github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= +github.com/database64128/tfo-go/v2 v2.3.1 h1:EGE+ELd5/AQ0X6YBlQ9RgKs8+kciNhgN3d8lRvfEJQw= +github.com/database64128/tfo-go/v2 v2.3.1/go.mod h1:k9wcpg/8i5zenspBkc9jUEYehpZZccBnCElzOJB++bU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= +github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= +github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/godbus/dbus/v5 v5.2.1 h1:I4wwMdWSkmI57ewd+elNGwLRf2/dtSaFz1DujfWYvOk= +github.com/godbus/dbus/v5 v5.2.1/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= +github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 h1:MEufgJohwIjFi2n3eJv4c/8UdRLQVUwPwSWQPoER+eU= +github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE= +github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ= +github.com/libdns/alidns v1.0.6-beta.3 h1:KAmb7FQ1tRzKsaAUGa7ZpGKAMRANwg7+1c7tUbSELq8= +github.com/libdns/alidns v1.0.6-beta.3/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec= +github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= +github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= +github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= +github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= +github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= +github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo= +github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= +github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= +github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287 h1:0BYNmr0ptjsII948U0oBFmrbo4qEaCFcrE2JPRg3Zlk= +github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287 h1:ghxhYSBQpzkakqWqJDvXr/Zmxe0WjTjKuALEGbjGiGY= +github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287/go.mod h1:M+4ZjPhLJXIvoxcQsbDofmc19Wrig59hZ+hLvj6S3To= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f h1:8jZbZ4KBTdcXDFLwUBNQt5Xci6ZuAKh255S8TwuBCaM= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f h1:tG0hCx+0u5zca7qQ7AMkcv4DCrBG/DKW1ggs/P+BRRI= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f h1:ZXp5hKJIA7iJ52ZShJCKMQEPLpp/7dDIVZmPGV9Il40= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f h1:gL7H8HS8s38adz4/HZtRHh79qMwsbLTRRPz4GQ9LcWI= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f h1:Dchgc0pAY5Jwb5lzUlE+1nhHIzqLx+YOurXLHgvWd/0= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f h1:+MOLSQoduuKDxF410i1LcSPaQGaiP0eZb0INvMlmjM4= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f h1:lIZna05Vn6n8k21p8OpSUnhwGm+E57PrMjiI4ZUfMSg= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f h1:B2aFQ5CRHI20t8YsEizvtguS5W2QfK7D5XV/NzTIxPE= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f h1:qpSwJ1rFGYCfJDenNCZoWYjoG7N+xEa6ke+E7/JO1i4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f h1:cx7Ipg0tSvTDjS4maMEYz4vuzz93BMPAysmZ1YLrz80= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f h1:4jOHuUiBxD8pJEpBBVQfJqyLmxjpd3t4MLRzU7YLFyg= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f h1:OpXBa2WlRU+Mam9oRe9Nn4/zf7gQ+qiBTNK8A5RwbfQ= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f h1:nJpGFi+6hI85tl4zoyNFEnFEQ5+xEV5gyvsUoMvd8g0= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f h1:SEy2rpmgOJgrqcEryJI/RSnqUWIsEsp0cfYoA8y21jc= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f h1:EW2TuFMLm0iBGqRZtuGwIZdeYmDtDsDmRcRRJQOMxUo= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f h1:3U5woxrNCkzfv1+UX+mVoWh1228AE1qAiMG02F9oFbY= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f h1:YwFTfuWG3mmctroeDYtFZ6LHjGsedVO+5wInYbbUuUY= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f h1:r4V0ddPCRLgGu0VdgR3aUsO9NjpmyjAf+h+3oTD9D6E= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f h1:B8yf4gFvEYUnwWmtVK9sdwUsflYZ387MhYmlOP2ohFQ= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f h1:9YyaMg4rO1/jIgrxmNb0LKH+X7frSYWfX2pFgW5JUVM= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f h1:B0fnGu0sh9yT/9JDN5u/GqThGoOzNN/daOAuGWFLXEk= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f h1:lxPcIXKSSI5JDhc7rx/6yufISWM4vtBS2FY9PavWQTs= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPrSEgNII+HkfqC8hKMpYLodom4o4EU= +github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/quic-go v0.59.0-sing-box-mod.2 h1:hJUL+HtxEOjxsa0CsucbBVqI/AMS4k52NwNU637zmdw= +github.com/sagernet/quic-go v0.59.0-sing-box-mod.2/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= +github.com/sagernet/sing v0.8.0-beta.16 h1:Fe+6E9VHYky9Mx4cf0ugbZPWDcXRflpAu7JQ5bWXvaA= +github.com/sagernet/sing v0.8.0-beta.16/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= +github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= +github.com/sagernet/sing-quic v0.6.0-beta.11 h1:eUusxITKKRedhWC2ScUYFUvD96h/QfbKLaS3N6/7in4= +github.com/sagernet/sing-quic v0.6.0-beta.11/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= +github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= +github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= +github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= +github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= +github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= +github.com/sagernet/sing-tun v0.8.0-beta.17 h1:6DdbNXeTFYj8Tb4FCh8Mp2boA3rVY6VNqzTOObj7Xis= +github.com/sagernet/sing-tun v0.8.0-beta.17/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8= +github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= +github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= +github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= +github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 h1:eYz/OpMqWCvO2++iw3dEuzrlfC2xv78GdlGvprIM6O8= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 h1:E2tZFeg9mGYGQ7E7BbxMv1cU35HxwgRm6tPKI2Pp7DA= +github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spyzhov/ajson v0.9.4 h1:MVibcTCgO7DY4IlskdqIlCmDOsUOZ9P7oKj8ifdcf84= +github.com/spyzhov/ajson v0.9.4/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= +lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/test/http_test.go b/test/http_test.go new file mode 100644 index 00000000..0e9185ef --- /dev/null +++ b/test/http_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestHTTPSelf(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeHTTP, + Tag: "http-out", + Options: &option.HTTPOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "http-out", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go new file mode 100644 index 00000000..115af4a7 --- /dev/null +++ b/test/hysteria2_test.go @@ -0,0 +1,234 @@ +package main + +import ( + "net/netip" + "testing" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestHysteria2Self(t *testing.T) { + t.Run("self", func(t *testing.T) { + testHysteria2Self(t, "", false) + }) + t.Run("self-salamander", func(t *testing.T) { + testHysteria2Self(t, "password", false) + }) + t.Run("self-hop", func(t *testing.T) { + testHysteria2Self(t, "", true) + }) + t.Run("self-hop-salamander", func(t *testing.T) { + testHysteria2Self(t, "password", true) + }) +} + +func TestHysteria2Hop(t *testing.T) { + testHysteria2Self(t, "password", true) +} + +func testHysteria2Self(t *testing.T, salamanderPassword string, portHop bool) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + var obfs *option.Hysteria2Obfs + if salamanderPassword != "" { + obfs = &option.Hysteria2Obfs{ + Type: hysteria2.ObfsTypeSalamander, + Password: salamanderPassword, + } + } + var ( + serverPorts []string + hopInterval time.Duration + ) + if portHop { + serverPorts = []string{F.ToString(serverPort, ":", serverPort)} + hopInterval = 5 * time.Second + } + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeHysteria2, + Options: &option.Hysteria2InboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + UpMbps: 100, + DownMbps: 100, + Obfs: obfs, + Users: []option.Hysteria2User{{ + Password: "password", + }}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeHysteria2, + Tag: "hy2-out", + Options: &option.Hysteria2OutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + ServerPorts: serverPorts, + HopInterval: badoption.Duration(hopInterval), + UpMbps: 100, + DownMbps: 100, + Obfs: obfs, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "hy2-out", + }, + }, + }, + }, + }, + }, + }) + testSuitLargeUDP(t, clientPort, testPort) + if portHop { + time.Sleep(5 * time.Second) + testSuitLargeUDP(t, clientPort, testPort) + } +} + +func TestHysteria2Inbound(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeHysteria2, + Options: &option.Hysteria2InboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Obfs: &option.Hysteria2Obfs{ + Type: hysteria2.ObfsTypeSalamander, + Password: "cry_me_a_r1ver", + }, + Users: []option.Hysteria2User{{ + Password: "password", + }}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageHysteria2, + Ports: []uint16{serverPort, clientPort}, + Cmd: []string{"client", "-c", "/etc/hysteria/config.yml", "--disable-update-check", "--log-level", "debug"}, + Bind: map[string]string{ + "hysteria2-client.yml": "/etc/hysteria/config.yml", + caPem: "/etc/hysteria/ca.pem", + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestHysteria2Outbound(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startDockerContainer(t, DockerOptions{ + Image: ImageHysteria2, + Ports: []uint16{testPort}, + Cmd: []string{"server", "-c", "/etc/hysteria/config.yml", "--disable-update-check", "--log-level", "debug"}, + Bind: map[string]string{ + "hysteria2-server.yml": "/etc/hysteria/config.yml", + certPem: "/etc/hysteria/cert.pem", + keyPem: "/etc/hysteria/key.pem", + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeHysteria2, + Options: &option.Hysteria2OutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Obfs: &option.Hysteria2Obfs{ + Type: hysteria2.ObfsTypeSalamander, + Password: "cry_me_a_r1ver", + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + }) + testSuitSimple1(t, clientPort, testPort) +} diff --git a/test/hysteria_test.go b/test/hysteria_test.go new file mode 100644 index 00000000..a7f73dea --- /dev/null +++ b/test/hysteria_test.go @@ -0,0 +1,189 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestHysteriaSelf(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeHysteria, + Options: &option.HysteriaInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + UpMbps: 100, + DownMbps: 100, + Users: []option.HysteriaUser{{ + AuthString: "password", + }}, + Obfs: "fuck me till the daylight", + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeHysteria, + Tag: "hy-out", + Options: &option.HysteriaOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UpMbps: 100, + DownMbps: 100, + AuthString: "password", + Obfs: "fuck me till the daylight", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "hy-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestHysteriaInbound(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeHysteria, + Options: &option.HysteriaInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + UpMbps: 100, + DownMbps: 100, + Users: []option.HysteriaUser{{ + AuthString: "password", + }}, + Obfs: "fuck me till the daylight", + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageHysteria, + Ports: []uint16{serverPort, clientPort}, + Cmd: []string{"-c", "/etc/hysteria/config.json", "client"}, + Bind: map[string]string{ + "hysteria-client.json": "/etc/hysteria/config.json", + caPem: "/etc/hysteria/ca.pem", + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestHysteriaOutbound(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startDockerContainer(t, DockerOptions{ + Image: ImageHysteria, + Ports: []uint16{testPort}, + Cmd: []string{"-c", "/etc/hysteria/config.json", "server"}, + Bind: map[string]string{ + "hysteria-server.json": "/etc/hysteria/config.json", + certPem: "/etc/hysteria/cert.pem", + keyPem: "/etc/hysteria/key.pem", + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeHysteria, + Options: &option.HysteriaOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UpMbps: 100, + DownMbps: 100, + AuthString: "password", + Obfs: "fuck me till the daylight", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + }) + testSuitSimple1(t, clientPort, testPort) +} diff --git a/test/inbound_detour_test.go b/test/inbound_detour_test.go new file mode 100644 index 00000000..f4043895 --- /dev/null +++ b/test/inbound_detour_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestChainedInbound(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + Detour: "detour", + }, + Method: method, + Password: password, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour", + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: otherPort, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + Method: method, + Password: password, + DialerOptions: option.DialerOptions{ + Detour: "detour-out", + }, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} diff --git a/test/ktls_test.go b/test/ktls_test.go new file mode 100644 index 00000000..c873a162 --- /dev/null +++ b/test/ktls_test.go @@ -0,0 +1,295 @@ +package main + +import ( + "net/netip" + "testing" + + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +func TestKTLS(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: "password", + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + // KernelTx: true, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KernelTx: true, + KernelRx: true, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestKTLSECH(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: "password", + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + KernelTx: true, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KernelTx: true, + KernelRx: true, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestKTLSReality(t *testing.T) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVLESS, + Options: &option.VLESSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VLESSUser{{UUID: user.String()}}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + KernelTx: true, + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVLESS, + Tag: "ss-out", + Options: &option.VLESSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + KernelTx: true, + KernelRx: true, + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/mkcert.go b/test/mkcert.go new file mode 100644 index 00000000..2d35ff84 --- /dev/null +++ b/test/mkcert.go @@ -0,0 +1,88 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sagernet/sing/common/rw" + + "github.com/stretchr/testify/require" +) + +func createSelfSignedCertificate(t *testing.T, domain string) (caPem, certPem, keyPem string) { + const userAndHostname = "sekai@nekohasekai.local" + tempDir, err := os.MkdirTemp("", "sing-box-test") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(tempDir) + }) + caKey, err := rsa.GenerateKey(rand.Reader, 3072) + require.NoError(t, err) + spkiASN1, err := x509.MarshalPKIXPublicKey(caKey.Public()) + var spki struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString + } + _, err = asn1.Unmarshal(spkiASN1, &spki) + require.NoError(t, err) + skid := sha1.Sum(spki.SubjectPublicKey.Bytes) + caTpl := &x509.Certificate{ + SerialNumber: randomSerialNumber(t), + Subject: pkix.Name{ + Organization: []string{"sing-box test CA"}, + OrganizationalUnit: []string{userAndHostname}, + CommonName: "sing-box " + userAndHostname, + }, + SubjectKeyId: skid[:], + NotAfter: time.Now().AddDate(10, 0, 0), + NotBefore: time.Now(), + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + caCert, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, caKey.Public(), caKey) + require.NoError(t, err) + err = rw.WriteFile(filepath.Join(tempDir, "ca.pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})) + require.NoError(t, err) + key, err := rsa.GenerateKey(rand.Reader, 2048) + domainTpl := &x509.Certificate{ + SerialNumber: randomSerialNumber(t), + Subject: pkix.Name{ + Organization: []string{"sing-box test certificate"}, + OrganizationalUnit: []string{"sing-box " + userAndHostname}, + }, + NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 30), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + domainTpl.DNSNames = append(domainTpl.DNSNames, domain) + cert, err := x509.CreateCertificate(rand.Reader, domainTpl, caTpl, key.Public(), caKey) + require.NoError(t, err) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}) + privDER, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + privPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER}) + err = rw.WriteFile(filepath.Join(tempDir, domain+".pem"), certPEM) + require.NoError(t, err) + err = rw.WriteFile(filepath.Join(tempDir, domain+".key.pem"), privPEM) + require.NoError(t, err) + return filepath.Join(tempDir, "ca.pem"), filepath.Join(tempDir, domain+".pem"), filepath.Join(tempDir, domain+".key.pem") +} + +func randomSerialNumber(t *testing.T) *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + return serialNumber +} diff --git a/test/mux_cool_test.go b/test/mux_cool_test.go new file mode 100644 index 00000000..ed42a059 --- /dev/null +++ b/test/mux_cool_test.go @@ -0,0 +1,182 @@ +package main + +import ( + "net/netip" + "os" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/spyzhov/ajson" + "github.com/stretchr/testify/require" +) + +func TestMuxCoolServer(t *testing.T) { + userId := newUUID() + content, err := os.ReadFile("config/vmess-mux-client.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) + outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) + outbound.MustKey("port").SetNumeric(float64(serverPort)) + user := outbound.MustKey("users").MustIndex(0) + user.MustKey("id").SetString(userId.String()) + + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + }) + + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: userId.String(), + }, + }, + }, + }, + }, + }) + + testSuitSimple(t, clientPort, testPort) +} + +func TestMuxCoolClient(t *testing.T) { + user := newUUID() + content, err := os.ReadFile("config/vmess-server.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + inbound := config.MustKey("inbounds").MustIndex(0) + inbound.MustKey("port").SetNumeric(float64(serverPort)) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String()) + + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageXRayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "xray", + Stdin: content, + }) + + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeVMess, + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + PacketEncoding: "xudp", + }, + }, + }, + }) + testSuitSimple(t, clientPort, testPort) +} + +func TestMuxCoolSelf(t *testing.T) { + user := newUUID() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: user.String(), + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + PacketEncoding: "xudp", + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuitSimple(t, clientPort, testPort) +} diff --git a/test/mux_test.go b/test/mux_test.go new file mode 100644 index 00000000..6454d19c --- /dev/null +++ b/test/mux_test.go @@ -0,0 +1,198 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +var muxProtocols = []string{ + "h2mux", + "smux", + "yamux", +} + +func TestVMessSMux(t *testing.T) { + testVMessMux(t, option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "smux", + }) +} + +func TestShadowsocksMux(t *testing.T) { + for _, protocol := range muxProtocols { + t.Run(protocol, func(t *testing.T) { + testShadowsocksMux(t, option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: protocol, + }) + }) + } +} + +func TestShadowsockH2Mux(t *testing.T) { + testShadowsocksMux(t, option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "h2mux", + Padding: true, + }) +} + +func TestShadowsockSMuxPadding(t *testing.T) { + testShadowsocksMux(t, option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "smux", + Padding: true, + }) +} + +func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Method: method, + Password: password, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + Multiplex: &options, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + UUID: user.String(), + }, + }, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Security: "auto", + UUID: user.String(), + Multiplex: &options, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/naive_self_test.go b/test/naive_self_test.go new file mode 100644 index 00000000..9d293bfb --- /dev/null +++ b/test/naive_self_test.go @@ -0,0 +1,533 @@ +//go:build with_naive_outbound + +package main + +import ( + "net/netip" + "os" + "strings" + "testing" + + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/naive" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func TestNaiveSelf(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + caPemContent, err := os.ReadFile(caPem) + require.NoError(t, err) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeNaive, + Tag: "naive-in", + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkTCP, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeNaive, + Tag: "naive-out", + Options: &option.NaiveOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Username: "sekai", + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + Certificate: []string{string(caPemContent)}, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "naive-out", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestNaiveSelfECH(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + caPemContent, err := os.ReadFile(caPem) + require.NoError(t, err) + echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) + instance := startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeNaive, + Tag: "naive-in", + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkTCP, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeNaive, + Tag: "naive-out", + Options: &option.NaiveOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Username: "sekai", + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + Certificate: []string{string(caPemContent)}, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "naive-out", + }, + }, + }, + }, + }, + }, + }) + + naiveOut, ok := instance.Outbound().Outbound("naive-out") + require.True(t, ok) + naiveOutbound := naiveOut.(*naive.Outbound) + + netLogPath := "/tmp/naive_ech_netlog.json" + require.True(t, naiveOutbound.Client().Engine().StartNetLogToFile(netLogPath, true)) + defer naiveOutbound.Client().Engine().StopNetLog() + + testTCP(t, clientPort, testPort) + + naiveOutbound.Client().Engine().StopNetLog() + + logContent, err := os.ReadFile(netLogPath) + require.NoError(t, err) + logStr := string(logContent) + + require.True(t, strings.Contains(logStr, `"encrypted_client_hello":true`), + "ECH should be accepted in TLS handshake. NetLog saved to: %s", netLogPath) +} + +func TestNaiveSelfInsecureConcurrency(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + caPemContent, err := os.ReadFile(caPem) + require.NoError(t, err) + + instance := startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeNaive, + Tag: "naive-in", + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkTCP, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeNaive, + Tag: "naive-out", + Options: &option.NaiveOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Username: "sekai", + Password: "password", + InsecureConcurrency: 3, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + Certificate: []string{string(caPemContent)}, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "naive-out", + }, + }, + }, + }, + }, + }, + }) + + naiveOut, ok := instance.Outbound().Outbound("naive-out") + require.True(t, ok) + naiveOutbound := naiveOut.(*naive.Outbound) + + netLogPath := "/tmp/naive_concurrency_netlog.json" + require.True(t, naiveOutbound.Client().Engine().StartNetLogToFile(netLogPath, true)) + defer naiveOutbound.Client().Engine().StopNetLog() + + // Send multiple sequential connections to trigger round-robin + // With insecure_concurrency=3, connections will be distributed to 3 pools + for i := 0; i < 6; i++ { + testTCP(t, clientPort, testPort) + } + + naiveOutbound.Client().Engine().StopNetLog() + + // Verify NetLog contains multiple independent HTTP/2 sessions + logContent, err := os.ReadFile(netLogPath) + require.NoError(t, err) + logStr := string(logContent) + + // Count HTTP2_SESSION_INITIALIZED events to verify connection pool isolation + // NetLog stores event types as numeric IDs, HTTP2_SESSION_INITIALIZED = 249 + sessionCount := strings.Count(logStr, `"type":249`) + require.GreaterOrEqual(t, sessionCount, 3, + "Expected at least 3 HTTP/2 sessions with insecure_concurrency=3. NetLog: %s", netLogPath) +} + +func TestNaiveSelfQUIC(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + caPemContent, err := os.ReadFile(caPem) + require.NoError(t, err) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeNaive, + Tag: "naive-in", + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkUDP, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeNaive, + Tag: "naive-out", + Options: &option.NaiveOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Username: "sekai", + Password: "password", + QUIC: true, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + Certificate: []string{string(caPemContent)}, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "naive-out", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestNaiveSelfQUICCongestionControl(t *testing.T) { + testCases := []struct { + name string + congestionControl string + }{ + {"BBR", "bbr"}, + {"BBR2", "bbr2"}, + {"Cubic", "cubic"}, + {"Reno", "reno"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + caPemContent, err := os.ReadFile(caPem) + require.NoError(t, err) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeNaive, + Tag: "naive-in", + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkUDP, + QUICCongestionControl: tc.congestionControl, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeNaive, + Tag: "naive-out", + Options: &option.NaiveOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Username: "sekai", + Password: "password", + QUIC: true, + QUICCongestionControl: tc.congestionControl, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + Certificate: []string{string(caPemContent)}, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "naive-out", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) + }) + } +} diff --git a/test/naive_test.go b/test/naive_test.go new file mode 100644 index 00000000..9dbe9855 --- /dev/null +++ b/test/naive_test.go @@ -0,0 +1,147 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/network" +) + +func TestNaiveInboundWithNginx(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeNaive, + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: otherPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkTCP, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageNginx, + Ports: []uint16{serverPort, otherPort}, + Bind: map[string]string{ + "nginx.conf": "/etc/nginx/nginx.conf", + "naive-nginx.conf": "/etc/nginx/conf.d/naive.conf", + certPem: "/etc/nginx/cert.pem", + keyPem: "/etc/nginx/key.pem", + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageNaive, + Ports: []uint16{serverPort, clientPort}, + Bind: map[string]string{ + "naive.json": "/etc/naiveproxy/config.json", + caPem: "/etc/naiveproxy/ca.pem", + }, + Env: []string{ + "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestNaiveInbound(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeNaive, + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkTCP, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageNaive, + Ports: []uint16{serverPort, clientPort}, + Bind: map[string]string{ + "naive.json": "/etc/naiveproxy/config.json", + caPem: "/etc/naiveproxy/ca.pem", + }, + Env: []string{ + "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestNaiveHTTP3Inbound(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeNaive, + Options: &option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkUDP, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageNaive, + Ports: []uint16{serverPort, clientPort}, + Bind: map[string]string{ + "naive-quic.json": "/etc/naiveproxy/config.json", + caPem: "/etc/naiveproxy/ca.pem", + }, + Env: []string{ + "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", + }, + }) + testTCP(t, clientPort, testPort) +} diff --git a/test/reality_test.go b/test/reality_test.go new file mode 100644 index 00000000..220ccb6c --- /dev/null +++ b/test/reality_test.go @@ -0,0 +1,108 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +func TestReality(t *testing.T) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVLESS, + Options: &option.VLESSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VLESSUser{{UUID: user.String()}}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + }, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVLESS, + Tag: "ss-out", + Options: &option.VLESSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/shadowsocks_legacy_test.go b/test/shadowsocks_legacy_test.go new file mode 100644 index 00000000..8182e6cb --- /dev/null +++ b/test/shadowsocks_legacy_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks2/shadowstream" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestShadowsocksLegacy(t *testing.T) { + testShadowsocksLegacy(t, shadowstream.MethodList[0]) +} + +func testShadowsocksLegacy(t *testing.T, method string) { + startDockerContainer(t, DockerOptions{ + Image: ImageShadowsocksLegacy, + Ports: []uint16{serverPort}, + Env: []string{ + "SS_MODULE=ss-server", + F.ToString("SS_CONFIG=-s 0.0.0.0 -u -p 10000 -m ", method, " -k FzcLbKs2dY9mhL"), + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: "FzcLbKs2dY9mhL", + }, + }, + }, + }) + testSuitSimple(t, clientPort, testPort) +} diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go new file mode 100644 index 00000000..3f526b10 --- /dev/null +++ b/test/shadowsocks_test.go @@ -0,0 +1,367 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/stretchr/testify/require" +) + +const ( + serverPort uint16 = 10000 + iota + clientPort + testPort + otherPort + otherClientPort +) + +func TestShadowsocks(t *testing.T) { + for _, method := range []string{ + "aes-128-gcm", + "aes-256-gcm", + "chacha20-ietf-poly1305", + } { + t.Run(method+"-inbound", func(t *testing.T) { + testShadowsocksInboundWithShadowsocksRust(t, method, mkBase64(t, 16)) + }) + t.Run(method+"-outbound", func(t *testing.T) { + testShadowsocksOutboundWithShadowsocksRust(t, method, mkBase64(t, 16)) + }) + t.Run(method+"-self", func(t *testing.T) { + testShadowsocksSelf(t, method, mkBase64(t, 16)) + }) + } +} + +func TestShadowsocksNone(t *testing.T) { + testShadowsocksSelf(t, "none", "") +} + +func TestShadowsocks2022(t *testing.T) { + for _, method16 := range []string{ + "2022-blake3-aes-128-gcm", + } { + t.Run(method16+"-inbound", func(t *testing.T) { + testShadowsocksInboundWithShadowsocksRust(t, method16, mkBase64(t, 16)) + }) + t.Run(method16+"-outbound", func(t *testing.T) { + testShadowsocksOutboundWithShadowsocksRust(t, method16, mkBase64(t, 16)) + }) + t.Run(method16+"-self", func(t *testing.T) { + testShadowsocksSelf(t, method16, mkBase64(t, 16)) + }) + } + for _, method32 := range []string{ + "2022-blake3-aes-256-gcm", + "2022-blake3-chacha20-poly1305", + } { + t.Run(method32+"-inbound", func(t *testing.T) { + testShadowsocksInboundWithShadowsocksRust(t, method32, mkBase64(t, 32)) + }) + t.Run(method32+"-outbound", func(t *testing.T) { + testShadowsocksOutboundWithShadowsocksRust(t, method32, mkBase64(t, 32)) + }) + t.Run(method32+"-self", func(t *testing.T) { + testShadowsocksSelf(t, method32, mkBase64(t, 32)) + }) + } +} + +func TestShadowsocks2022EIH(t *testing.T) { + for _, method16 := range []string{ + "2022-blake3-aes-128-gcm", + } { + t.Run(method16, func(t *testing.T) { + testShadowsocks2022EIH(t, method16, mkBase64(t, 16)) + }) + } + for _, method32 := range []string{ + "2022-blake3-aes-256-gcm", + } { + t.Run(method32, func(t *testing.T) { + testShadowsocks2022EIH(t, method32, mkBase64(t, 32)) + }) + } +} + +func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, password string) { + startDockerContainer(t, DockerOptions{ + Image: ImageShadowsocksRustClient, + EntryPoint: "sslocal", + Ports: []uint16{serverPort, clientPort}, + Cmd: []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"}, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, password string) { + startDockerContainer(t, DockerOptions{ + Image: ImageShadowsocksRustServer, + EntryPoint: "ssserver", + Ports: []uint16{serverPort, testPort}, + Cmd: []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"}, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testShadowsocksSelf(t *testing.T, method string, password string) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestShadowsocksUoT(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + UDPOverTCP: &option.UDPOverTCPOptions{ + Enabled: true, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testShadowsocks2022EIH(t *testing.T, method string, password string) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Method: method, + Password: password, + Users: []option.ShadowsocksUser{ + { + Password: password, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password + ":" + password, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func mkBase64(t *testing.T, length int) string { + psk := make([]byte, length) + _, err := rand.Read(psk) + require.NoError(t, err) + return base64.StdEncoding.EncodeToString(psk) +} diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go new file mode 100644 index 00000000..6c4b71d4 --- /dev/null +++ b/test/shadowtls_test.go @@ -0,0 +1,498 @@ +package main + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/stretchr/testify/require" +) + +func TestShadowTLS(t *testing.T) { + t.Run("v1", func(t *testing.T) { + testShadowTLS(t, 1, "", false, option.ShadowTLSWildcardSNIOff) + }) + t.Run("v2", func(t *testing.T) { + testShadowTLS(t, 2, "hello", false, option.ShadowTLSWildcardSNIOff) + }) + t.Run("v3", func(t *testing.T) { + testShadowTLS(t, 3, "hello", false, option.ShadowTLSWildcardSNIOff) + }) + t.Run("v2-utls", func(t *testing.T) { + testShadowTLS(t, 2, "hello", true, option.ShadowTLSWildcardSNIOff) + }) + t.Run("v3-utls", func(t *testing.T) { + testShadowTLS(t, 3, "hello", true, option.ShadowTLSWildcardSNIOff) + }) + t.Run("v3-wildcard-sni-authed", func(t *testing.T) { + testShadowTLS(t, 3, "hello", false, option.ShadowTLSWildcardSNIAuthed) + }) + t.Run("v3-wildcard-sni-all", func(t *testing.T) { + testShadowTLS(t, 3, "hello", false, option.ShadowTLSWildcardSNIAll) + }) + t.Run("v3-wildcard-sni-authed-utls", func(t *testing.T) { + testShadowTLS(t, 3, "hello", true, option.ShadowTLSWildcardSNIAll) + }) + t.Run("v3-wildcard-sni-all-utls", func(t *testing.T) { + testShadowTLS(t, 3, "hello", true, option.ShadowTLSWildcardSNIAll) + }) +} + +func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool, wildcardSNI option.WildcardSNI) { + method := shadowaead_2022.List[0] + ssPassword := mkBase64(t, 16) + var clientServerName string + if wildcardSNI != option.ShadowTLSWildcardSNIOff { + clientServerName = "cloudflare.com" + } else { + clientServerName = "google.com" + } + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowTLS, + Tag: "in", + Options: &option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + Detour: "detour", + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + Version: version, + Password: password, + Users: []option.ShadowTLSUser{{Password: password}}, + WildcardSNI: wildcardSNI, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour", + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: otherPort, + }, + Method: method, + Password: ssPassword, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksOutboundOptions{ + Method: method, + Password: ssPassword, + DialerOptions: option.DialerOptions{ + Detour: "detour", + }, + }, + }, + { + Type: C.TypeShadowTLS, + Tag: "detour", + Options: &option.ShadowTLSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: clientServerName, + UTLS: &option.OutboundUTLSOptions{ + Enabled: utlsEanbled, + }, + }, + }, + Version: version, + Password: password, + }, + }, + { + Type: C.TypeDirect, + Tag: "direct", + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"detour"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "direct", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestShadowTLSFallback(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeShadowTLS, + Options: &option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "bing.com", + ServerPort: 443, + }, + }, + Version: 3, + Users: []option.ShadowTLSUser{ + {Password: "hello"}, + }, + }, + }, + }, + }) + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) + }, + }, + } + response, err := client.Get("https://bing.com") + require.NoError(t, err) + require.Equal(t, response.StatusCode, 200) + response.Body.Close() + client.CloseIdleConnections() +} + +func TestShadowTLSFallbackWildcardAll(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeShadowTLS, + Options: &option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Version: 3, + Users: []option.ShadowTLSUser{ + {Password: "hello"}, + }, + WildcardSNI: option.ShadowTLSWildcardSNIAll, + }, + }, + }, + }) + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) + }, + }, + } + response, err := client.Get("https://www.bing.com") + require.NoError(t, err) + require.Equal(t, response.StatusCode, 200) + response.Body.Close() + client.CloseIdleConnections() +} + +func TestShadowTLSFallbackWildcardAuthedFail(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeShadowTLS, + Options: &option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "bing.com", + ServerPort: 443, + }, + }, + Version: 3, + Users: []option.ShadowTLSUser{ + {Password: "hello"}, + }, + WildcardSNI: option.ShadowTLSWildcardSNIAuthed, + }, + }, + }, + }) + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) + }, + }, + } + _, err := client.Get("https://baidu.com") + expected := &tls.CertificateVerificationError{} + require.ErrorAs(t, err, &expected) + client.CloseIdleConnections() +} + +func TestShadowTLSFallbackWildcardOffFail(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeShadowTLS, + Options: &option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "bing.com", + ServerPort: 443, + }, + }, + Version: 3, + Users: []option.ShadowTLSUser{ + {Password: "hello"}, + }, + WildcardSNI: option.ShadowTLSWildcardSNIOff, + }, + }, + }, + }) + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) + }, + }, + } + _, err := client.Get("https://baidu.com") + expected := &tls.CertificateVerificationError{} + require.ErrorAs(t, err, &expected) + client.CloseIdleConnections() +} + +func TestShadowTLSInbound(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startDockerContainer(t, DockerOptions{ + Image: ImageShadowTLS, + Ports: []uint16{serverPort, otherPort}, + EntryPoint: "shadow-tls", + Cmd: []string{"--v3", "--threads", "1", "client", "--listen", "0.0.0.0:" + F.ToString(otherPort), "--server", "127.0.0.1:" + F.ToString(serverPort), "--sni", "google.com", "--password", password}, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowTLS, + Options: &option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + Detour: "detour", + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + Version: 3, + Users: []option.ShadowTLSUser{ + {Password: password}, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour", + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: otherPort, + }, + Method: method, + Password: password, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "out", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestShadowTLSOutbound(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startDockerContainer(t, DockerOptions{ + Image: ImageShadowTLS, + Ports: []uint16{serverPort, otherPort}, + EntryPoint: "shadow-tls", + Cmd: []string{"--v3", "--threads", "1", "server", "--listen", "0.0.0.0:" + F.ToString(serverPort), "--server", "127.0.0.1:" + F.ToString(otherPort), "--tls", "google.com:443", "--password", "hello"}, + Env: []string{"RUST_LOG=trace"}, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour", + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: otherPort, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksOutboundOptions{ + Method: method, + Password: password, + DialerOptions: option.DialerOptions{ + Detour: "detour", + }, + }, + }, + { + Type: C.TypeShadowTLS, + Tag: "detour", + Options: &option.ShadowTLSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + }, + }, + Version: 3, + Password: "hello", + }, + }, + { + Type: C.TypeDirect, + Tag: "direct", + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"detour"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "direct", + }, + }, + }, + }, + }, + }, + }) + testTCP(t, clientPort, testPort) +} diff --git a/test/socks_test.go b/test/socks_test.go new file mode 100644 index 00000000..d33e349c --- /dev/null +++ b/test/socks_test.go @@ -0,0 +1,133 @@ +package main + +import ( + "context" + "net" + "net/netip" + "testing" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" + + "github.com/stretchr/testify/require" +) + +func TestSOCKSUDPTimeout(t *testing.T) { + const testTimeout = 2 * time.Second + udpTimeout := option.UDPTimeoutCompat(testTimeout) + + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeSOCKS, + Tag: "socks-in", + Options: &option.SocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + UDPTimeout: udpTimeout, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + }, + }) + + testUDPSessionIdleTimeout(t, clientPort, testPort, testTimeout) +} + +func TestMixedUDPTimeout(t *testing.T) { + const testTimeout = 2 * time.Second + udpTimeout := option.UDPTimeoutCompat(testTimeout) + + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + UDPTimeout: udpTimeout, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + }, + }) + + testUDPSessionIdleTimeout(t, clientPort, testPort, testTimeout) +} + +func testUDPSessionIdleTimeout(t *testing.T, proxyPort uint16, echoPort uint16, expectedTimeout time.Duration) { + echoServer, err := listenPacket("udp", ":"+F.ToString(echoPort)) + require.NoError(t, err) + defer echoServer.Close() + + go func() { + buffer := make([]byte, 1024) + for { + n, address, err := echoServer.ReadFrom(buffer) + if err != nil { + return + } + _, _ = echoServer.WriteTo(buffer[:n], address) + } + }() + + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "") + + packetConn, err := dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + defer packetConn.Close() + + remoteAddress := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(echoPort)} + + _, err = packetConn.WriteTo([]byte("hello"), remoteAddress) + require.NoError(t, err) + + buffer := make([]byte, 1024) + packetConn.SetReadDeadline(time.Now().Add(5 * time.Second)) + n, _, err := packetConn.ReadFrom(buffer) + require.NoError(t, err, "failed to receive echo response") + require.Equal(t, "hello", string(buffer[:n])) + t.Log("UDP echo successful, session established") + + packetConn.SetReadDeadline(time.Time{}) + + waitTime := expectedTimeout + time.Second + t.Logf("Waiting %v for UDP session to timeout...", waitTime) + time.Sleep(waitTime) + + _, err = packetConn.WriteTo([]byte("after-timeout"), remoteAddress) + if err != nil { + t.Logf("Write after timeout correctly failed: %v", err) + return + } + + packetConn.SetReadDeadline(time.Now().Add(3 * time.Second)) + n, _, err = packetConn.ReadFrom(buffer) + if err != nil { + t.Logf("Read after timeout correctly failed: %v", err) + return + } + + t.Fatalf("UDP session should have timed out after %v, but received response: %s", + expectedTimeout, string(buffer[:n])) +} diff --git a/test/ss_plugin_test.go b/test/ss_plugin_test.go new file mode 100644 index 00000000..f5d18639 --- /dev/null +++ b/test/ss_plugin_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestShadowsocksObfs(t *testing.T) { + for _, mode := range []string{ + "http", "tls", + } { + t.Run("obfs-local "+mode, func(t *testing.T) { + testShadowsocksPlugin(t, "obfs-local", "obfs="+mode, "--plugin obfs-server --plugin-opts obfs="+mode) + }) + } +} + +// Since I can't test this on m1 mac (rosetta error: bss_size overflow), I don't care about it +func _TestShadowsocksV2RayPlugin(t *testing.T) { + testShadowsocksPlugin(t, "v2ray-plugin", "", "--plugin v2ray-plugin --plugin-opts=server") +} + +func testShadowsocksPlugin(t *testing.T, name string, opts string, args string) { + startDockerContainer(t, DockerOptions{ + Image: ImageShadowsocksLegacy, + Ports: []uint16{serverPort, testPort}, + Env: []string{ + "SS_MODULE=ss-server", + "SS_CONFIG=-s 0.0.0.0 -u -p 10000 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL " + args, + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: "chacha20-ietf-poly1305", + Password: "FzcLbKs2dY9mhL", + Plugin: name, + PluginOptions: opts, + }, + }, + }, + }) + testSuitSimple(t, clientPort, testPort) +} diff --git a/test/tfo_test.go b/test/tfo_test.go new file mode 100644 index 00000000..d74dbfb6 --- /dev/null +++ b/test/tfo_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestTCPSlowOpen(t *testing.T) { + method := shadowaead.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Options: &option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + TCPFastOpen: true, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + Options: &option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + DialerOptions: option.DialerOptions{ + TCPFastOpen: true, + }, + Method: method, + Password: password, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/tls_test.go b/test/tls_test.go new file mode 100644 index 00000000..5aaf37c3 --- /dev/null +++ b/test/tls_test.go @@ -0,0 +1,99 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestUTLS(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: "password", + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + Fingerprint: "chrome", + }, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/trojan_test.go b/test/trojan_test.go new file mode 100644 index 00000000..cceed407 --- /dev/null +++ b/test/trojan_test.go @@ -0,0 +1,209 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +func TestTrojanOutbound(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startDockerContainer(t, DockerOptions{ + Image: ImageTrojan, + Ports: []uint16{serverPort, testPort}, + Bind: map[string]string{ + "trojan.json": "/config/config.json", + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeTrojan, + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestTrojanSelf(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: "password", + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestTrojanPlainSelf(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: "password", + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "trojan-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/tuic_test.go b/test/tuic_test.go new file mode 100644 index 00000000..d5f13bec --- /dev/null +++ b/test/tuic_test.go @@ -0,0 +1,198 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" +) + +func TestTUICSelf(t *testing.T) { + t.Run("self", func(t *testing.T) { + testTUICSelf(t, false, false) + }) + t.Run("self-udp-stream", func(t *testing.T) { + testTUICSelf(t, true, false) + }) + t.Run("self-early", func(t *testing.T) { + testTUICSelf(t, false, true) + }) +} + +func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + var udpRelayMode string + if udpStream { + udpRelayMode = "quic" + } + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTUIC, + Options: &option.TUICInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TUICUser{{ + UUID: uuid.Nil.String(), + }}, + ZeroRTTHandshake: zeroRTTHandshake, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTUIC, + Tag: "tuic-out", + Options: &option.TUICOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: uuid.Nil.String(), + UDPRelayMode: udpRelayMode, + ZeroRTTHandshake: zeroRTTHandshake, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "tuic-out", + }, + }, + }, + }, + }, + }, + }) + testSuitLargeUDP(t, clientPort, testPort) +} + +func TestTUICInbound(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeTUIC, + Options: &option.TUICInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TUICUser{{ + UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", + Password: "tuic", + }}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageTUICClient, + Ports: []uint16{serverPort, clientPort}, + Bind: map[string]string{ + "tuic-client.json": "/etc/tuic/config.json", + caPem: "/etc/tuic/ca.pem", + }, + }) + testSuitLargeUDP(t, clientPort, testPort) +} + +func TestTUICOutbound(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startDockerContainer(t, DockerOptions{ + Image: ImageTUICServer, + Ports: []uint16{testPort}, + Bind: map[string]string{ + "tuic-server.json": "/etc/tuic/config.json", + certPem: "/etc/tuic/cert.pem", + keyPem: "/etc/tuic/key.pem", + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeTUIC, + Options: &option.TUICOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", + Password: "tuic", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + }, + }) + testSuitLargeUDP(t, clientPort, testPort) +} diff --git a/test/v2ray_api_test.go b/test/v2ray_api_test.go new file mode 100644 index 00000000..22257032 --- /dev/null +++ b/test/v2ray_api_test.go @@ -0,0 +1,60 @@ +package main + +/* +import ( + "context" + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/v2rayapi" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/stretchr/testify/require" +) + +func TestV2RayAPI(t *testing.T) { + i := startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + Tag: "out", + }, + }, + Experimental: &option.ExperimentalOptions{ + V2RayAPI: &option.V2RayAPIOptions{ + Listen: "127.0.0.1:8080", + Stats: &option.V2RayStatsServiceOptions{ + Enabled: true, + Inbounds: []string{"in"}, + Outbounds: []string{"out"}, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) + statsService := i.Router().V2RayServer().StatsService() + require.NotNil(t, statsService) + response, err := statsService.(v2rayapi.StatsServiceServer).QueryStats(context.Background(), &v2rayapi.QueryStatsRequest{Regexp: true, Patterns: []string{".*"}}) + require.NoError(t, err) + count := response.Stat[0].Value + require.Equal(t, len(response.Stat), 4) + for _, stat := range response.Stat { + require.Equal(t, count, stat.Value) + } +} +*/ diff --git a/test/v2ray_grpc_test.go b/test/v2ray_grpc_test.go new file mode 100644 index 00000000..884cc42e --- /dev/null +++ b/test/v2ray_grpc_test.go @@ -0,0 +1,219 @@ +package main + +import ( + "net/netip" + "os" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" + "github.com/spyzhov/ajson" + "github.com/stretchr/testify/require" +) + +func TestV2RayGRPCInbound(t *testing.T) { + t.Run("origin", func(t *testing.T) { + testV2RayGRPCInbound(t, false) + }) + t.Run("lite", func(t *testing.T) { + testV2RayGRPCInbound(t, true) + }) +} + +func testV2RayGRPCInbound(t *testing.T, forceLite bool) { + userId, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: userId.String(), + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + Transport: &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + ForceLite: forceLite, + }, + }, + }, + }, + }, + }) + content, err := os.ReadFile("config/vmess-grpc-client.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) + outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) + outbound.MustKey("port").SetNumeric(float64(serverPort)) + user := outbound.MustKey("users").MustIndex(0) + user.MustKey("id").SetString(userId.String()) + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + Bind: map[string]string{ + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", + }, + }) + + testSuitSimple(t, clientPort, testPort) +} + +func TestV2RayGRPCOutbound(t *testing.T) { + t.Run("origin", func(t *testing.T) { + testV2RayGRPCOutbound(t, false) + }) + t.Run("lite", func(t *testing.T) { + testV2RayGRPCOutbound(t, true) + }) +} + +func testV2RayGRPCOutbound(t *testing.T, forceLite bool) { + userId, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + + content, err := os.ReadFile("config/vmess-grpc-server.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + inbound := config.MustKey("inbounds").MustIndex(0) + inbound.MustKey("port").SetNumeric(float64(serverPort)) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(userId.String()) + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, + Bind: map[string]string{ + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: userId.String(), + Security: "zero", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + Transport: &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + ForceLite: forceLite, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestV2RayGRPCLite(t *testing.T) { + t.Run("server", func(t *testing.T) { + testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + ForceLite: true, + }, + }, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + }, + }) + }) + t.Run("client", func(t *testing.T) { + testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + }, + }, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + ForceLite: true, + }, + }) + }) + t.Run("self", func(t *testing.T) { + testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + ForceLite: true, + }, + }, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeGRPC, + GRPCOptions: option.V2RayGRPCOptions{ + ServiceName: "TunService", + ForceLite: true, + }, + }) + }) +} diff --git a/test/v2ray_httpupgrade_test.go b/test/v2ray_httpupgrade_test.go new file mode 100644 index 00000000..9a79aa3a --- /dev/null +++ b/test/v2ray_httpupgrade_test.go @@ -0,0 +1,16 @@ +package main + +import ( + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +func TestV2RayHTTPUpgrade(t *testing.T) { + t.Run("self", func(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeHTTPUpgrade, + }) + }) +} diff --git a/test/v2ray_transport_test.go b/test/v2ray_transport_test.go new file mode 100644 index 00000000..b32a5caf --- /dev/null +++ b/test/v2ray_transport_test.go @@ -0,0 +1,384 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" + "github.com/stretchr/testify/require" +) + +func TestV2RayHTTPSelf(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeHTTP, + HTTPOptions: option.V2RayHTTPOptions{ + Method: "POST", + }, + }) +} + +func TestV2RayHTTPPlainSelf(t *testing.T) { + testV2RayTransportNOTLSSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeHTTP, + }) +} + +func testV2RayTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) { + testV2RayTransportSelfWith(t, transport, transport) +} + +func testV2RayTransportSelfWith(t *testing.T, server, client *option.V2RayTransportOptions) { + t.Run("vmess", func(t *testing.T) { + testVMessTransportSelf(t, server, client) + }) + t.Run("trojan", func(t *testing.T) { + testTrojanTransportSelf(t, server, client) + }) +} + +func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, client *option.V2RayTransportOptions) { + user, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: user.String(), + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + Transport: server, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + Security: "zero", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + Transport: client, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, client *option.V2RayTransportOptions) { + user, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + Options: &option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: user.String(), + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + Transport: server, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "vmess-out", + Options: &option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: user.String(), + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + Transport: client, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestVMessQUICSelf(t *testing.T) { + transport := &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeQUIC, + } + user, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: user.String(), + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + Transport: transport, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + Security: "zero", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + Transport: transport, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuitSimple1(t, clientPort, testPort) +} + +func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportOptions) { + user, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: user.String(), + }, + }, + Transport: transport, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + Security: "zero", + Transport: transport, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/v2ray_ws_test.go b/test/v2ray_ws_test.go new file mode 100644 index 00000000..4db4b372 --- /dev/null +++ b/test/v2ray_ws_test.go @@ -0,0 +1,205 @@ +package main + +import ( + "net/netip" + "os" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" + "github.com/spyzhov/ajson" + "github.com/stretchr/testify/require" +) + +func TestV2RayWebsocket(t *testing.T) { + t.Run("self", func(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeWebsocket, + }) + }) + t.Run("self-early-data", func(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeWebsocket, + WebsocketOptions: option.V2RayWebsocketOptions{ + MaxEarlyData: 2048, + }, + }) + }) + t.Run("self-xray-early-data", func(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeWebsocket, + WebsocketOptions: option.V2RayWebsocketOptions{ + MaxEarlyData: 2048, + EarlyDataHeaderName: "Sec-WebSocket-Protocol", + }, + }) + }) + t.Run("inbound", func(t *testing.T) { + testV2RayWebsocketInbound(t, 0, "") + }) + t.Run("inbound-early-data", func(t *testing.T) { + testV2RayWebsocketInbound(t, 2048, "") + }) + t.Run("inbound-xray-early-data", func(t *testing.T) { + testV2RayWebsocketInbound(t, 2048, "Sec-WebSocket-Protocol") + }) + t.Run("outbound", func(t *testing.T) { + testV2RayWebsocketOutbound(t, 0, "") + }) + t.Run("outbound-early-data", func(t *testing.T) { + testV2RayWebsocketOutbound(t, 2048, "") + }) + t.Run("outbound-xray-early-data", func(t *testing.T) { + testV2RayWebsocketOutbound(t, 2048, "Sec-WebSocket-Protocol") + }) +} + +func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeaderName string) { + userId, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: userId.String(), + }, + }, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + Transport: &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeWebsocket, + WebsocketOptions: option.V2RayWebsocketOptions{ + MaxEarlyData: maxEarlyData, + EarlyDataHeaderName: earlyDataHeaderName, + }, + }, + }, + }, + }, + }) + content, err := os.ReadFile("config/vmess-ws-client.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) + outbound := config.MustKey("outbounds").MustIndex(0) + settings := outbound.MustKey("settings").MustKey("vnext").MustIndex(0) + settings.MustKey("port").SetNumeric(float64(serverPort)) + user := settings.MustKey("users").MustIndex(0) + user.MustKey("id").SetString(userId.String()) + wsSettings := outbound.MustKey("streamSettings").MustKey("wsSettings") + wsSettings.MustKey("maxEarlyData").SetNumeric(float64(maxEarlyData)) + wsSettings.MustKey("earlyDataHeaderName").SetString(earlyDataHeaderName) + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + Bind: map[string]string{ + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", + }, + }) + + testSuitSimple(t, clientPort, testPort) +} + +func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHeaderName string) { + userId, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + + content, err := os.ReadFile("config/vmess-ws-server.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + inbound := config.MustKey("inbounds").MustIndex(0) + inbound.MustKey("port").SetNumeric(float64(serverPort)) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(userId.String()) + wsSettings := inbound.MustKey("streamSettings").MustKey("wsSettings") + wsSettings.MustKey("maxEarlyData").SetNumeric(float64(maxEarlyData)) + wsSettings.MustKey("earlyDataHeaderName").SetString(earlyDataHeaderName) + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, + Bind: map[string]string{ + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", + }, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: userId.String(), + Security: "zero", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + Transport: &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeWebsocket, + WebsocketOptions: option.V2RayWebsocketOptions{ + MaxEarlyData: maxEarlyData, + EarlyDataHeaderName: earlyDataHeaderName, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/vmess_test.go b/test/vmess_test.go new file mode 100644 index 00000000..4da76c6e --- /dev/null +++ b/test/vmess_test.go @@ -0,0 +1,337 @@ +package main + +import ( + "net/netip" + "os" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" + + "github.com/gofrs/uuid/v5" + "github.com/spyzhov/ajson" + "github.com/stretchr/testify/require" +) + +func newUUID() uuid.UUID { + user, _ := uuid.DefaultGenerator.NewV4() + return user +} + +func TestVMessAuto(t *testing.T) { + security := "auto" + t.Run("self", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, false) + }) + t.Run("packetaddr", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, true) + }) + t.Run("inbound", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, 0, false) + }) + t.Run("outbound", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 0) + }) +} + +func TestVMess(t *testing.T) { + for _, security := range []string{ + "zero", + } { + t.Run(security, func(t *testing.T) { + testVMess0(t, security) + }) + } + for _, security := range []string{ + "none", + } { + t.Run(security, func(t *testing.T) { + testVMess1(t, security) + }) + } + for _, security := range []string{ + "aes-128-gcm", "chacha20-poly1305", "aes-128-cfb", + } { + t.Run(security, func(t *testing.T) { + testVMess2(t, security) + }) + } +} + +func testVMess0(t *testing.T, security string) { + t.Run("self", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, false) + }) + t.Run("self-legacy", func(t *testing.T) { + testVMessSelf(t, security, 1, false, false, false) + }) + t.Run("packetaddr", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, true) + }) + t.Run("outbound", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 0) + }) + t.Run("outbound-legacy", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 1) + }) +} + +func testVMess1(t *testing.T, security string) { + t.Run("self", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, false) + }) + t.Run("self-legacy", func(t *testing.T) { + testVMessSelf(t, security, 1, false, false, false) + }) + t.Run("packetaddr", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, true) + }) + t.Run("inbound", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, 0, false) + }) + t.Run("outbound", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 0) + }) + t.Run("outbound-legacy", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 1) + }) +} + +func testVMess2(t *testing.T, security string) { + t.Run("self", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, false) + }) + t.Run("self-padding", func(t *testing.T) { + testVMessSelf(t, security, 0, true, false, false) + }) + t.Run("self-authid", func(t *testing.T) { + testVMessSelf(t, security, 0, false, true, false) + }) + t.Run("self-padding-authid", func(t *testing.T) { + testVMessSelf(t, security, 0, true, true, false) + }) + t.Run("self-legacy", func(t *testing.T) { + testVMessSelf(t, security, 1, false, false, false) + }) + t.Run("self-legacy-padding", func(t *testing.T) { + testVMessSelf(t, security, 1, true, false, false) + }) + t.Run("packetaddr", func(t *testing.T) { + testVMessSelf(t, security, 0, false, false, true) + }) + t.Run("inbound", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, 0, false) + }) + t.Run("inbound-authid", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, 0, true) + }) + t.Run("inbound-legacy", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, 64, false) + }) + t.Run("outbound", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 0) + }) + t.Run("outbound-padding", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, true, false, 0) + }) + t.Run("outbound-authid", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, true, 0) + }) + t.Run("outbound-padding-authid", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, true, true, 0) + }) + t.Run("outbound-legacy", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, false, false, 1) + }) + t.Run("outbound-legacy-padding", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, true, false, 1) + }) +} + +func testVMessInboundWithV2Ray(t *testing.T, security string, alterId int, authenticatedLength bool) { + userId := newUUID() + content, err := os.ReadFile("config/vmess-client.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) + outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) + outbound.MustKey("port").SetNumeric(float64(serverPort)) + user := outbound.MustKey("users").MustIndex(0) + user.MustKey("id").SetString(userId.String()) + user.MustKey("alterId").SetNumeric(float64(alterId)) + user.MustKey("security").SetString(security) + var experiments string + if authenticatedLength { + experiments += "AuthenticatedLength" + } + user.MustKey("experiments").SetString(experiments) + + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, + }) + + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: userId.String(), + AlterId: alterId, + }, + }, + }, + }, + }, + }) + + testSuitSimple(t, clientPort, testPort) +} + +func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding bool, authenticatedLength bool, alterId int) { + user := newUUID() + content, err := os.ReadFile("config/vmess-server.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + inbound := config.MustKey("inbounds").MustIndex(0) + inbound.MustKey("port").SetNumeric(float64(serverPort)) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String()) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("alterId").SetNumeric(float64(alterId)) + + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Cmd: []string{"run"}, + Stdin: content, + Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, + }) + + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeVMess, + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Security: security, + UUID: user.String(), + GlobalPadding: globalPadding, + AuthenticatedLength: authenticatedLength, + AlterId: alterId, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testVMessSelf(t *testing.T, security string, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) { + user := newUUID() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + Options: &option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + Options: &option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: user.String(), + AlterId: alterId, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "vmess-out", + Options: &option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Security: security, + UUID: user.String(), + AlterId: alterId, + GlobalPadding: globalPadding, + AuthenticatedLength: authenticatedLength, + PacketEncoding: "packetaddr", + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/wrapper_test.go b/test/wrapper_test.go new file mode 100644 index 00000000..9eb9ed78 --- /dev/null +++ b/test/wrapper_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + + "github.com/stretchr/testify/require" +) + +func TestOptionsWrapper(t *testing.T) { + inbound := option.Inbound{ + Type: C.TypeHTTP, + Options: &option.HTTPMixedInboundOptions{ + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + }, + }, + }, + } + tlsOptionsWrapper, loaded := inbound.Options.(option.InboundTLSOptionsWrapper) + require.True(t, loaded, "find inbound tls options") + tlsOptions := tlsOptionsWrapper.TakeInboundTLSOptions() + require.NotNil(t, tlsOptions, "find inbound tls options") + tlsOptions.Enabled = false + tlsOptionsWrapper.ReplaceInboundTLSOptions(tlsOptions) + require.False(t, inbound.Options.(*option.HTTPMixedInboundOptions).TLS.Enabled, "replace tls enabled") +} diff --git a/transport/simple-obfs/README.md b/transport/simple-obfs/README.md new file mode 100644 index 00000000..d438d0f9 --- /dev/null +++ b/transport/simple-obfs/README.md @@ -0,0 +1,4 @@ +# simple-obfs + +mod from https://github.com/Dreamacro/clash/transport/simple-obfs +version: 1.11.8 \ No newline at end of file diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go new file mode 100644 index 00000000..df38768e --- /dev/null +++ b/transport/simple-obfs/http.go @@ -0,0 +1,98 @@ +package obfs + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "math/rand" + "net" + "net/http" + + B "github.com/sagernet/sing/common/buf" +) + +// HTTPObfs is shadowsocks http simple-obfs implementation +type HTTPObfs struct { + net.Conn + host string + port string + buf []byte + offset int + firstRequest bool + firstResponse bool +} + +func (ho *HTTPObfs) Read(b []byte) (int, error) { + if ho.buf != nil { + n := copy(b, ho.buf[ho.offset:]) + ho.offset += n + if ho.offset == len(ho.buf) { + B.Put(ho.buf) + ho.buf = nil + } + return n, nil + } + + if ho.firstResponse { + buf := B.Get(B.BufferSize) + n, err := ho.Conn.Read(buf) + if err != nil { + B.Put(buf) + return 0, err + } + idx := bytes.Index(buf[:n], []byte("\r\n\r\n")) + if idx == -1 { + B.Put(buf) + return 0, io.EOF + } + ho.firstResponse = false + length := n - (idx + 4) + n = copy(b, buf[idx+4:n]) + if length > n { + ho.buf = buf[:idx+4+length] + ho.offset = idx + 4 + n + } else { + B.Put(buf) + } + return n, nil + } + return ho.Conn.Read(b) +} + +func (ho *HTTPObfs) Write(b []byte) (int, error) { + if ho.firstRequest { + randBytes := make([]byte, 16) + rand.Read(randBytes) + req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Connection", "Upgrade") + req.Host = ho.host + if ho.port != "80" { + req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port) + } + req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) + req.ContentLength = int64(len(b)) + err := req.Write(ho.Conn) + ho.firstRequest = false + return len(b), err + } + + return ho.Conn.Write(b) +} + +func (ho *HTTPObfs) Upstream() any { + return ho.Conn +} + +// NewHTTPObfs return a HTTPObfs +func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn { + return &HTTPObfs{ + Conn: conn, + firstRequest: true, + firstResponse: true, + host: host, + port: port, + } +} diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go new file mode 100644 index 00000000..96564815 --- /dev/null +++ b/transport/simple-obfs/tls.go @@ -0,0 +1,205 @@ +package obfs + +import ( + "bytes" + "encoding/binary" + "io" + "math/rand" + "net" + "time" + + B "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/random" +) + +func init() { + random.InitializeSeed() +} + +const ( + chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 +) + +// TLSObfs is shadowsocks tls simple-obfs implementation +type TLSObfs struct { + net.Conn + server string + remain int + firstRequest bool + firstResponse bool +} + +func (to *TLSObfs) read(b []byte, discardN int) (int, error) { + buf := B.Get(discardN) + _, err := io.ReadFull(to.Conn, buf) + B.Put(buf) + if err != nil { + return 0, err + } + + sizeBuf := make([]byte, 2) + _, err = io.ReadFull(to.Conn, sizeBuf) + if err != nil { + return 0, nil + } + + length := int(binary.BigEndian.Uint16(sizeBuf)) + if length > len(b) { + n, err := to.Conn.Read(b) + if err != nil { + return n, err + } + to.remain = length - n + return n, nil + } + + return io.ReadFull(to.Conn, b[:length]) +} + +func (to *TLSObfs) Read(b []byte) (int, error) { + if to.remain > 0 { + length := to.remain + if length > len(b) { + length = len(b) + } + + n, err := io.ReadFull(to.Conn, b[:length]) + to.remain -= n + return n, err + } + + if to.firstResponse { + // type + ver + lensize + 91 = 96 + // type + ver + lensize + 1 = 6 + // type + ver = 3 + to.firstResponse = false + return to.read(b, 105) + } + + // type + ver = 3 + return to.read(b, 3) +} + +func (to *TLSObfs) Write(b []byte) (int, error) { + length := len(b) + for i := 0; i < length; i += chunkSize { + end := i + chunkSize + if end > length { + end = length + } + + n, err := to.write(b[i:end]) + if err != nil { + return n, err + } + } + return length, nil +} + +func (to *TLSObfs) write(b []byte) (int, error) { + if to.firstRequest { + helloMsg := makeClientHelloMsg(b, to.server) + _, err := to.Conn.Write(helloMsg) + to.firstRequest = false + return len(b), err + } + + buf := B.NewSize(5 + len(b)) + defer buf.Release() + buf.Write([]byte{0x17, 0x03, 0x03}) + binary.Write(buf, binary.BigEndian, uint16(len(b))) + buf.Write(b) + _, err := to.Conn.Write(buf.Bytes()) + return len(b), err +} + +func (to *TLSObfs) Upstream() any { + return to.Conn +} + +// NewTLSObfs return a SimpleObfs +func NewTLSObfs(conn net.Conn, server string) net.Conn { + return &TLSObfs{ + Conn: conn, + server: server, + firstRequest: true, + firstResponse: true, + } +} + +func makeClientHelloMsg(data []byte, server string) []byte { + random := make([]byte, 28) + sessionID := make([]byte, 32) + rand.Read(random) + rand.Read(sessionID) + + buf := &bytes.Buffer{} + + // handshake, TLS 1.0 version, length + buf.WriteByte(22) + buf.Write([]byte{0x03, 0x01}) + length := uint16(212 + len(data) + len(server)) + buf.WriteByte(byte(length >> 8)) + buf.WriteByte(byte(length & 0xff)) + + // clientHello, length, TLS 1.2 version + buf.WriteByte(1) + buf.WriteByte(0) + binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server))) + buf.Write([]byte{0x03, 0x03}) + + // random with timestamp, sid len, sid + binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + buf.Write(random) + buf.WriteByte(32) + buf.Write(sessionID) + + // cipher suites + buf.Write([]byte{0x00, 0x38}) + buf.Write([]byte{ + 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, + 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, + 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, + }) + + // compression + buf.Write([]byte{0x01, 0x00}) + + // extension length + binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server))) + + // session ticket + buf.Write([]byte{0x00, 0x23}) + binary.Write(buf, binary.BigEndian, uint16(len(data))) + buf.Write(data) + + // server name + buf.Write([]byte{0x00, 0x00}) + binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) + binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) + buf.WriteByte(0) + binary.Write(buf, binary.BigEndian, uint16(len(server))) + buf.Write([]byte(server)) + + // ec_point + buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) + + // groups + buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) + + // signature + buf.Write([]byte{ + 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, + 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, + 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, + }) + + // encrypt then mac + buf.Write([]byte{0x00, 0x16, 0x00, 0x00}) + + // extended master secret + buf.Write([]byte{0x00, 0x17, 0x00, 0x00}) + + return buf.Bytes() +} diff --git a/transport/sip003/args.go b/transport/sip003/args.go new file mode 100644 index 00000000..b9fae3da --- /dev/null +++ b/transport/sip003/args.go @@ -0,0 +1,119 @@ +package sip003 + +import ( + "bytes" + "fmt" +) + +// mod from https://github.com/shadowsocks/v2ray-plugin/blob/master/args.go + +// Args maps a string key to a list of values. It is similar to url.Values. +type Args map[string][]string + +// Get the first value associated with the given key. If there are any values +// associated with the key, the value return has the value and ok is set to +// true. If there are no values for the given key, value is "" and ok is false. +// If you need access to multiple values, use the map directly. +func (args Args) Get(key string) (value string, ok bool) { + if args == nil { + return "", false + } + vals, ok := args[key] + if !ok || len(vals) == 0 { + return "", false + } + return vals[0], true +} + +// Add Append value to the list of values for key. +func (args Args) Add(key, value string) { + args[key] = append(args[key], value) +} + +// Return the index of the next unescaped byte in s that is in the term set, or +// else the length of the string if no terminators appear. Additionally return +// the unescaped string up to the returned index. +func indexUnescaped(s string, term []byte) (int, string, error) { + var i int + unesc := make([]byte, 0) + for i = 0; i < len(s); i++ { + b := s[i] + // A terminator byte? + if bytes.IndexByte(term, b) != -1 { + break + } + if b == '\\' { + i++ + if i >= len(s) { + return 0, "", fmt.Errorf("nothing following final escape in %q", s) + } + b = s[i] + } + unesc = append(unesc, b) + } + return i, string(unesc), nil +} + +// ParsePluginOptions Parse a name–value mapping as from SS_PLUGIN_OPTIONS. +// +// " is a k=v string value with options that are to be passed to the +// transport. semicolons, equal signs and backslashes must be escaped +// with a backslash." +// Example: secret=nou;cache=/tmp/cache;secret=yes +func ParsePluginOptions(s string) (opts Args, err error) { + opts = make(Args) + if len(s) == 0 { + return + } + i := 0 + for { + var key, value string + var offset, begin int + + if i >= len(s) { + break + } + begin = i + // Read the key. + offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'}) + if err != nil { + return + } + if len(key) == 0 { + err = fmt.Errorf("empty key in %q", s[begin:i]) + return + } + i += offset + // End of string or no equals sign? + if i >= len(s) || s[i] != '=' { + opts.Add(key, "1") + // Skip the semicolon. + i++ + continue + } + // Skip the equals sign. + i++ + // Read the value. + offset, value, err = indexUnescaped(s[i:], []byte{';'}) + if err != nil { + return + } + i += offset + opts.Add(key, value) + // Skip the semicolon. + i++ + } + return opts, nil +} + +// Escape backslashes and all the bytes that are in set. +func backslashEscape(s string, set []byte) string { + var buf bytes.Buffer + for _, b := range []byte(s) { + if b == '\\' || bytes.IndexByte(set, b) != -1 { + buf.WriteByte('\\') + } + buf.WriteByte(b) + } + return buf.String() +} diff --git a/transport/sip003/obfs.go b/transport/sip003/obfs.go new file mode 100644 index 00000000..129e7756 --- /dev/null +++ b/transport/sip003/obfs.go @@ -0,0 +1,62 @@ +package sip003 + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/transport/simple-obfs" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ Plugin = (*ObfsLocal)(nil) + +func init() { + RegisterPlugin("obfs-local", newObfsLocal) +} + +func newObfsLocal(ctx context.Context, pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) { + plugin := &ObfsLocal{ + dialer: dialer, + serverAddr: serverAddr, + } + mode := "http" + if obfsMode, loaded := pluginOpts.Get("obfs"); loaded { + mode = obfsMode + } + if obfsHost, loaded := pluginOpts.Get("obfs-host"); loaded { + plugin.host = obfsHost + } + switch mode { + case "http": + case "tls": + plugin.tls = true + default: + return nil, E.New("unknown obfs mode ", mode) + } + plugin.port = F.ToString(serverAddr.Port) + return plugin, nil +} + +type ObfsLocal struct { + dialer N.Dialer + serverAddr M.Socksaddr + tls bool + host string + port string +} + +func (o *ObfsLocal) DialContext(ctx context.Context) (net.Conn, error) { + conn, err := o.dialer.DialContext(ctx, N.NetworkTCP, o.serverAddr) + if err != nil { + return nil, err + } + if !o.tls { + return obfs.NewHTTPObfs(conn, o.host, o.port), nil + } else { + return obfs.NewTLSObfs(conn, o.host), nil + } +} diff --git a/transport/sip003/plugin.go b/transport/sip003/plugin.go new file mode 100644 index 00000000..546a3f36 --- /dev/null +++ b/transport/sip003/plugin.go @@ -0,0 +1,38 @@ +package sip003 + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type PluginConstructor func(ctx context.Context, pluginArgs Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) + +type Plugin interface { + DialContext(ctx context.Context) (net.Conn, error) +} + +var plugins map[string]PluginConstructor + +func RegisterPlugin(name string, constructor PluginConstructor) { + if plugins == nil { + plugins = make(map[string]PluginConstructor) + } + plugins[name] = constructor +} + +func CreatePlugin(ctx context.Context, name string, pluginArgs string, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) { + pluginOptions, err := ParsePluginOptions(pluginArgs) + if err != nil { + return nil, E.Cause(err, "parse plugin_opts") + } + constructor, loaded := plugins[name] + if !loaded { + return nil, E.New("plugin not found: ", name) + } + return constructor(ctx, pluginOptions, router, dialer, serverAddr) +} diff --git a/transport/sip003/v2ray.go b/transport/sip003/v2ray.go new file mode 100644 index 00000000..f35e2654 --- /dev/null +++ b/transport/sip003/v2ray.go @@ -0,0 +1,119 @@ +package sip003 + +import ( + "context" + "net" + "strconv" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2ray" + "github.com/sagernet/sing-vmess" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func init() { + RegisterPlugin("v2ray-plugin", newV2RayPlugin) +} + +func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) { + var tlsOptions option.OutboundTLSOptions + if _, loaded := pluginOpts.Get("tls"); loaded { + tlsOptions.Enabled = true + } + if certPath, certLoaded := pluginOpts.Get("cert"); certLoaded { + tlsOptions.CertificatePath = certPath + } + if certRaw, certLoaded := pluginOpts.Get("certRaw"); certLoaded { + certHead := "-----BEGIN CERTIFICATE-----" + certTail := "-----END CERTIFICATE-----" + fixedCert := certHead + "\n" + certRaw + "\n" + certTail + tlsOptions.Certificate = []string{fixedCert} + } + + mode := "websocket" + if modeOpt, loaded := pluginOpts.Get("mode"); loaded { + mode = modeOpt + } + + host := "cloudfront.com" + path := "/" + + if hostOpt, loaded := pluginOpts.Get("host"); loaded { + host = hostOpt + tlsOptions.ServerName = hostOpt + } + if pathOpt, loaded := pluginOpts.Get("path"); loaded { + path = pathOpt + } + + var tlsClient tls.Config + var err error + if tlsOptions.Enabled { + tlsClient, err = tls.NewClient(ctx, logger.NOP(), serverAddr.AddrString(), tlsOptions) + if err != nil { + return nil, err + } + } + + var mux int + var transportOptions option.V2RayTransportOptions + switch mode { + case "websocket": + transportOptions = option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeWebsocket, + WebsocketOptions: option.V2RayWebsocketOptions{ + Headers: map[string]badoption.Listable[string]{ + "Host": []string{host}, + }, + Path: path, + }, + } + if muxOpt, loaded := pluginOpts.Get("mux"); loaded { + muxVal, err := strconv.Atoi(muxOpt) + if err != nil { + return nil, E.Cause(err, "parse mux value") + } + mux = muxVal + } else { + mux = 1 + } + case "quic": + transportOptions = option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeQUIC, + } + default: + return nil, E.New("v2ray-plugin: unknown mode: " + mode) + } + + transport, err := v2ray.NewClientTransport(context.Background(), dialer, serverAddr, transportOptions, tlsClient) + if err != nil { + return nil, err + } + + if mux > 0 { + return &v2rayMuxWrapper{transport}, nil + } + + return transport, nil +} + +var _ Plugin = (*v2rayMuxWrapper)(nil) + +type v2rayMuxWrapper struct { + adapter.V2RayClientTransport +} + +func (w *v2rayMuxWrapper) DialContext(ctx context.Context) (net.Conn, error) { + conn, err := w.V2RayClientTransport.DialContext(ctx) + if err != nil { + return nil, err + } + return vmess.NewMuxConnWrapper(conn, vmess.MuxDestination), nil +} diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go new file mode 100644 index 00000000..72d5a776 --- /dev/null +++ b/transport/trojan/mux.go @@ -0,0 +1,84 @@ +package trojan + +import ( + std_bufio "bufio" + "context" + "net" + "os" + + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/task" + "github.com/sagernet/smux" +) + +func HandleMuxConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler, logger logger.ContextLogger, onClose N.CloseHandlerFunc) error { + session, err := smux.Server(conn, smuxConfig()) + if err != nil { + return err + } + var group task.Group + group.Append0(func(_ context.Context) error { + var stream net.Conn + for { + stream, err = session.AcceptStream() + if err != nil { + return err + } + go newMuxConnection(ctx, stream, source, handler, logger) + } + }) + group.Cleanup(func() { + session.Close() + if onClose != nil { + onClose(os.ErrClosed) + } + }) + return group.Run(ctx) +} + +func newMuxConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler, logger logger.ContextLogger) { + err := newMuxConnection0(ctx, conn, source, handler) + if err != nil { + logger.ErrorContext(ctx, E.Cause(err, "process trojan-go multiplex connection")) + } +} + +func newMuxConnection0(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler) error { + reader := std_bufio.NewReader(conn) + command, err := reader.ReadByte() + if err != nil { + return E.Cause(err, "read command") + } + destination, err := M.SocksaddrSerializer.ReadAddrPort(reader) + if err != nil { + return E.Cause(err, "read destination") + } + if reader.Buffered() > 0 { + buffer := buf.NewSize(reader.Buffered()) + _, err = buffer.ReadFullFrom(reader, buffer.Len()) + if err != nil { + return err + } + conn = bufio.NewCachedConn(conn, buffer) + } + switch command { + case CommandTCP: + handler.NewConnectionEx(ctx, conn, source, destination, nil) + case CommandUDP: + handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, source, destination, nil) + default: + return E.New("unknown command ", command) + } + return nil +} + +func smuxConfig() *smux.Config { + config := smux.DefaultConfig() + config.KeepAliveDisabled = true + return config +} diff --git a/transport/trojan/protocol.go b/transport/trojan/protocol.go new file mode 100644 index 00000000..0456b6b9 --- /dev/null +++ b/transport/trojan/protocol.go @@ -0,0 +1,321 @@ +package trojan + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "net" + "os" + "sync" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" +) + +const ( + KeyLength = 56 + CommandTCP = 1 + CommandUDP = 3 + CommandMux = 0x7f +) + +var CRLF = []byte{'\r', '\n'} + +var _ N.EarlyWriter = (*ClientConn)(nil) + +type ClientConn struct { + N.ExtendedConn + key [KeyLength]byte + destination M.Socksaddr + headerWritten bool +} + +func NewClientConn(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr) *ClientConn { + return &ClientConn{ + ExtendedConn: bufio.NewExtendedConn(conn), + key: key, + destination: destination, + } +} + +func (c *ClientConn) NeedHandshakeForWrite() bool { + return !c.headerWritten +} + +func (c *ClientConn) Write(p []byte) (n int, err error) { + if c.headerWritten { + return c.ExtendedConn.Write(p) + } + err = ClientHandshake(c.ExtendedConn, c.key, c.destination, p) + if err != nil { + return + } + n = len(p) + c.headerWritten = true + return +} + +func (c *ClientConn) WriteBuffer(buffer *buf.Buffer) error { + if c.headerWritten { + return c.ExtendedConn.WriteBuffer(buffer) + } + err := ClientHandshakeBuffer(c.ExtendedConn, c.key, c.destination, buffer) + if err != nil { + return err + } + c.headerWritten = true + return nil +} + +func (c *ClientConn) FrontHeadroom() int { + if !c.headerWritten { + return KeyLength + 5 + M.MaxSocksaddrLength + } + return 0 +} + +func (c *ClientConn) Upstream() any { + return c.ExtendedConn +} + +func (c *ClientConn) ReaderReplaceable() bool { + return c.headerWritten +} + +func (c *ClientConn) WriterReplaceable() bool { + return c.headerWritten +} + +type ClientPacketConn struct { + net.Conn + access sync.Mutex + key [KeyLength]byte + headerWritten bool + readWaitOptions N.ReadWaitOptions +} + +func NewClientPacketConn(conn net.Conn, key [KeyLength]byte) *ClientPacketConn { + return &ClientPacketConn{ + Conn: conn, + key: key, + } +} + +func (c *ClientPacketConn) NeedHandshake() bool { + return !c.headerWritten +} + +func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + return ReadPacket(c.Conn, buffer) +} + +func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + if !c.headerWritten { + c.access.Lock() + if c.headerWritten { + c.access.Unlock() + } else { + err := ClientHandshakePacket(c.Conn, c.key, destination, buffer) + c.headerWritten = true + c.access.Unlock() + return err + } + } + return WritePacket(c.Conn, buffer, destination) +} + +func (c *ClientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + buffer := buf.With(p) + destination, err := c.ReadPacket(buffer) + if err != nil { + return + } + n = buffer.Len() + if destination.IsDomain() { + addr = destination + } else { + addr = destination.UDPAddr() + } + return +} + +func (c *ClientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return bufio.WritePacket(c, p, addr) +} + +func (c *ClientPacketConn) Read(p []byte) (n int, err error) { + n, _, err = c.ReadFrom(p) + return +} + +func (c *ClientPacketConn) Write(p []byte) (n int, err error) { + return 0, os.ErrInvalid +} + +func (c *ClientPacketConn) FrontHeadroom() int { + if !c.headerWritten { + return KeyLength + 2*M.MaxSocksaddrLength + 9 + } + return M.MaxSocksaddrLength + 4 +} + +func (c *ClientPacketConn) Upstream() any { + return c.Conn +} + +func Key(password string) [KeyLength]byte { + var key [KeyLength]byte + hash := sha256.New224() + common.Must1(hash.Write([]byte(password))) + hex.Encode(key[:], hash.Sum(nil)) + return key +} + +func ClientHandshakeRaw(conn net.Conn, key [KeyLength]byte, command byte, destination M.Socksaddr, payload []byte) error { + _, err := conn.Write(key[:]) + if err != nil { + return err + } + _, err = conn.Write(CRLF) + if err != nil { + return err + } + _, err = conn.Write([]byte{command}) + if err != nil { + return err + } + err = M.SocksaddrSerializer.WriteAddrPort(conn, destination) + if err != nil { + return err + } + _, err = conn.Write(CRLF) + if err != nil { + return err + } + if len(payload) > 0 { + _, err = conn.Write(payload) + if err != nil { + return err + } + } + return nil +} + +func ClientHandshake(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload []byte) error { + headerLen := KeyLength + M.SocksaddrSerializer.AddrPortLen(destination) + 5 + header := buf.NewSize(headerLen + len(payload)) + defer header.Release() + common.Must1(header.Write(key[:])) + common.Must1(header.Write(CRLF)) + common.Must(header.WriteByte(CommandTCP)) + err := M.SocksaddrSerializer.WriteAddrPort(header, destination) + if err != nil { + return err + } + common.Must1(header.Write(CRLF)) + common.Must1(header.Write(payload)) + _, err = conn.Write(header.Bytes()) + if err != nil { + return E.Cause(err, "write request") + } + return nil +} + +func ClientHandshakeBuffer(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload *buf.Buffer) error { + header := buf.With(payload.ExtendHeader(KeyLength + M.SocksaddrSerializer.AddrPortLen(destination) + 5)) + common.Must1(header.Write(key[:])) + common.Must1(header.Write(CRLF)) + common.Must(header.WriteByte(CommandTCP)) + err := M.SocksaddrSerializer.WriteAddrPort(header, destination) + if err != nil { + return err + } + common.Must1(header.Write(CRLF)) + + _, err = conn.Write(payload.Bytes()) + if err != nil { + return E.Cause(err, "write request") + } + return nil +} + +func ClientHandshakePacket(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload *buf.Buffer) error { + headerLen := KeyLength + 2*M.SocksaddrSerializer.AddrPortLen(destination) + 9 + payloadLen := payload.Len() + var header *buf.Buffer + var writeHeader bool + if payload.Start() >= headerLen { + header = buf.With(payload.ExtendHeader(headerLen)) + } else { + header = buf.NewSize(headerLen) + defer header.Release() + writeHeader = true + } + common.Must1(header.Write(key[:])) + common.Must1(header.Write(CRLF)) + common.Must(header.WriteByte(CommandUDP)) + err := M.SocksaddrSerializer.WriteAddrPort(header, destination) + if err != nil { + return err + } + common.Must1(header.Write(CRLF)) + common.Must(M.SocksaddrSerializer.WriteAddrPort(header, destination)) + common.Must(binary.Write(header, binary.BigEndian, uint16(payloadLen))) + common.Must1(header.Write(CRLF)) + + if writeHeader { + _, err := conn.Write(header.Bytes()) + if err != nil { + return E.Cause(err, "write request") + } + } + + _, err = conn.Write(payload.Bytes()) + if err != nil { + return E.Cause(err, "write payload") + } + return nil +} + +func ReadPacket(conn net.Conn, buffer *buf.Buffer) (M.Socksaddr, error) { + destination, err := M.SocksaddrSerializer.ReadAddrPort(conn) + if err != nil { + return M.Socksaddr{}, E.Cause(err, "read destination") + } + + var length uint16 + err = binary.Read(conn, binary.BigEndian, &length) + if err != nil { + return M.Socksaddr{}, E.Cause(err, "read chunk length") + } + + err = rw.SkipN(conn, 2) + if err != nil { + return M.Socksaddr{}, E.Cause(err, "skip crlf") + } + + _, err = buffer.ReadFullFrom(conn, int(length)) + return destination, err +} + +func WritePacket(conn net.Conn, buffer *buf.Buffer, destination M.Socksaddr) error { + defer buffer.Release() + bufferLen := buffer.Len() + header := buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination) + 4)) + err := M.SocksaddrSerializer.WriteAddrPort(header, destination) + if err != nil { + return err + } + common.Must(binary.Write(header, binary.BigEndian, uint16(bufferLen))) + common.Must1(header.Write(CRLF)) + _, err = conn.Write(buffer.Bytes()) + if err != nil { + return E.Cause(err, "write packet") + } + return nil +} diff --git a/transport/trojan/protocol_wait.go b/transport/trojan/protocol_wait.go new file mode 100644 index 00000000..c6b4ec06 --- /dev/null +++ b/transport/trojan/protocol_wait.go @@ -0,0 +1,45 @@ +package trojan + +import ( + "encoding/binary" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" +) + +var _ N.PacketReadWaiter = (*ClientPacketConn)(nil) + +func (c *ClientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *ClientPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + destination, err = M.SocksaddrSerializer.ReadAddrPort(c.Conn) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read destination") + } + + var length uint16 + err = binary.Read(c.Conn, binary.BigEndian, &length) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read chunk length") + } + + err = rw.SkipN(c.Conn, 2) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "skip crlf") + } + + buffer = c.readWaitOptions.NewPacketBuffer() + _, err = buffer.ReadFullFrom(c.Conn, int(length)) + if err != nil { + buffer.Release() + return + } + c.readWaitOptions.PostReturn(buffer) + return +} diff --git a/transport/trojan/service.go b/transport/trojan/service.go new file mode 100644 index 00000000..7f1803bb --- /dev/null +++ b/transport/trojan/service.go @@ -0,0 +1,149 @@ +package trojan + +import ( + "context" + "encoding/binary" + "net" + + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" +) + +type Handler interface { + N.TCPConnectionHandlerEx + N.UDPConnectionHandlerEx +} + +type Service[K comparable] struct { + users map[K][56]byte + keys map[[56]byte]K + handler Handler + fallbackHandler N.TCPConnectionHandlerEx + logger logger.ContextLogger +} + +func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandlerEx, logger logger.ContextLogger) *Service[K] { + return &Service[K]{ + users: make(map[K][56]byte), + keys: make(map[[56]byte]K), + handler: handler, + fallbackHandler: fallbackHandler, + logger: logger, + } +} + +var ErrUserExists = E.New("user already exists") + +func (s *Service[K]) UpdateUsers(userList []K, passwordList []string) error { + users := make(map[K][56]byte) + keys := make(map[[56]byte]K) + for i, user := range userList { + if _, loaded := users[user]; loaded { + return ErrUserExists + } + key := Key(passwordList[i]) + if oldUser, loaded := keys[key]; loaded { + return E.Extend(ErrUserExists, "password used by ", oldUser) + } + users[user] = key + keys[key] = user + } + s.users = users + s.keys = keys + return nil +} + +func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + var key [KeyLength]byte + n, err := conn.Read(key[:]) + if err != nil { + return err + } else if n != KeyLength { + return s.fallback(ctx, conn, source, key[:n], E.New("bad request size"), onClose) + } + + if user, loaded := s.keys[key]; loaded { + ctx = auth.ContextWithUser(ctx, user) + } else { + return s.fallback(ctx, conn, source, key[:], E.New("bad request"), onClose) + } + + err = rw.SkipN(conn, 2) + if err != nil { + return E.Cause(err, "skip crlf") + } + + var command byte + err = binary.Read(conn, binary.BigEndian, &command) + if err != nil { + return E.Cause(err, "read command") + } + + switch command { + case CommandTCP, CommandUDP, CommandMux: + default: + return E.New("unknown command ", command) + } + + // var destination M.Socksaddr + destination, err := M.SocksaddrSerializer.ReadAddrPort(conn) + if err != nil { + return E.Cause(err, "read destination") + } + + err = rw.SkipN(conn, 2) + if err != nil { + return E.Cause(err, "skip crlf") + } + + switch command { + case CommandTCP: + s.handler.NewConnectionEx(ctx, conn, source, destination, onClose) + case CommandUDP: + s.handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, source, destination, onClose) + // case CommandMux: + default: + return HandleMuxConnection(ctx, conn, source, s.handler, s.logger, onClose) + } + return nil +} + +func (s *Service[K]) fallback(ctx context.Context, conn net.Conn, source M.Socksaddr, header []byte, err error, onClose N.CloseHandlerFunc) error { + if s.fallbackHandler == nil { + return E.Extend(err, "fallback disabled") + } + conn = bufio.NewCachedConn(conn, buf.As(header).ToOwned()) + s.fallbackHandler.NewConnectionEx(ctx, conn, source, M.Socksaddr{}, onClose) + return nil +} + +type PacketConn struct { + net.Conn + readWaitOptions N.ReadWaitOptions +} + +func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + return ReadPacket(c.Conn, buffer) +} + +func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + return WritePacket(c.Conn, buffer, destination) +} + +func (c *PacketConn) FrontHeadroom() int { + return M.MaxSocksaddrLength + 4 +} + +func (c *PacketConn) NeedAdditionalReadDeadline() bool { + return true +} + +func (c *PacketConn) Upstream() any { + return c.Conn +} diff --git a/transport/trojan/service_wait.go b/transport/trojan/service_wait.go new file mode 100644 index 00000000..5ec082fe --- /dev/null +++ b/transport/trojan/service_wait.go @@ -0,0 +1,45 @@ +package trojan + +import ( + "encoding/binary" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" +) + +var _ N.PacketReadWaiter = (*PacketConn)(nil) + +func (c *PacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *PacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + destination, err = M.SocksaddrSerializer.ReadAddrPort(c.Conn) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read destination") + } + + var length uint16 + err = binary.Read(c.Conn, binary.BigEndian, &length) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read chunk length") + } + + err = rw.SkipN(c.Conn, 2) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "skip crlf") + } + + buffer = c.readWaitOptions.NewPacketBuffer() + _, err = buffer.ReadFullFrom(c.Conn, int(length)) + if err != nil { + buffer.Release() + return + } + c.readWaitOptions.PostReturn(buffer) + return +} diff --git a/transport/v2ray/grpc.go b/transport/v2ray/grpc.go new file mode 100644 index 00000000..1b4250ad --- /dev/null +++ b/transport/v2ray/grpc.go @@ -0,0 +1,30 @@ +//go:build with_grpc + +package v2ray + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2raygrpc" + "github.com/sagernet/sing-box/transport/v2raygrpclite" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewGRPCServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + if options.ForceLite { + return v2raygrpclite.NewServer(ctx, logger, options, tlsConfig, handler) + } + return v2raygrpc.NewServer(ctx, logger, options, tlsConfig, handler) +} + +func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + if options.ForceLite { + return v2raygrpclite.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil + } + return v2raygrpc.NewClient(ctx, dialer, serverAddr, options, tlsConfig) +} diff --git a/transport/v2ray/grpc_lite.go b/transport/v2ray/grpc_lite.go new file mode 100644 index 00000000..4f2814a7 --- /dev/null +++ b/transport/v2ray/grpc_lite.go @@ -0,0 +1,23 @@ +//go:build !with_grpc + +package v2ray + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2raygrpclite" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewGRPCServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + return v2raygrpclite.NewServer(ctx, logger, options, tlsConfig, handler) +} + +func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + return v2raygrpclite.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil +} diff --git a/transport/v2ray/quic.go b/transport/v2ray/quic.go new file mode 100644 index 00000000..4d3cdc6f --- /dev/null +++ b/transport/v2ray/quic.go @@ -0,0 +1,37 @@ +package v2ray + +import ( + "context" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var ( + quicServerConstructor ServerConstructor[option.V2RayQUICOptions] + quicClientConstructor ClientConstructor[option.V2RayQUICOptions] +) + +func RegisterQUICConstructor(server ServerConstructor[option.V2RayQUICOptions], client ClientConstructor[option.V2RayQUICOptions]) { + quicServerConstructor = server + quicClientConstructor = client +} + +func NewQUICServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + if quicServerConstructor == nil { + return nil, os.ErrInvalid + } + return quicServerConstructor(ctx, logger, options, tlsConfig, handler) +} + +func NewQUICClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + if quicClientConstructor == nil { + return nil, os.ErrInvalid + } + return quicClientConstructor(ctx, dialer, serverAddr, options, tlsConfig) +} diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go new file mode 100644 index 00000000..ab52f55e --- /dev/null +++ b/transport/v2ray/transport.go @@ -0,0 +1,68 @@ +package v2ray + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing-box/transport/v2rayhttpupgrade" + "github.com/sagernet/sing-box/transport/v2raywebsocket" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type ( + ServerConstructor[O any] func(ctx context.Context, logger logger.ContextLogger, options O, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) + ClientConstructor[O any] func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options O, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) +) + +func NewServerTransport(ctx context.Context, logger logger.ContextLogger, options option.V2RayTransportOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + if options.Type == "" { + return nil, nil + } + switch options.Type { + case C.V2RayTransportTypeHTTP: + return v2rayhttp.NewServer(ctx, logger, options.HTTPOptions, tlsConfig, handler) + case C.V2RayTransportTypeWebsocket: + return v2raywebsocket.NewServer(ctx, logger, options.WebsocketOptions, tlsConfig, handler) + case C.V2RayTransportTypeQUIC: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } + return NewQUICServer(ctx, logger, options.QUICOptions, tlsConfig, handler) + case C.V2RayTransportTypeGRPC: + return NewGRPCServer(ctx, logger, options.GRPCOptions, tlsConfig, handler) + case C.V2RayTransportTypeHTTPUpgrade: + return v2rayhttpupgrade.NewServer(ctx, logger, options.HTTPUpgradeOptions, tlsConfig, handler) + default: + return nil, E.New("unknown transport type: " + options.Type) + } +} + +func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayTransportOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + if options.Type == "" { + return nil, nil + } + switch options.Type { + case C.V2RayTransportTypeHTTP: + return v2rayhttp.NewClient(ctx, dialer, serverAddr, options.HTTPOptions, tlsConfig) + case C.V2RayTransportTypeGRPC: + return NewGRPCClient(ctx, dialer, serverAddr, options.GRPCOptions, tlsConfig) + case C.V2RayTransportTypeWebsocket: + return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig) + case C.V2RayTransportTypeQUIC: + if tlsConfig == nil { + return nil, C.ErrTLSRequired + } + return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig) + case C.V2RayTransportTypeHTTPUpgrade: + return v2rayhttpupgrade.NewClient(ctx, dialer, serverAddr, options.HTTPUpgradeOptions, tlsConfig) + default: + return nil, E.New("unknown transport type: " + options.Type) + } +} diff --git a/transport/v2raygrpc/client.go b/transport/v2raygrpc/client.go new file mode 100644 index 00000000..5af53856 --- /dev/null +++ b/transport/v2raygrpc/client.go @@ -0,0 +1,118 @@ +package v2raygrpc + +import ( + "context" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/http2" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + ctx context.Context + dialer N.Dialer + serverAddr string + serviceName string + dialOptions []grpc.DialOption + conn atomic.Pointer[grpc.ClientConn] + connAccess sync.Mutex +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + var dialOptions []grpc.DialOption + if tlsConfig != nil { + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) + } + dialOptions = append(dialOptions, grpc.WithTransportCredentials(NewTLSTransportCredentials(tlsConfig))) + } else { + dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + if options.IdleTimeout > 0 { + dialOptions = append(dialOptions, grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: time.Duration(options.IdleTimeout), + Timeout: time.Duration(options.PingTimeout), + PermitWithoutStream: options.PermitWithoutStream, + })) + } + dialOptions = append(dialOptions, grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.Config{ + BaseDelay: 500 * time.Millisecond, + Multiplier: 1.5, + Jitter: 0.2, + MaxDelay: 19 * time.Second, + }, + MinConnectTimeout: 5 * time.Second, + })) + dialOptions = append(dialOptions, grpc.WithContextDialer(func(ctx context.Context, server string) (net.Conn, error) { + return dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(server)) + })) + //nolint:staticcheck + dialOptions = append(dialOptions, grpc.WithReturnConnectionError()) + return &Client{ + ctx: ctx, + dialer: dialer, + serverAddr: serverAddr.String(), + serviceName: options.ServiceName, + dialOptions: dialOptions, + }, nil +} + +func (c *Client) connect() (*grpc.ClientConn, error) { + conn := c.conn.Load() + if conn != nil && conn.GetState() != connectivity.Shutdown { + return conn, nil + } + c.connAccess.Lock() + defer c.connAccess.Unlock() + conn = c.conn.Load() + if conn != nil && conn.GetState() != connectivity.Shutdown { + return conn, nil + } + //nolint:staticcheck + conn, err := grpc.DialContext(c.ctx, c.serverAddr, c.dialOptions...) + if err != nil { + return nil, err + } + c.conn.Store(conn) + return conn, nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + clientConn, err := c.connect() + if err != nil { + return nil, err + } + client := NewGunServiceClient(clientConn).(GunServiceCustomNameClient) + ctx, cancel := common.ContextWithCancelCause(ctx) + stream, err := client.TunCustomName(ctx, c.serviceName) + if err != nil { + cancel(err) + return nil, err + } + return NewGRPCConn(stream, cancel), nil +} + +func (c *Client) Close() error { + conn := c.conn.Swap(nil) + if conn != nil { + conn.Close() + } + return nil +} diff --git a/transport/v2raygrpc/conn.go b/transport/v2raygrpc/conn.go new file mode 100644 index 00000000..87be9661 --- /dev/null +++ b/transport/v2raygrpc/conn.go @@ -0,0 +1,106 @@ +package v2raygrpc + +import ( + "context" + "net" + "os" + "sync" + "time" + + "github.com/sagernet/sing/common/baderror" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ net.Conn = (*GRPCConn)(nil) + +type GRPCConn struct { + GunService + cache []byte + cancel context.CancelCauseFunc + closeOnce sync.Once +} + +func NewGRPCConn(service GunService, cancel context.CancelCauseFunc) *GRPCConn { + //nolint:staticcheck + if client, isClient := service.(GunService_TunClient); isClient { + service = &clientConnWrapper{client} + } + return &GRPCConn{ + GunService: service, + cancel: cancel, + } +} + +func (c *GRPCConn) Read(b []byte) (n int, err error) { + if len(c.cache) > 0 { + n = copy(b, c.cache) + c.cache = c.cache[n:] + return + } + hunk, err := c.Recv() + err = baderror.WrapGRPC(err) + if err != nil { + return + } + n = copy(b, hunk.Data) + if n < len(hunk.Data) { + c.cache = hunk.Data[n:] + } + return +} + +func (c *GRPCConn) Write(b []byte) (n int, err error) { + err = baderror.WrapGRPC(c.Send(&Hunk{Data: b})) + if err != nil { + return + } + return len(b), nil +} + +func (c *GRPCConn) Close() error { + c.closeOnce.Do(func() { + if c.cancel != nil { + c.cancel(nil) + } + }) + return nil +} + +func (c *GRPCConn) LocalAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *GRPCConn) RemoteAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *GRPCConn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *GRPCConn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *GRPCConn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *GRPCConn) NeedAdditionalReadDeadline() bool { + return true +} + +func (c *GRPCConn) Upstream() any { + return c.GunService +} + +var _ N.WriteCloser = (*clientConnWrapper)(nil) + +type clientConnWrapper struct { + GunService_TunClient +} + +func (c *clientConnWrapper) CloseWrite() error { + return c.CloseSend() +} diff --git a/transport/v2raygrpc/credentials/credentials.go b/transport/v2raygrpc/credentials/credentials.go new file mode 100644 index 00000000..32c9b590 --- /dev/null +++ b/transport/v2raygrpc/credentials/credentials.go @@ -0,0 +1,49 @@ +/* + * Copyright 2021 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package credentials + +import ( + "context" +) + +// requestInfoKey is a struct to be used as the key to store RequestInfo in a +// context. +type requestInfoKey struct{} + +// NewRequestInfoContext creates a context with ri. +func NewRequestInfoContext(ctx context.Context, ri interface{}) context.Context { + return context.WithValue(ctx, requestInfoKey{}, ri) +} + +// RequestInfoFromContext extracts the RequestInfo from ctx. +func RequestInfoFromContext(ctx context.Context) interface{} { + return ctx.Value(requestInfoKey{}) +} + +// clientHandshakeInfoKey is a struct used as the key to store +// ClientHandshakeInfo in a context. +type clientHandshakeInfoKey struct{} + +// ClientHandshakeInfoFromContext extracts the ClientHandshakeInfo from ctx. +func ClientHandshakeInfoFromContext(ctx context.Context) interface{} { + return ctx.Value(clientHandshakeInfoKey{}) +} + +// NewClientHandshakeInfoContext creates a context with chi. +func NewClientHandshakeInfoContext(ctx context.Context, chi interface{}) context.Context { + return context.WithValue(ctx, clientHandshakeInfoKey{}, chi) +} diff --git a/transport/v2raygrpc/credentials/spiffe.go b/transport/v2raygrpc/credentials/spiffe.go new file mode 100644 index 00000000..25ade623 --- /dev/null +++ b/transport/v2raygrpc/credentials/spiffe.go @@ -0,0 +1,75 @@ +/* + * + * Copyright 2020 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package credentials defines APIs for parsing SPIFFE ID. +// +// All APIs in this package are experimental. +package credentials + +import ( + "crypto/tls" + "crypto/x509" + "net/url" + + "google.golang.org/grpc/grpclog" +) + +var logger = grpclog.Component("credentials") + +// SPIFFEIDFromState parses the SPIFFE ID from State. If the SPIFFE ID format +// is invalid, return nil with warning. +func SPIFFEIDFromState(state tls.ConnectionState) *url.URL { + if len(state.PeerCertificates) == 0 || len(state.PeerCertificates[0].URIs) == 0 { + return nil + } + return SPIFFEIDFromCert(state.PeerCertificates[0]) +} + +// SPIFFEIDFromCert parses the SPIFFE ID from x509.Certificate. If the SPIFFE +// ID format is invalid, return nil with warning. +func SPIFFEIDFromCert(cert *x509.Certificate) *url.URL { + if cert == nil || cert.URIs == nil { + return nil + } + var spiffeID *url.URL + for _, uri := range cert.URIs { + if uri == nil || uri.Scheme != "spiffe" || uri.Opaque != "" || (uri.User != nil && uri.User.Username() != "") { + continue + } + // From this point, we assume the uri is intended for a SPIFFE ID. + if len(uri.String()) > 2048 { + logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes") + return nil + } + if len(uri.Host) == 0 || len(uri.Path) == 0 { + logger.Warning("invalid SPIFFE ID: domain or workload ID is empty") + return nil + } + if len(uri.Host) > 255 { + logger.Warning("invalid SPIFFE ID: domain length larger than 255 characters") + return nil + } + // A valid SPIFFE certificate can only have exactly one URI SAN field. + if len(cert.URIs) > 1 { + logger.Warning("invalid SPIFFE ID: multiple URI SANs") + return nil + } + spiffeID = uri + } + return spiffeID +} diff --git a/transport/v2raygrpc/credentials/syscallconn.go b/transport/v2raygrpc/credentials/syscallconn.go new file mode 100644 index 00000000..2919632d --- /dev/null +++ b/transport/v2raygrpc/credentials/syscallconn.go @@ -0,0 +1,58 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package credentials + +import ( + "net" + "syscall" +) + +type sysConn = syscall.Conn + +// syscallConn keeps reference of rawConn to support syscall.Conn for channelz. +// SyscallConn() (the method in interface syscall.Conn) is explicitly +// implemented on this type, +// +// Interface syscall.Conn is implemented by most net.Conn implementations (e.g. +// TCPConn, UnixConn), but is not part of net.Conn interface. So wrapper conns +// that embed net.Conn don't implement syscall.Conn. (Side note: tls.Conn +// doesn't embed net.Conn, so even if syscall.Conn is part of net.Conn, it won't +// help here). +type syscallConn struct { + net.Conn + // sysConn is a type alias of syscall.Conn. It's necessary because the name + // `Conn` collides with `net.Conn`. + sysConn +} + +// WrapSyscallConn tries to wrap rawConn and newConn into a net.Conn that +// implements syscall.Conn. rawConn will be used to support syscall, and newConn +// will be used for read/write. +// +// This function returns newConn if rawConn doesn't implement syscall.Conn. +func WrapSyscallConn(rawConn, newConn net.Conn) net.Conn { + sysConn, ok := rawConn.(syscall.Conn) + if !ok { + return newConn + } + return &syscallConn{ + Conn: newConn, + sysConn: sysConn, + } +} diff --git a/transport/v2raygrpc/credentials/util.go b/transport/v2raygrpc/credentials/util.go new file mode 100644 index 00000000..f792fd22 --- /dev/null +++ b/transport/v2raygrpc/credentials/util.go @@ -0,0 +1,52 @@ +/* + * + * Copyright 2020 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package credentials + +import ( + "crypto/tls" +) + +const alpnProtoStrH2 = "h2" + +// AppendH2ToNextProtos appends h2 to next protos. +func AppendH2ToNextProtos(ps []string) []string { + for _, p := range ps { + if p == alpnProtoStrH2 { + return ps + } + } + ret := make([]string, 0, len(ps)+1) + ret = append(ret, ps...) + return append(ret, alpnProtoStrH2) +} + +// CloneTLSConfig returns a shallow clone of the exported +// fields of cfg, ignoring the unexported sync.Once, which +// contains a mutex and must not be copied. +// +// If cfg is nil, a new zero tls.Config is returned. +// +// TODO: inline this function if possible. +func CloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + + return cfg.Clone() +} diff --git a/transport/v2raygrpc/custom_name.go b/transport/v2raygrpc/custom_name.go new file mode 100644 index 00000000..ce970dc6 --- /dev/null +++ b/transport/v2raygrpc/custom_name.go @@ -0,0 +1,51 @@ +package v2raygrpc + +import ( + "context" + + "google.golang.org/grpc" +) + +type GunService interface { + Context() context.Context + Send(*Hunk) error + Recv() (*Hunk, error) +} + +func ServerDesc(name string) grpc.ServiceDesc { + return grpc.ServiceDesc{ + ServiceName: name, + HandlerType: (*GunServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Tun", + Handler: _GunService_Tun_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "gun.proto", + } +} + +func (c *gunServiceClient) TunCustomName(ctx context.Context, name string, opts ...grpc.CallOption) (GunService_TunClient, error) { + stream, err := c.cc.NewStream(ctx, &ServerDesc(name).Streams[0], "/"+name+"/Tun", opts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Hunk, Hunk]{ClientStream: stream} + return x, nil +} + +var _ GunServiceCustomNameClient = (*gunServiceClient)(nil) + +type GunServiceCustomNameClient interface { + TunCustomName(ctx context.Context, name string, opts ...grpc.CallOption) (GunService_TunClient, error) + Tun(ctx context.Context, opts ...grpc.CallOption) (GunService_TunClient, error) +} + +func RegisterGunServiceCustomNameServer(s *grpc.Server, srv GunServiceServer, name string) { + desc := ServerDesc(name) + s.RegisterService(&desc, srv) +} diff --git a/transport/v2raygrpc/server.go b/transport/v2raygrpc/server.go new file mode 100644 index 00000000..4d426aa1 --- /dev/null +++ b/transport/v2raygrpc/server.go @@ -0,0 +1,97 @@ +package v2raygrpc + +import ( + "context" + "net" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/http2" + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" + gM "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + logger logger.ContextLogger + handler adapter.V2RayServerTransportHandler + server *grpc.Server +} + +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { + var serverOptions []grpc.ServerOption + if tlsConfig != nil { + if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) { + tlsConfig.SetNextProtos(append([]string{"h2"}, tlsConfig.NextProtos()...)) + } + serverOptions = append(serverOptions, grpc.Creds(NewTLSTransportCredentials(tlsConfig))) + } + if options.IdleTimeout > 0 { + serverOptions = append(serverOptions, grpc.KeepaliveParams(keepalive.ServerParameters{ + Time: time.Duration(options.IdleTimeout), + Timeout: time.Duration(options.PingTimeout), + })) + } + server := &Server{ctx, logger, handler, grpc.NewServer(serverOptions...)} + RegisterGunServiceCustomNameServer(server.server, server, options.ServiceName) + return server, nil +} + +func (s *Server) Tun(server GunService_TunServer) error { + conn := NewGRPCConn(server, nil) + var source M.Socksaddr + if remotePeer, loaded := peer.FromContext(server.Context()); loaded { + source = M.SocksaddrFromNet(remotePeer.Addr) + } + if grpcMetadata, loaded := gM.FromIncomingContext(server.Context()); loaded { + forwardFrom := strings.Join(grpcMetadata.Get("X-Forwarded-For"), ",") + if forwardFrom != "" { + for _, from := range strings.Split(forwardFrom, ",") { + originAddr := M.ParseSocksaddr(from) + if originAddr.IsValid() { + source = originAddr.Unwrap() + } + } + } + } + done := make(chan struct{}) + go s.handler.NewConnectionEx(log.ContextWithNewID(s.ctx), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) { + close(done) + })) + <-done + return nil +} + +func (s *Server) mustEmbedUnimplementedGunServiceServer() { +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func (s *Server) Serve(listener net.Listener) error { + return s.server.Serve(listener) +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + +func (s *Server) Close() error { + s.server.Stop() + return nil +} diff --git a/transport/v2raygrpc/stream.pb.go b/transport/v2raygrpc/stream.pb.go new file mode 100644 index 00000000..9576c739 --- /dev/null +++ b/transport/v2raygrpc/stream.pb.go @@ -0,0 +1,125 @@ +package v2raygrpc + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Hunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hunk) Reset() { + *x = Hunk{} + mi := &file_transport_v2raygrpc_stream_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hunk) ProtoMessage() {} + +func (x *Hunk) ProtoReflect() protoreflect.Message { + mi := &file_transport_v2raygrpc_stream_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hunk.ProtoReflect.Descriptor instead. +func (*Hunk) Descriptor() ([]byte, []int) { + return file_transport_v2raygrpc_stream_proto_rawDescGZIP(), []int{0} +} + +func (x *Hunk) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_transport_v2raygrpc_stream_proto protoreflect.FileDescriptor + +const file_transport_v2raygrpc_stream_proto_rawDesc = "" + + "\n" + + " transport/v2raygrpc/stream.proto\x12\x13transport.v2raygrpc\"\x1a\n" + + "\x04Hunk\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data2M\n" + + "\n" + + "GunService\x12?\n" + + "\x03Tun\x12\x19.transport.v2raygrpc.Hunk\x1a\x19.transport.v2raygrpc.Hunk(\x010\x01B2Z0github.com/sagernet/sing-box/transport/v2raygrpcb\x06proto3" + +var ( + file_transport_v2raygrpc_stream_proto_rawDescOnce sync.Once + file_transport_v2raygrpc_stream_proto_rawDescData []byte +) + +func file_transport_v2raygrpc_stream_proto_rawDescGZIP() []byte { + file_transport_v2raygrpc_stream_proto_rawDescOnce.Do(func() { + file_transport_v2raygrpc_stream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_v2raygrpc_stream_proto_rawDesc), len(file_transport_v2raygrpc_stream_proto_rawDesc))) + }) + return file_transport_v2raygrpc_stream_proto_rawDescData +} + +var ( + file_transport_v2raygrpc_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 1) + file_transport_v2raygrpc_stream_proto_goTypes = []any{ + (*Hunk)(nil), // 0: transport.v2raygrpc.Hunk + } +) + +var file_transport_v2raygrpc_stream_proto_depIdxs = []int32{ + 0, // 0: transport.v2raygrpc.GunService.Tun:input_type -> transport.v2raygrpc.Hunk + 0, // 1: transport.v2raygrpc.GunService.Tun:output_type -> transport.v2raygrpc.Hunk + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_v2raygrpc_stream_proto_init() } +func file_transport_v2raygrpc_stream_proto_init() { + if File_transport_v2raygrpc_stream_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_v2raygrpc_stream_proto_rawDesc), len(file_transport_v2raygrpc_stream_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_transport_v2raygrpc_stream_proto_goTypes, + DependencyIndexes: file_transport_v2raygrpc_stream_proto_depIdxs, + MessageInfos: file_transport_v2raygrpc_stream_proto_msgTypes, + }.Build() + File_transport_v2raygrpc_stream_proto = out.File + file_transport_v2raygrpc_stream_proto_goTypes = nil + file_transport_v2raygrpc_stream_proto_depIdxs = nil +} diff --git a/transport/v2raygrpc/stream.proto b/transport/v2raygrpc/stream.proto new file mode 100644 index 00000000..514072c0 --- /dev/null +++ b/transport/v2raygrpc/stream.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package transport.v2raygrpc; +option go_package = "github.com/sagernet/sing-box/transport/v2raygrpc"; + +message Hunk { + bytes data = 1; +} + +service GunService { + rpc Tun (stream Hunk) returns (stream Hunk); +} diff --git a/transport/v2raygrpc/stream_grpc.pb.go b/transport/v2raygrpc/stream_grpc.pb.go new file mode 100644 index 00000000..21cc3279 --- /dev/null +++ b/transport/v2raygrpc/stream_grpc.pb.go @@ -0,0 +1,110 @@ +package v2raygrpc + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GunService_Tun_FullMethodName = "/transport.v2raygrpc.GunService/Tun" +) + +// GunServiceClient is the client API for GunService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GunServiceClient interface { + Tun(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Hunk, Hunk], error) +} + +type gunServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGunServiceClient(cc grpc.ClientConnInterface) GunServiceClient { + return &gunServiceClient{cc} +} + +func (c *gunServiceClient) Tun(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Hunk, Hunk], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GunService_ServiceDesc.Streams[0], GunService_Tun_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Hunk, Hunk]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GunService_TunClient = grpc.BidiStreamingClient[Hunk, Hunk] + +// GunServiceServer is the server API for GunService service. +// All implementations must embed UnimplementedGunServiceServer +// for forward compatibility. +type GunServiceServer interface { + Tun(grpc.BidiStreamingServer[Hunk, Hunk]) error + mustEmbedUnimplementedGunServiceServer() +} + +// UnimplementedGunServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGunServiceServer struct{} + +func (UnimplementedGunServiceServer) Tun(grpc.BidiStreamingServer[Hunk, Hunk]) error { + return status.Error(codes.Unimplemented, "method Tun not implemented") +} +func (UnimplementedGunServiceServer) mustEmbedUnimplementedGunServiceServer() {} +func (UnimplementedGunServiceServer) testEmbeddedByValue() {} + +// UnsafeGunServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GunServiceServer will +// result in compilation errors. +type UnsafeGunServiceServer interface { + mustEmbedUnimplementedGunServiceServer() +} + +func RegisterGunServiceServer(s grpc.ServiceRegistrar, srv GunServiceServer) { + // If the following call panics, it indicates UnimplementedGunServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GunService_ServiceDesc, srv) +} + +func _GunService_Tun_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(GunServiceServer).Tun(&grpc.GenericServerStream[Hunk, Hunk]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GunService_TunServer = grpc.BidiStreamingServer[Hunk, Hunk] + +// GunService_ServiceDesc is the grpc.ServiceDesc for GunService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GunService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "transport.v2raygrpc.GunService", + HandlerType: (*GunServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Tun", + Handler: _GunService_Tun_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "transport/v2raygrpc/stream.proto", +} diff --git a/transport/v2raygrpc/tls_credentials.go b/transport/v2raygrpc/tls_credentials.go new file mode 100644 index 00000000..53a3b7ab --- /dev/null +++ b/transport/v2raygrpc/tls_credentials.go @@ -0,0 +1,86 @@ +package v2raygrpc + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/common/tls" + internal_credentials "github.com/sagernet/sing-box/transport/v2raygrpc/credentials" + + "google.golang.org/grpc/credentials" +) + +type TLSTransportCredentials struct { + config tls.Config +} + +func NewTLSTransportCredentials(config tls.Config) credentials.TransportCredentials { + return &TLSTransportCredentials{config} +} + +func (c *TLSTransportCredentials) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{ + SecurityProtocol: "tls", + SecurityVersion: "1.2", + ServerName: c.config.ServerName(), + } +} + +func (c *TLSTransportCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { + cfg := c.config.Clone() + if cfg.ServerName() == "" { + serverName, _, err := net.SplitHostPort(authority) + if err != nil { + serverName = authority + } + cfg.SetServerName(serverName) + } + conn, err := tls.ClientHandshake(ctx, rawConn, cfg) + if err != nil { + return nil, nil, err + } + tlsInfo := credentials.TLSInfo{ + State: conn.ConnectionState(), + CommonAuthInfo: credentials.CommonAuthInfo{ + SecurityLevel: credentials.PrivacyAndIntegrity, + }, + } + id := internal_credentials.SPIFFEIDFromState(conn.ConnectionState()) + if id != nil { + tlsInfo.SPIFFEID = id + } + return internal_credentials.WrapSyscallConn(rawConn, conn), tlsInfo, nil +} + +func (c *TLSTransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { + serverConfig, isServer := c.config.(tls.ServerConfig) + if !isServer { + return nil, nil, os.ErrInvalid + } + conn, err := tls.ServerHandshake(context.Background(), rawConn, serverConfig) + if err != nil { + rawConn.Close() + return nil, nil, err + } + tlsInfo := credentials.TLSInfo{ + State: conn.ConnectionState(), + CommonAuthInfo: credentials.CommonAuthInfo{ + SecurityLevel: credentials.PrivacyAndIntegrity, + }, + } + id := internal_credentials.SPIFFEIDFromState(conn.ConnectionState()) + if id != nil { + tlsInfo.SPIFFEID = id + } + return internal_credentials.WrapSyscallConn(rawConn, conn), tlsInfo, nil +} + +func (c *TLSTransportCredentials) Clone() credentials.TransportCredentials { + return NewTLSTransportCredentials(c.config) +} + +func (c *TLSTransportCredentials) OverrideServerName(serverNameOverride string) error { + c.config.SetServerName(serverNameOverride) + return nil +} diff --git a/transport/v2raygrpclite/client.go b/transport/v2raygrpclite/client.go new file mode 100644 index 00000000..b2aab911 --- /dev/null +++ b/transport/v2raygrpclite/client.go @@ -0,0 +1,108 @@ +package v2raygrpclite + +import ( + "context" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/http2" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +var defaultClientHeader = http.Header{ + "Content-Type": []string{"application/grpc"}, + "User-Agent": []string{"grpc-go/1.48.0"}, + "TE": []string{"trailers"}, +} + +type Client struct { + ctx context.Context + serverAddr M.Socksaddr + transport *http2.Transport + options option.V2RayGRPCOptions + url *url.URL + host string +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) adapter.V2RayClientTransport { + var host string + if tlsConfig != nil && tlsConfig.ServerName() != "" { + host = M.ParseSocksaddrHostPort(tlsConfig.ServerName(), serverAddr.Port).String() + } else { + host = serverAddr.String() + } + client := &Client{ + ctx: ctx, + serverAddr: serverAddr, + options: options, + transport: &http2.Transport{ + ReadIdleTimeout: time.Duration(options.IdleTimeout), + PingTimeout: time.Duration(options.PingTimeout), + DisableCompression: true, + }, + url: &url.URL{ + Scheme: "https", + Host: serverAddr.String(), + Path: "/" + options.ServiceName + "/Tun", + RawPath: "/" + url.PathEscape(options.ServiceName) + "/Tun", + }, + host: host, + } + if tlsConfig == nil { + client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + } else { + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) + } + tlsDialer := tls.NewDialer(dialer, tlsConfig) + client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { + return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr)) + } + } + + return client +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + pipeInReader, pipeInWriter := io.Pipe() + request := &http.Request{ + Method: http.MethodPost, + Body: pipeInReader, + URL: c.url, + Header: defaultClientHeader, + Host: c.host, + } + request = request.WithContext(ctx) + conn := newLateGunConn(pipeInWriter) + go func() { + response, err := c.transport.RoundTrip(request) + if err != nil { + conn.setup(nil, err) + } else if response.StatusCode != 200 { + response.Body.Close() + conn.setup(nil, E.New("v2ray-grpc: unexpected status: ", response.Status)) + } else { + conn.setup(response.Body, nil) + } + }() + return conn, nil +} + +func (c *Client) Close() error { + v2rayhttp.ResetTransport(c.transport) + return nil +} diff --git a/transport/v2raygrpclite/conn.go b/transport/v2raygrpclite/conn.go new file mode 100644 index 00000000..5feafbb6 --- /dev/null +++ b/transport/v2raygrpclite/conn.go @@ -0,0 +1,169 @@ +package v2raygrpclite + +import ( + std_bufio "bufio" + "encoding/binary" + "io" + "net" + "net/http" + "os" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/baderror" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" +) + +// kanged from: https://github.com/Qv2ray/gun-lite + +var _ net.Conn = (*GunConn)(nil) + +type GunConn struct { + rawReader io.Reader + reader *std_bufio.Reader + writer io.Writer + flusher http.Flusher + create chan struct{} + err error + readRemaining int +} + +func newGunConn(reader io.Reader, writer io.Writer, flusher http.Flusher) *GunConn { + return &GunConn{ + rawReader: reader, + reader: std_bufio.NewReader(reader), + writer: writer, + flusher: flusher, + } +} + +func newLateGunConn(writer io.Writer) *GunConn { + return &GunConn{ + create: make(chan struct{}), + writer: writer, + } +} + +func (c *GunConn) setup(reader io.Reader, err error) { + if reader != nil { + c.rawReader = reader + c.reader = std_bufio.NewReader(reader) + } + c.err = err + close(c.create) +} + +func (c *GunConn) Read(b []byte) (n int, err error) { + n, err = c.read(b) + return n, baderror.WrapH2(err) +} + +func (c *GunConn) read(b []byte) (n int, err error) { + if c.reader == nil { + <-c.create + if c.err != nil { + return 0, c.err + } + } + + if c.readRemaining > 0 { + if len(b) > c.readRemaining { + b = b[:c.readRemaining] + } + n, err = c.reader.Read(b) + c.readRemaining -= n + return + } + + _, err = c.reader.Discard(6) + if err != nil { + return + } + + dataLen, err := binary.ReadUvarint(c.reader) + if err != nil { + return + } + + readLen := int(dataLen) + c.readRemaining = readLen + if len(b) > readLen { + b = b[:readLen] + } + + n, err = c.reader.Read(b) + c.readRemaining -= n + return +} + +func (c *GunConn) Write(b []byte) (n int, err error) { + varLen := varbin.UvarintLen(uint64(len(b))) + buffer := buf.NewSize(6 + varLen + len(b)) + header := buffer.Extend(6 + varLen) + header[0] = 0x00 + binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+len(b))) + header[5] = 0x0A + binary.PutUvarint(header[6:], uint64(len(b))) + common.Must1(buffer.Write(b)) + _, err = c.writer.Write(buffer.Bytes()) + if err != nil { + return 0, baderror.WrapH2(err) + } + if c.flusher != nil { + c.flusher.Flush() + } + return len(b), nil +} + +func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + varLen := varbin.UvarintLen(uint64(dataLen)) + header := buffer.ExtendHeader(6 + varLen) + header[0] = 0x00 + binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen)) + header[5] = 0x0A + binary.PutUvarint(header[6:], uint64(dataLen)) + err := common.Error(c.writer.Write(buffer.Bytes())) + if err != nil { + return baderror.WrapH2(err) + } + if c.flusher != nil { + c.flusher.Flush() + } + return nil +} + +func (c *GunConn) FrontHeadroom() int { + return 6 + binary.MaxVarintLen64 +} + +func (c *GunConn) Close() error { + return common.Close(c.rawReader, c.writer) +} + +func (c *GunConn) LocalAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *GunConn) RemoteAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *GunConn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *GunConn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *GunConn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *GunConn) NeedAdditionalReadDeadline() bool { + return true +} diff --git a/transport/v2raygrpclite/server.go b/transport/v2raygrpclite/server.go new file mode 100644 index 00000000..622d785a --- /dev/null +++ b/transport/v2raygrpclite/server.go @@ -0,0 +1,119 @@ +package v2raygrpclite + +import ( + "context" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHttp "github.com/sagernet/sing/protocol/http" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + tlsConfig tls.ServerConfig + logger logger.ContextLogger + handler adapter.V2RayServerTransportHandler + httpServer *http.Server + h2Server *http2.Server + h2cHandler http.Handler + path string +} + +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { + server := &Server{ + tlsConfig: tlsConfig, + logger: logger, + handler: handler, + path: "/" + options.ServiceName + "/Tun", + h2Server: &http2.Server{ + IdleTimeout: time.Duration(options.IdleTimeout), + }, + } + server.httpServer = &http.Server{ + Handler: server, + BaseContext: func(net.Listener) context.Context { + return ctx + }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + } + server.h2cHandler = h2c.NewHandler(server, server.h2Server) + return server, nil +} + +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" { + s.h2cHandler.ServeHTTP(writer, request) + return + } + if request.URL.Path != s.path { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + if request.Method != http.MethodPost { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) + return + } + if ct := request.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/grpc") { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad content type: ", ct)) + return + } + writer.Header().Set("Content-Type", "application/grpc") + writer.Header().Set("TE", "trailers") + writer.WriteHeader(http.StatusOK) + done := make(chan struct{}) + conn := v2rayhttp.NewHTTP2Wrapper(newGunConn(request.Body, writer, writer.(http.Flusher))) + s.handler.NewConnectionEx(request.Context(), conn, sHttp.SourceAddress(request), M.Socksaddr{}, N.OnceClose(func(it error) { + close(done) + })) + <-done + conn.CloseWrapper() +} + +func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { + if statusCode > 0 { + writer.WriteHeader(statusCode) + } + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func (s *Server) Serve(listener net.Listener) error { + if s.tlsConfig != nil { + if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...)) + } + listener = aTLS.NewListener(listener, s.tlsConfig) + } + return s.httpServer.Serve(listener) +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + +func (s *Server) Close() error { + return common.Close(common.PtrOrNil(s.httpServer)) +} diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go new file mode 100644 index 00000000..6c327cd6 --- /dev/null +++ b/transport/v2rayhttp/client.go @@ -0,0 +1,157 @@ +package v2rayhttp + +import ( + "context" + "io" + "math/rand" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHTTP "github.com/sagernet/sing/protocol/http" + + "golang.org/x/net/http2" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + ctx context.Context + dialer N.Dialer + serverAddr M.Socksaddr + transport http.RoundTripper + http2 bool + requestURL url.URL + host []string + method string + headers http.Header +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + var transport http.RoundTripper + if tlsConfig == nil { + transport = &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + } + } else { + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) + } + tlsDialer := tls.NewDialer(dialer, tlsConfig) + transport = &http2.Transport{ + ReadIdleTimeout: time.Duration(options.IdleTimeout), + PingTimeout: time.Duration(options.PingTimeout), + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { + return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr)) + }, + } + } + if options.Method == "" { + options.Method = http.MethodPut + } + var requestURL url.URL + if tlsConfig == nil { + requestURL.Scheme = "http" + } else { + requestURL.Scheme = "https" + } + requestURL.Host = serverAddr.String() + requestURL.Path = options.Path + err := sHTTP.URLSetPath(&requestURL, options.Path) + if err != nil { + return nil, E.Cause(err, "parse path") + } + if !strings.HasPrefix(requestURL.Path, "/") { + requestURL.Path = "/" + requestURL.Path + } + return &Client{ + ctx: ctx, + dialer: dialer, + serverAddr: serverAddr, + requestURL: requestURL, + host: options.Host, + method: options.Method, + headers: options.Headers.Build(), + transport: transport, + http2: tlsConfig != nil, + }, nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + if !c.http2 { + return c.dialHTTP(ctx) + } else { + return c.dialHTTP2(ctx) + } +} + +func (c *Client) dialHTTP(ctx context.Context) (net.Conn, error) { + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) + if err != nil { + return nil, err + } + + request := &http.Request{ + Method: c.method, + URL: &c.requestURL, + Header: c.headers.Clone(), + } + switch hostLen := len(c.host); hostLen { + case 0: + request.Host = c.serverAddr.AddrString() + case 1: + request.Host = c.host[0] + default: + request.Host = c.host[rand.Intn(hostLen)] + } + + return NewHTTP1Conn(conn, request), nil +} + +func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { + pipeInReader, pipeInWriter := io.Pipe() + request := &http.Request{ + Method: c.method, + Body: pipeInReader, + URL: &c.requestURL, + Header: c.headers.Clone(), + } + request = request.WithContext(ctx) + switch hostLen := len(c.host); hostLen { + case 0: + // https://github.com/v2fly/v2ray-core/blob/master/transport/internet/http/config.go#L13 + request.Host = "www.example.com" + case 1: + request.Host = c.host[0] + default: + request.Host = c.host[rand.Intn(hostLen)] + } + conn := NewLateHTTPConn(pipeInWriter) + go func() { + response, err := c.transport.RoundTrip(request) + if err != nil { + conn.Setup(nil, err) + } else if response.StatusCode != 200 { + response.Body.Close() + conn.Setup(nil, E.New("v2ray-http: unexpected status: ", response.Status)) + } else { + conn.Setup(response.Body, nil) + } + }() + return conn, nil +} + +func (c *Client) Close() error { + c.transport = ResetTransport(c.transport) + return nil +} diff --git a/transport/v2rayhttp/conn.go b/transport/v2rayhttp/conn.go new file mode 100644 index 00000000..b339a753 --- /dev/null +++ b/transport/v2rayhttp/conn.go @@ -0,0 +1,267 @@ +package v2rayhttp + +import ( + std_bufio "bufio" + "context" + "io" + "net" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/baderror" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type HTTPConn struct { + net.Conn + request *http.Request + requestWritten bool + responseRead bool + responseCache *buf.Buffer +} + +func NewHTTP1Conn(conn net.Conn, request *http.Request) *HTTPConn { + if request.Header.Get("Host") == "" { + request.Header.Set("Host", request.Host) + } + return &HTTPConn{ + Conn: conn, + request: request, + } +} + +func (c *HTTPConn) Read(b []byte) (n int, err error) { + if !c.responseRead { + reader := std_bufio.NewReader(c.Conn) + response, err := http.ReadResponse(reader, c.request) + if err != nil { + return 0, E.Cause(err, "read response") + } + if response.StatusCode != 200 { + return 0, E.New("v2ray-http: unexpected status: ", response.Status) + } + if cacheLen := reader.Buffered(); cacheLen > 0 { + c.responseCache = buf.NewSize(cacheLen) + _, err = c.responseCache.ReadFullFrom(reader, cacheLen) + if err != nil { + c.responseCache.Release() + return 0, E.Cause(err, "read cache") + } + } + c.responseRead = true + } + if c.responseCache != nil { + n, err = c.responseCache.Read(b) + if err == io.EOF { + c.responseCache.Release() + c.responseCache = nil + } + if n > 0 { + return n, nil + } + } + return c.Conn.Read(b) +} + +func (c *HTTPConn) Write(b []byte) (int, error) { + if !c.requestWritten { + err := c.writeRequest(b) + if err != nil { + return 0, E.Cause(err, "write request") + } + c.requestWritten = true + return len(b), nil + } + return c.Conn.Write(b) +} + +func (c *HTTPConn) writeRequest(payload []byte) error { + writer := bufio.NewBufferedWriter(c.Conn, buf.New()) + const CRLF = "\r\n" + _, err := writer.Write([]byte(F.ToString(c.request.Method, " ", c.request.URL.RequestURI(), " HTTP/1.1", CRLF))) + if err != nil { + return err + } + for key, value := range c.request.Header { + _, err = writer.Write([]byte(F.ToString(key, ": ", strings.Join(value, ", "), CRLF))) + if err != nil { + return err + } + } + _, err = writer.Write([]byte(CRLF)) + if err != nil { + return err + } + _, err = writer.Write(payload) + if err != nil { + return err + } + err = writer.Fallthrough() + if err != nil { + return err + } + return nil +} + +func (c *HTTPConn) ReaderReplaceable() bool { + return c.responseRead +} + +func (c *HTTPConn) WriterReplaceable() bool { + return c.requestWritten +} + +func (c *HTTPConn) NeedHandshake() bool { + return !c.requestWritten +} + +func (c *HTTPConn) Upstream() any { + return c.Conn +} + +type HTTP2Conn struct { + reader io.Reader + writer io.Writer + create chan struct{} + err error +} + +func NewHTTPConn(reader io.Reader, writer io.Writer) HTTP2Conn { + return HTTP2Conn{ + reader: reader, + writer: writer, + } +} + +func NewLateHTTPConn(writer io.Writer) *HTTP2Conn { + return &HTTP2Conn{ + create: make(chan struct{}), + writer: writer, + } +} + +func (c *HTTP2Conn) Setup(reader io.Reader, err error) { + c.reader = reader + c.err = err + close(c.create) +} + +func (c *HTTP2Conn) Read(b []byte) (n int, err error) { + if c.reader == nil { + <-c.create + if c.err != nil { + return 0, c.err + } + } + n, err = c.reader.Read(b) + return n, baderror.WrapH2(err) +} + +func (c *HTTP2Conn) Write(b []byte) (n int, err error) { + n, err = c.writer.Write(b) + return n, baderror.WrapH2(err) +} + +func (c *HTTP2Conn) Close() error { + return common.Close(c.reader, c.writer) +} + +func (c *HTTP2Conn) LocalAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *HTTP2Conn) RemoteAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *HTTP2Conn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *HTTP2Conn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *HTTP2Conn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *HTTP2Conn) NeedAdditionalReadDeadline() bool { + return true +} + +type ServerHTTPConn struct { + HTTP2Conn + Flusher http.Flusher +} + +func (c *ServerHTTPConn) Write(b []byte) (n int, err error) { + n, err = c.writer.Write(b) + if err == nil { + c.Flusher.Flush() + } + return +} + +type HTTP2ConnWrapper struct { + N.ExtendedConn + access sync.Mutex + closed bool +} + +func NewHTTP2Wrapper(conn net.Conn) *HTTP2ConnWrapper { + return &HTTP2ConnWrapper{ + ExtendedConn: bufio.NewExtendedConn(conn), + } +} + +func (w *HTTP2ConnWrapper) Write(p []byte) (n int, err error) { + w.access.Lock() + defer w.access.Unlock() + if w.closed { + return 0, net.ErrClosed + } + return w.ExtendedConn.Write(p) +} + +func (w *HTTP2ConnWrapper) WriteBuffer(buffer *buf.Buffer) error { + w.access.Lock() + defer w.access.Unlock() + if w.closed { + return net.ErrClosed + } + return w.ExtendedConn.WriteBuffer(buffer) +} + +func (w *HTTP2ConnWrapper) CloseWrapper() { + w.access.Lock() + defer w.access.Unlock() + w.closed = true +} + +func (w *HTTP2ConnWrapper) Close() error { + w.CloseWrapper() + return w.ExtendedConn.Close() +} + +func (w *HTTP2ConnWrapper) Upstream() any { + return w.ExtendedConn +} + +func DupContext(ctx context.Context) context.Context { + id, loaded := log.IDFromContext(ctx) + if !loaded { + return context.Background() + } + return log.ContextWithID(context.Background(), id) +} diff --git a/transport/v2rayhttp/force_close.go b/transport/v2rayhttp/force_close.go new file mode 100644 index 00000000..d574a510 --- /dev/null +++ b/transport/v2rayhttp/force_close.go @@ -0,0 +1,47 @@ +package v2rayhttp + +import ( + "net/http" + "reflect" + "sync" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/net/http2" +) + +type clientConnPool struct { + t *http2.Transport + mu sync.Mutex + conns map[string][]*http2.ClientConn // key is host:port +} + +type efaceWords struct { + typ unsafe.Pointer + data unsafe.Pointer +} + +func ResetTransport(rawTransport http.RoundTripper) http.RoundTripper { + switch transport := rawTransport.(type) { + case *http.Transport: + transport.CloseIdleConnections() + return transport.Clone() + case *http2.Transport: + connPool := transportConnPool(transport) + p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) + p.mu.Lock() + defer p.mu.Unlock() + for _, vv := range p.conns { + for _, cc := range vv { + cc.Close() + } + } + return transport + default: + panic(E.New("unknown transport type: ", reflect.TypeOf(transport))) + } +} + +//go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool +func transportConnPool(t *http2.Transport) http2.ClientConnPool diff --git a/transport/v2rayhttp/pool.go b/transport/v2rayhttp/pool.go new file mode 100644 index 00000000..7e9ba64f --- /dev/null +++ b/transport/v2rayhttp/pool.go @@ -0,0 +1,13 @@ +package v2rayhttp + +import "net/http" + +type ConnectionPool interface { + CloseIdleConnections() +} + +func CloseIdleConnections(transport http.RoundTripper) { + if connectionPool, ok := transport.(ConnectionPool); ok { + connectionPool.CloseIdleConnections() + } +} diff --git a/transport/v2rayhttp/server.go b/transport/v2rayhttp/server.go new file mode 100644 index 00000000..282c7c23 --- /dev/null +++ b/transport/v2rayhttp/server.go @@ -0,0 +1,183 @@ +package v2rayhttp + +import ( + "context" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHttp "github.com/sagernet/sing/protocol/http" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + logger logger.ContextLogger + tlsConfig tls.ServerConfig + handler adapter.V2RayServerTransportHandler + httpServer *http.Server + h2Server *http2.Server + h2cHandler http.Handler + host []string + path string + method string + headers http.Header +} + +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { + server := &Server{ + ctx: ctx, + tlsConfig: tlsConfig, + logger: logger, + handler: handler, + h2Server: &http2.Server{ + IdleTimeout: time.Duration(options.IdleTimeout), + }, + host: options.Host, + path: options.Path, + method: options.Method, + headers: options.Headers.Build(), + } + if !strings.HasPrefix(server.path, "/") { + server.path = "/" + server.path + } + server.httpServer = &http.Server{ + Handler: server, + ReadHeaderTimeout: C.TCPTimeout, + MaxHeaderBytes: http.DefaultMaxHeaderBytes, + BaseContext: func(net.Listener) context.Context { + return ctx + }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + } + server.h2cHandler = h2c.NewHandler(server, server.h2Server) + return server, nil +} + +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" { + s.h2cHandler.ServeHTTP(writer, request) + return + } + host := request.Host + if len(s.host) > 0 && !common.Contains(s.host, host) { + s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host)) + return + } + if !strings.HasPrefix(request.URL.Path, s.path) { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + if s.method != "" && request.Method != s.method { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) + return + } + + writer.Header().Set("Cache-Control", "no-store") + + for key, values := range s.headers { + for _, value := range values { + writer.Header().Set(key, value) + } + } + + source := sHttp.SourceAddress(request) + if h, ok := writer.(http.Hijacker); ok { + var requestBody *buf.Buffer + if contentLength := int(request.ContentLength); contentLength > 0 { + requestBody = buf.NewSize(contentLength) + _, err := requestBody.ReadFullFrom(request.Body, contentLength) + if err != nil { + s.invalidRequest(writer, request, 0, E.Cause(err, "read request")) + return + } + } + writer.WriteHeader(http.StatusOK) + writer.(http.Flusher).Flush() + conn, reader, err := h.Hijack() + if err != nil { + s.invalidRequest(writer, request, 0, E.Cause(err, "hijack conn")) + return + } + if cacheLen := reader.Reader.Buffered(); cacheLen > 0 { + cache := buf.NewSize(cacheLen) + _, err = cache.ReadFullFrom(reader.Reader, cacheLen) + if err != nil { + conn.Close() + s.invalidRequest(writer, request, 0, E.Cause(err, "read cache")) + return + } + conn = bufio.NewCachedConn(conn, cache) + } + if requestBody != nil { + conn = bufio.NewCachedConn(conn, requestBody) + } + s.handler.NewConnectionEx(DupContext(request.Context()), conn, source, M.Socksaddr{}, nil) + } else { + writer.WriteHeader(http.StatusOK) + flusher := writer.(http.Flusher) + flusher.Flush() + done := make(chan struct{}) + conn := NewHTTP2Wrapper(&ServerHTTPConn{ + NewHTTPConn(request.Body, writer), + flusher, + }) + s.handler.NewConnectionEx(request.Context(), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) { + close(done) + })) + <-done + conn.CloseWrapper() + } +} + +func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { + if statusCode > 0 { + writer.WriteHeader(statusCode) + } + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func (s *Server) Serve(listener net.Listener) error { + if s.tlsConfig != nil { + if len(s.tlsConfig.NextProtos()) == 0 { + s.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) + } else if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, s.tlsConfig.NextProtos()...)) + } + listener = aTLS.NewListener(listener, s.tlsConfig) + } + return s.httpServer.Serve(listener) +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + +func (s *Server) Close() error { + return common.Close(common.PtrOrNil(s.httpServer)) +} diff --git a/transport/v2rayhttpupgrade/client.go b/transport/v2rayhttpupgrade/client.go new file mode 100644 index 00000000..f282d3f6 --- /dev/null +++ b/transport/v2rayhttpupgrade/client.go @@ -0,0 +1,115 @@ +package v2rayhttpupgrade + +import ( + std_bufio "bufio" + "context" + "net" + "net/http" + "net/url" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHTTP "github.com/sagernet/sing/protocol/http" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + dialer N.Dialer + serverAddr M.Socksaddr + requestURL url.URL + headers http.Header + host string +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.Config) (*Client, error) { + if tlsConfig != nil { + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{"http/1.1"}) + } + dialer = tls.NewDialer(dialer, tlsConfig) + } + var host string + if options.Host != "" { + host = options.Host + } else if tlsConfig != nil && tlsConfig.ServerName() != "" { + host = tlsConfig.ServerName() + } else { + host = serverAddr.String() + } + var requestURL url.URL + if tlsConfig == nil { + requestURL.Scheme = "http" + } else { + requestURL.Scheme = "https" + } + requestURL.Host = serverAddr.String() + requestURL.Path = options.Path + err := sHTTP.URLSetPath(&requestURL, options.Path) + if err != nil { + return nil, E.Cause(err, "parse path") + } + if !strings.HasPrefix(requestURL.Path, "/") { + requestURL.Path = "/" + requestURL.Path + } + headers := make(http.Header) + for key, value := range options.Headers { + headers[key] = value + } + return &Client{ + dialer: dialer, + serverAddr: serverAddr, + requestURL: requestURL, + headers: headers, + host: host, + }, nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) + if err != nil { + return nil, err + } + request := &http.Request{ + Method: http.MethodGet, + URL: &c.requestURL, + Header: c.headers.Clone(), + Host: c.host, + } + request.Header.Set("Connection", "Upgrade") + request.Header.Set("Upgrade", "websocket") + err = request.Write(conn) + if err != nil { + return nil, err + } + bufReader := std_bufio.NewReader(conn) + response, err := http.ReadResponse(bufReader, request) + if err != nil { + return nil, err + } + if response.StatusCode != 101 || + !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || + !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { + return nil, E.New("v2ray-http-upgrade: unexpected status: ", response.Status) + } + if bufReader.Buffered() > 0 { + buffer := buf.NewSize(bufReader.Buffered()) + _, err = buffer.ReadFullFrom(bufReader, buffer.Len()) + if err != nil { + return nil, err + } + conn = bufio.NewCachedConn(conn, buffer) + } + return conn, nil +} + +func (c *Client) Close() error { + return nil +} diff --git a/transport/v2rayhttpupgrade/server.go b/transport/v2rayhttpupgrade/server.go new file mode 100644 index 00000000..338b7248 --- /dev/null +++ b/transport/v2rayhttpupgrade/server.go @@ -0,0 +1,145 @@ +package v2rayhttpupgrade + +import ( + "context" + "net" + "net/http" + "os" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHttp "github.com/sagernet/sing/protocol/http" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + logger logger.ContextLogger + tlsConfig tls.ServerConfig + handler adapter.V2RayServerTransportHandler + httpServer *http.Server + host string + path string + headers http.Header +} + +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { + server := &Server{ + ctx: ctx, + logger: logger, + tlsConfig: tlsConfig, + handler: handler, + host: options.Host, + path: options.Path, + headers: options.Headers.Build(), + } + if !strings.HasPrefix(server.path, "/") { + server.path = "/" + server.path + } + server.httpServer = &http.Server{ + Handler: server, + ReadHeaderTimeout: C.TCPTimeout, + MaxHeaderBytes: http.DefaultMaxHeaderBytes, + BaseContext: func(net.Listener) context.Context { + return ctx + }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + TLSNextProto: make(map[string]func(*http.Server, *tls.STDConn, http.Handler)), + } + return server, nil +} + +type httpFlusher interface { + FlushError() error +} + +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + host := request.Host + if len(s.host) > 0 && host != s.host { + s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host)) + return + } + if request.URL.Path != s.path { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + if request.Method != http.MethodGet { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) + return + } + if !strings.EqualFold(request.Header.Get("Connection"), "upgrade") { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a upgrade request")) + return + } + if !strings.EqualFold(request.Header.Get("Upgrade"), "websocket") { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a websocket request")) + return + } + if request.Header.Get("Sec-WebSocket-Key") != "" { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("real websocket request received")) + return + } + writer.Header().Set("Connection", "upgrade") + writer.Header().Set("Upgrade", "websocket") + writer.WriteHeader(http.StatusSwitchingProtocols) + if flusher, isFlusher := writer.(httpFlusher); isFlusher { + err := flusher.FlushError() + if err != nil { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("flush response")) + } + } + hijacker, canHijack := writer.(http.Hijacker) + if !canHijack { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("invalid connection, maybe HTTP/2")) + return + } + conn, _, err := hijacker.Hijack() + if err != nil { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "hijack failed")) + return + } + s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, sHttp.SourceAddress(request), M.Socksaddr{}, nil) +} + +func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { + if statusCode > 0 { + writer.WriteHeader(statusCode) + } + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func (s *Server) Serve(listener net.Listener) error { + if s.tlsConfig != nil { + if len(s.tlsConfig.NextProtos()) == 0 { + s.tlsConfig.SetNextProtos([]string{"http/1.1"}) + } + listener = aTLS.NewListener(listener, s.tlsConfig) + } + return s.httpServer.Serve(listener) +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + +func (s *Server) Close() error { + return common.Close(common.PtrOrNil(s.httpServer)) +} diff --git a/transport/v2rayquic/client.go b/transport/v2rayquic/client.go new file mode 100644 index 00000000..3e0d8b81 --- /dev/null +++ b/transport/v2rayquic/client.go @@ -0,0 +1,110 @@ +//go:build with_quic + +package v2rayquic + +import ( + "context" + "net" + "sync" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + ctx context.Context + dialer N.Dialer + serverAddr M.Socksaddr + tlsConfig tls.Config + quicConfig *quic.Config + connAccess sync.Mutex + conn common.TypedValue[*quic.Conn] + rawConn net.Conn +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + quicConfig := &quic.Config{ + DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, + } + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{http3.NextProtoH3}) + } + return &Client{ + ctx: ctx, + dialer: dialer, + serverAddr: serverAddr, + tlsConfig: tlsConfig, + quicConfig: quicConfig, + }, nil +} + +func (c *Client) offer() (*quic.Conn, error) { + conn := c.conn.Load() + if conn != nil && !common.Done(conn.Context()) { + return conn, nil + } + c.connAccess.Lock() + defer c.connAccess.Unlock() + conn = c.conn.Load() + if conn != nil && !common.Done(conn.Context()) { + return conn, nil + } + conn, err := c.offerNew() + if err != nil { + return nil, err + } + return conn, nil +} + +func (c *Client) offerNew() (*quic.Conn, error) { + udpConn, err := c.dialer.DialContext(c.ctx, "udp", c.serverAddr) + if err != nil { + return nil, err + } + packetConn := bufio.NewUnbindPacketConn(udpConn) + quicConn, err := qtls.Dial(c.ctx, packetConn, udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig) + if err != nil { + packetConn.Close() + return nil, err + } + c.conn.Store(quicConn) + c.rawConn = udpConn + return quicConn, nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + conn, err := c.offer() + if err != nil { + return nil, err + } + stream, err := conn.OpenStream() + if err != nil { + return nil, err + } + return &StreamWrapper{Conn: conn, Stream: stream}, nil +} + +func (c *Client) Close() error { + c.connAccess.Lock() + defer c.connAccess.Unlock() + conn := c.conn.Swap(nil) + if conn != nil { + conn.CloseWithError(0, "") + } + if c.rawConn != nil { + c.rawConn.Close() + } + c.rawConn = nil + return nil +} diff --git a/transport/v2rayquic/init.go b/transport/v2rayquic/init.go new file mode 100644 index 00000000..83c7fc3d --- /dev/null +++ b/transport/v2rayquic/init.go @@ -0,0 +1,9 @@ +//go:build with_quic + +package v2rayquic + +import "github.com/sagernet/sing-box/transport/v2ray" + +func init() { + v2ray.RegisterQUICConstructor(NewServer, NewClient) +} diff --git a/transport/v2rayquic/server.go b/transport/v2rayquic/server.go new file mode 100644 index 00000000..c50d92f0 --- /dev/null +++ b/transport/v2rayquic/server.go @@ -0,0 +1,99 @@ +//go:build with_quic + +package v2rayquic + +import ( + "context" + "net" + "os" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + logger logger.ContextLogger + tlsConfig tls.ServerConfig + quicConfig *quic.Config + handler adapter.V2RayServerTransportHandler + udpListener net.PacketConn + quicListener qtls.Listener +} + +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + quicConfig := &quic.Config{ + DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, + } + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{http3.NextProtoH3}) + } + server := &Server{ + ctx: ctx, + logger: logger, + tlsConfig: tlsConfig, + quicConfig: quicConfig, + handler: handler, + } + return server, nil +} + +func (s *Server) Network() []string { + return []string{N.NetworkUDP} +} + +func (s *Server) Serve(listener net.Listener) error { + return os.ErrInvalid +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + quicListener, err := qtls.Listen(listener, s.tlsConfig, s.quicConfig) + if err != nil { + return err + } + s.udpListener = listener + s.quicListener = quicListener + go s.acceptLoop() + return nil +} + +func (s *Server) acceptLoop() { + for { + conn, err := s.quicListener.Accept(s.ctx) + if err != nil { + return + } + go func() { + hErr := s.streamAcceptLoop(conn) + if hErr != nil && !E.IsClosedOrCanceled(hErr) { + s.logger.ErrorContext(conn.Context(), hErr) + } + }() + } +} + +func (s *Server) streamAcceptLoop(conn *quic.Conn) error { + for { + stream, err := conn.AcceptStream(s.ctx) + if err != nil { + return qtls.WrapError(err) + } + go s.handler.NewConnectionEx(conn.Context(), &StreamWrapper{Conn: conn, Stream: stream}, M.SocksaddrFromNet(conn.RemoteAddr()), M.Socksaddr{}, nil) + } +} + +func (s *Server) Close() error { + return common.Close(s.udpListener, s.quicListener) +} diff --git a/transport/v2rayquic/stream.go b/transport/v2rayquic/stream.go new file mode 100644 index 00000000..aad62afb --- /dev/null +++ b/transport/v2rayquic/stream.go @@ -0,0 +1,41 @@ +package v2rayquic + +import ( + "net" + + "github.com/sagernet/quic-go" + qtls "github.com/sagernet/sing-quic" +) + +type StreamWrapper struct { + Conn *quic.Conn + *quic.Stream +} + +func (s *StreamWrapper) Read(p []byte) (n int, err error) { + n, err = s.Stream.Read(p) + return n, qtls.WrapError(err) +} + +func (s *StreamWrapper) Write(p []byte) (n int, err error) { + n, err = s.Stream.Write(p) + return n, qtls.WrapError(err) +} + +func (s *StreamWrapper) LocalAddr() net.Addr { + return s.Conn.LocalAddr() +} + +func (s *StreamWrapper) RemoteAddr() net.Addr { + return s.Conn.RemoteAddr() +} + +func (s *StreamWrapper) Upstream() any { + return s.Stream +} + +func (s *StreamWrapper) Close() error { + s.CancelRead(0) + s.Stream.Close() + return nil +} diff --git a/transport/v2raywebsocket/client.go b/transport/v2raywebsocket/client.go new file mode 100644 index 00000000..e5630109 --- /dev/null +++ b/transport/v2raywebsocket/client.go @@ -0,0 +1,123 @@ +package v2raywebsocket + +import ( + "context" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHTTP "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/ws" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + dialer N.Dialer + serverAddr M.Socksaddr + requestURL url.URL + headers http.Header + maxEarlyData uint32 + earlyDataHeaderName string +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { + if tlsConfig != nil { + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{"http/1.1"}) + } + dialer = tls.NewDialer(dialer, tlsConfig) + } + var requestURL url.URL + if tlsConfig == nil { + requestURL.Scheme = "ws" + } else { + requestURL.Scheme = "wss" + } + requestURL.Host = serverAddr.String() + requestURL.Path = options.Path + err := sHTTP.URLSetPath(&requestURL, options.Path) + if err != nil { + return nil, E.Cause(err, "parse path") + } + if !strings.HasPrefix(requestURL.Path, "/") { + requestURL.Path = "/" + requestURL.Path + } + headers := options.Headers.Build() + if host := headers.Get("Host"); host != "" { + headers.Del("Host") + requestURL.Host = host + } + if headers.Get("User-Agent") == "" { + headers.Set("User-Agent", "Go-http-client/1.1") + } + return &Client{ + dialer, + serverAddr, + requestURL, + headers, + options.MaxEarlyData, + options.EarlyDataHeaderName, + }, nil +} + +func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers http.Header) (*WebsocketConn, error) { + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) + if err != nil { + return nil, err + } + var deadlineConn net.Conn + if deadline.NeedAdditionalReadDeadline(conn) { + deadlineConn = deadline.NewConn(conn) + } else { + deadlineConn = conn + } + deadlineConn.SetDeadline(time.Now().Add(C.TCPTimeout)) + var protocols []string + if protocolHeader := headers.Get("Sec-WebSocket-Protocol"); protocolHeader != "" { + protocols = []string{protocolHeader} + headers.Del("Sec-WebSocket-Protocol") + } + reader, _, err := ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers), Protocols: protocols}.Upgrade(deadlineConn, requestURL) + deadlineConn.SetDeadline(time.Time{}) + if err != nil { + return nil, err + } + if reader != nil { + buffer := buf.NewSize(reader.Buffered()) + _, err = buffer.ReadFullFrom(reader, buffer.Len()) + if err != nil { + return nil, err + } + conn = bufio.NewCachedConn(conn, buffer) + } + return NewConn(conn, nil, ws.StateClientSide), nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + if c.maxEarlyData <= 0 { + conn, err := c.dialContext(ctx, &c.requestURL, c.headers) + if err != nil { + return nil, err + } + return conn, nil + } else { + return &EarlyWebsocketConn{Client: c, ctx: ctx, create: make(chan struct{})}, nil + } +} + +func (c *Client) Close() error { + return nil +} diff --git a/transport/v2raywebsocket/conn.go b/transport/v2raywebsocket/conn.go new file mode 100644 index 00000000..cb788fc9 --- /dev/null +++ b/transport/v2raywebsocket/conn.go @@ -0,0 +1,306 @@ +package v2raywebsocket + +import ( + "context" + "encoding/base64" + "errors" + "io" + "net" + "os" + "sync" + "sync/atomic" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/debug" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" +) + +type WebsocketConn struct { + net.Conn + *Writer + state ws.State + reader *wsutil.Reader + controlHandler wsutil.FrameHandlerFunc + remoteAddr net.Addr +} + +func NewConn(conn net.Conn, remoteAddr net.Addr, state ws.State) *WebsocketConn { + controlHandler := wsutil.ControlFrameHandler(conn, state) + return &WebsocketConn{ + Conn: conn, + state: state, + reader: &wsutil.Reader{ + Source: conn, + State: state, + SkipHeaderCheck: !debug.Enabled, + OnIntermediate: controlHandler, + }, + controlHandler: controlHandler, + remoteAddr: remoteAddr, + Writer: NewWriter(conn, state), + } +} + +func (c *WebsocketConn) Close() error { + c.Conn.SetWriteDeadline(time.Now().Add(C.TCPTimeout)) + frame := ws.NewCloseFrame(ws.NewCloseFrameBody( + ws.StatusNormalClosure, "", + )) + if c.state == ws.StateClientSide { + frame = ws.MaskFrameInPlace(frame) + } + ws.WriteFrame(c.Conn, frame) + c.Conn.Close() + return nil +} + +func (c *WebsocketConn) Read(b []byte) (n int, err error) { + var header ws.Header + for { + n, err = c.reader.Read(b) + if n > 0 { + err = nil + return + } + if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) { + err = wrapWsError(err) + return + } + header, err = wrapWsError0(c.reader.NextFrame()) + if err != nil { + return + } + if header.OpCode.IsControl() { + if header.Length > 128 { + err = wsutil.ErrFrameTooLarge + return + } + err = wrapWsError(c.controlHandler(header, c.reader)) + if err != nil { + return + } + continue + } + if header.OpCode&ws.OpBinary == 0 { + err = wrapWsError(c.reader.Discard()) + if err != nil { + return + } + continue + } + } +} + +func (c *WebsocketConn) Write(p []byte) (n int, err error) { + err = wrapWsError(wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p)) + if err != nil { + return + } + n = len(p) + return +} + +func (c *WebsocketConn) RemoteAddr() net.Addr { + if c.remoteAddr != nil { + return c.remoteAddr + } + return c.Conn.RemoteAddr() +} + +func (c *WebsocketConn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *WebsocketConn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *WebsocketConn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *WebsocketConn) NeedAdditionalReadDeadline() bool { + return true +} + +func (c *WebsocketConn) Upstream() any { + return c.Conn +} + +type EarlyWebsocketConn struct { + *Client + ctx context.Context + conn atomic.Pointer[WebsocketConn] + access sync.Mutex + create chan struct{} + err error +} + +func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) { + conn := c.conn.Load() + if conn == nil { + <-c.create + if c.err != nil { + return 0, c.err + } + conn = c.conn.Load() + } + return wrapWsError0(conn.Read(b)) +} + +func (c *EarlyWebsocketConn) writeRequest(content []byte) error { + var ( + earlyData []byte + lateData []byte + conn *WebsocketConn + err error + ) + if len(content) > int(c.maxEarlyData) { + earlyData = content[:c.maxEarlyData] + lateData = content[c.maxEarlyData:] + } else { + earlyData = content + } + if len(earlyData) > 0 { + earlyDataString := base64.RawURLEncoding.EncodeToString(earlyData) + if c.earlyDataHeaderName == "" { + requestURL := c.requestURL + requestURL.Path += earlyDataString + conn, err = c.dialContext(c.ctx, &requestURL, c.headers) + } else { + headers := c.headers.Clone() + headers.Set(c.earlyDataHeaderName, earlyDataString) + conn, err = c.dialContext(c.ctx, &c.requestURL, headers) + } + } else { + conn, err = c.dialContext(c.ctx, &c.requestURL, c.headers) + } + if err != nil { + return err + } + if len(lateData) > 0 { + _, err = conn.Write(lateData) + if err != nil { + return err + } + } + c.conn.Store(conn) + return nil +} + +func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) { + conn := c.conn.Load() + if conn != nil { + return wrapWsError0(conn.Write(b)) + } + c.access.Lock() + defer c.access.Unlock() + conn = c.conn.Load() + if c.err != nil { + return 0, c.err + } + if conn != nil { + return wrapWsError0(conn.Write(b)) + } + err = c.writeRequest(b) + c.err = err + close(c.create) + if err != nil { + return + } + return len(b), nil +} + +func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error { + conn := c.conn.Load() + if conn != nil { + return wrapWsError(conn.WriteBuffer(buffer)) + } + c.access.Lock() + defer c.access.Unlock() + if c.err != nil { + return c.err + } + conn = c.conn.Load() + if conn != nil { + return wrapWsError(conn.WriteBuffer(buffer)) + } + err := c.writeRequest(buffer.Bytes()) + c.err = err + close(c.create) + return err +} + +func (c *EarlyWebsocketConn) Close() error { + conn := c.conn.Load() + if conn == nil { + return nil + } + return conn.Close() +} + +func (c *EarlyWebsocketConn) LocalAddr() net.Addr { + conn := c.conn.Load() + if conn == nil { + return M.Socksaddr{} + } + return conn.LocalAddr() +} + +func (c *EarlyWebsocketConn) RemoteAddr() net.Addr { + conn := c.conn.Load() + if conn == nil { + return M.Socksaddr{} + } + return conn.RemoteAddr() +} + +func (c *EarlyWebsocketConn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *EarlyWebsocketConn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *EarlyWebsocketConn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *EarlyWebsocketConn) NeedAdditionalReadDeadline() bool { + return true +} + +func (c *EarlyWebsocketConn) Upstream() any { + return common.PtrOrNil(c.conn.Load()) +} + +func (c *EarlyWebsocketConn) LazyHeadroom() bool { + return c.conn.Load() == nil +} + +func wrapWsError(err error) error { + if err == nil { + return nil + } + var closedErr wsutil.ClosedError + if errors.As(err, &closedErr) { + if closedErr.Code == ws.StatusNormalClosure || closedErr.Code == ws.StatusNoStatusRcvd { + err = io.EOF + } + } + return err +} + +func wrapWsError0[T any](value T, err error) (T, error) { + if err == nil { + return value, nil + } + return value, wrapWsError(err) +} diff --git a/transport/v2raywebsocket/server.go b/transport/v2raywebsocket/server.go new file mode 100644 index 00000000..b54d760a --- /dev/null +++ b/transport/v2raywebsocket/server.go @@ -0,0 +1,145 @@ +package v2raywebsocket + +import ( + "context" + "encoding/base64" + "net" + "net/http" + "os" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHttp "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/ws" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + logger logger.ContextLogger + tlsConfig tls.ServerConfig + handler adapter.V2RayServerTransportHandler + httpServer *http.Server + path string + maxEarlyData uint32 + earlyDataHeaderName string + upgrader ws.HTTPUpgrader +} + +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { + server := &Server{ + ctx: ctx, + logger: logger, + tlsConfig: tlsConfig, + handler: handler, + path: options.Path, + maxEarlyData: options.MaxEarlyData, + earlyDataHeaderName: options.EarlyDataHeaderName, + upgrader: ws.HTTPUpgrader{ + Timeout: C.TCPTimeout, + Header: options.Headers.Build(), + }, + } + if !strings.HasPrefix(server.path, "/") { + server.path = "/" + server.path + } + server.httpServer = &http.Server{ + Handler: server, + ReadHeaderTimeout: C.TCPTimeout, + MaxHeaderBytes: http.DefaultMaxHeaderBytes, + BaseContext: func(net.Listener) context.Context { + return ctx + }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + } + return server, nil +} + +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if s.maxEarlyData == 0 || s.earlyDataHeaderName != "" { + if request.URL.Path != s.path { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + } + var ( + earlyData []byte + err error + conn net.Conn + ) + if s.earlyDataHeaderName == "" { + if strings.HasPrefix(request.URL.RequestURI(), s.path) { + earlyDataStr := request.URL.RequestURI()[len(s.path):] + earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr) + } else { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + } else { + if request.URL.Path != s.path { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + earlyDataStr := request.Header.Get(s.earlyDataHeaderName) + if earlyDataStr != "" { + earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr) + } + } + if err != nil { + s.invalidRequest(writer, request, http.StatusBadRequest, E.Cause(err, "decode early data")) + return + } + wsConn, _, _, err := ws.UpgradeHTTP(request, writer) + if err != nil { + s.invalidRequest(writer, request, 0, E.Cause(err, "upgrade websocket connection")) + return + } + source := sHttp.SourceAddress(request) + conn = NewConn(wsConn, source, ws.StateServerSide) + if len(earlyData) > 0 { + conn = bufio.NewCachedConn(conn, buf.As(earlyData)) + } + s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, source, M.Socksaddr{}, nil) +} + +func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { + if statusCode > 0 { + writer.WriteHeader(statusCode) + } + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func (s *Server) Serve(listener net.Listener) error { + if s.tlsConfig != nil { + listener = aTLS.NewListener(listener, s.tlsConfig) + } + return s.httpServer.Serve(listener) +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + +func (s *Server) Close() error { + return common.Close(common.PtrOrNil(s.httpServer)) +} diff --git a/transport/v2raywebsocket/writer.go b/transport/v2raywebsocket/writer.go new file mode 100644 index 00000000..cd5a5d6a --- /dev/null +++ b/transport/v2raywebsocket/writer.go @@ -0,0 +1,74 @@ +package v2raywebsocket + +import ( + "encoding/binary" + "io" + "math/rand" + + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/ws" +) + +type Writer struct { + writer N.ExtendedWriter + isServer bool +} + +func NewWriter(writer io.Writer, state ws.State) *Writer { + return &Writer{ + bufio.NewExtendedWriter(writer), + state == ws.StateServerSide, + } +} + +func (w *Writer) WriteBuffer(buffer *buf.Buffer) error { + var payloadBitLength int + dataLen := buffer.Len() + data := buffer.Bytes() + if dataLen < 126 { + payloadBitLength = 1 + } else if dataLen < 65536 { + payloadBitLength = 3 + } else { + payloadBitLength = 9 + } + + var headerLen int + headerLen += 1 // FIN / RSV / OPCODE + headerLen += payloadBitLength + if !w.isServer { + headerLen += 4 // MASK KEY + } + + header := buffer.ExtendHeader(headerLen) + header[0] = byte(ws.OpBinary) | 0x80 + if w.isServer { + header[1] = 0 + } else { + header[1] = 1 << 7 + } + + if dataLen < 126 { + header[1] |= byte(dataLen) + } else if dataLen < 65536 { + header[1] |= 126 + binary.BigEndian.PutUint16(header[2:], uint16(dataLen)) + } else { + header[1] |= 127 + binary.BigEndian.PutUint64(header[2:], uint64(dataLen)) + } + + if !w.isServer { + maskKey := rand.Uint32() + binary.BigEndian.PutUint32(header[1+payloadBitLength:], maskKey) + ws.Cipher(data, [4]byte(header[1+payloadBitLength:]), 0) + } + + return wrapWsError(w.writer.WriteBuffer(buffer)) +} + +func (w *Writer) FrontHeadroom() int { + return 14 +} diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go new file mode 100644 index 00000000..54b7be86 --- /dev/null +++ b/transport/wireguard/client_bind.go @@ -0,0 +1,262 @@ +package wireguard + +import ( + "context" + "net" + "net/netip" + "sync" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" + "github.com/sagernet/wireguard-go/conn" +) + +var _ conn.Bind = (*ClientBind)(nil) + +type ClientBind struct { + ctx context.Context + logger logger.Logger + pauseManager pause.Manager + bindCtx context.Context + bindDone context.CancelFunc + dialer N.Dialer + reservedForEndpoint map[netip.AddrPort][3]uint8 + connAccess sync.Mutex + conn *wireConn + done chan struct{} + isConnect bool + connectAddr netip.AddrPort + reserved [3]uint8 +} + +func NewClientBind(ctx context.Context, logger logger.Logger, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind { + return &ClientBind{ + ctx: ctx, + logger: logger, + pauseManager: service.FromContext[pause.Manager](ctx), + dialer: dialer, + reservedForEndpoint: make(map[netip.AddrPort][3]uint8), + done: make(chan struct{}), + isConnect: isConnect, + connectAddr: connectAddr, + reserved: reserved, + } +} + +func (c *ClientBind) connect() (*wireConn, error) { + serverConn := c.conn + if serverConn != nil { + select { + case <-serverConn.done: + serverConn = nil + default: + return serverConn, nil + } + } + c.connAccess.Lock() + defer c.connAccess.Unlock() + select { + case <-c.done: + return nil, net.ErrClosed + default: + } + serverConn = c.conn + if serverConn != nil { + select { + case <-serverConn.done: + serverConn = nil + default: + return serverConn, nil + } + } + if c.isConnect { + udpConn, err := c.dialer.DialContext(c.bindCtx, N.NetworkUDP, M.SocksaddrFromNetIP(c.connectAddr)) + if err != nil { + return nil, err + } + c.conn = &wireConn{ + PacketConn: bufio.NewUnbindPacketConn(udpConn), + done: make(chan struct{}), + } + } else { + udpConn, err := c.dialer.ListenPacket(c.bindCtx, M.Socksaddr{Addr: netip.IPv4Unspecified()}) + if err != nil { + return nil, err + } + c.conn = &wireConn{ + PacketConn: bufio.NewPacketConn(udpConn), + done: make(chan struct{}), + } + } + return c.conn, nil +} + +func (c *ClientBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) { + select { + case <-c.done: + c.done = make(chan struct{}) + default: + } + c.bindCtx, c.bindDone = context.WithCancel(c.ctx) + return []conn.ReceiveFunc{c.receive}, 0, nil +} + +func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) (count int, err error) { + udpConn, err := c.connect() + if err != nil { + select { + case <-c.done: + return + default: + } + c.logger.Error(E.Cause(err, "connect to server")) + err = nil + c.pauseManager.WaitActive() + time.Sleep(time.Second) + return + } + n, addr, err := udpConn.ReadFrom(packets[0]) + if err != nil { + udpConn.Close() + select { + case <-c.done: + default: + c.logger.Error(E.Cause(err, "read packet")) + err = nil + } + return + } + sizes[0] = n + if n > 3 { + b := packets[0] + common.ClearArray(b[1:4]) + } + eps[0] = remoteEndpoint(M.SocksaddrFromNet(addr).Unwrap().AddrPort()) + count = 1 + return +} + +func (c *ClientBind) Close() error { + select { + case <-c.done: + default: + close(c.done) + } + if c.bindDone != nil { + c.bindDone() + } + c.connAccess.Lock() + defer c.connAccess.Unlock() + common.Close(common.PtrOrNil(c.conn)) + return nil +} + +func (c *ClientBind) SetMark(mark uint32) error { + return nil +} + +func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint, offset int) error { + udpConn, err := c.connect() + if err != nil { + c.pauseManager.WaitActive() + time.Sleep(time.Second) + return err + } + destination := netip.AddrPort(ep.(remoteEndpoint)) + for _, buf := range bufs { + if offset > 0 { + buf = buf[offset:] + } + if len(buf) > 3 { + reserved, loaded := c.reservedForEndpoint[destination] + if !loaded { + reserved = c.reserved + } + copy(buf[1:4], reserved[:]) + } + _, err = udpConn.WriteToUDPAddrPort(buf, destination) + if err != nil { + udpConn.Close() + return err + } + } + return nil +} + +func (c *ClientBind) ParseEndpoint(s string) (conn.Endpoint, error) { + ap, err := netip.ParseAddrPort(s) + if err != nil { + return nil, err + } + return remoteEndpoint(ap), nil +} + +func (c *ClientBind) BatchSize() int { + return 1 +} + +func (c *ClientBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) { + c.reservedForEndpoint[destination] = reserved +} + +type wireConn struct { + net.PacketConn + conn net.Conn + access sync.Mutex + done chan struct{} +} + +func (w *wireConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { + if w.conn != nil { + return w.conn.Write(b) + } + return w.PacketConn.WriteTo(b, M.SocksaddrFromNetIP(addr).UDPAddr()) +} + +func (w *wireConn) Close() error { + w.access.Lock() + defer w.access.Unlock() + select { + case <-w.done: + return net.ErrClosed + default: + } + w.PacketConn.Close() + close(w.done) + return nil +} + +var _ conn.Endpoint = (*remoteEndpoint)(nil) + +type remoteEndpoint netip.AddrPort + +func (e remoteEndpoint) ClearSrc() { +} + +func (e remoteEndpoint) SrcToString() string { + return "" +} + +func (e remoteEndpoint) DstToString() string { + return (netip.AddrPort)(e).String() +} + +func (e remoteEndpoint) DstToBytes() []byte { + b, _ := (netip.AddrPort)(e).MarshalBinary() + return b +} + +func (e remoteEndpoint) DstIP() netip.Addr { + return (netip.AddrPort)(e).Addr() +} + +func (e remoteEndpoint) SrcIP() netip.Addr { + return netip.Addr{} +} diff --git a/transport/wireguard/device.go b/transport/wireguard/device.go new file mode 100644 index 00000000..4dd615c5 --- /dev/null +++ b/transport/wireguard/device.go @@ -0,0 +1,51 @@ +package wireguard + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/wireguard-go/device" + wgTun "github.com/sagernet/wireguard-go/tun" +) + +type Device interface { + wgTun.Device + N.Dialer + Start() error + SetDevice(device *device.Device) + Inet4Address() netip.Addr + Inet6Address() netip.Addr +} + +type DeviceOptions struct { + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + CreateDialer func(interfaceName string) N.Dialer + Name string + MTU uint32 + Address []netip.Prefix + AllowedAddress []netip.Prefix +} + +func NewDevice(options DeviceOptions) (Device, error) { + if !options.System { + return newStackDevice(options) + } else if !tun.WithGVisor { + return newSystemDevice(options) + } else { + return newSystemStackDevice(options) + } +} + +type NatDevice interface { + Device + CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) +} diff --git a/transport/wireguard/device_nat.go b/transport/wireguard/device_nat.go new file mode 100644 index 00000000..d214b737 --- /dev/null +++ b/transport/wireguard/device_nat.go @@ -0,0 +1,103 @@ +package wireguard + +import ( + "context" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/logger" +) + +var _ Device = (*natDeviceWrapper)(nil) + +type natDeviceWrapper struct { + Device + ctx context.Context + logger logger.ContextLogger + packetOutbound chan *buf.Buffer + rewriter *ping.SourceRewriter + buffer [][]byte +} + +func NewNATDevice(ctx context.Context, logger logger.ContextLogger, upstream Device) NatDevice { + wrapper := &natDeviceWrapper{ + Device: upstream, + ctx: ctx, + logger: logger, + packetOutbound: make(chan *buf.Buffer, 256), + rewriter: ping.NewSourceRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()), + } + return wrapper +} + +func (d *natDeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) { + select { + case packet := <-d.packetOutbound: + defer packet.Release() + sizes[0] = copy(bufs[0][offset:], packet.Bytes()) + return 1, nil + default: + } + return d.Device.Read(bufs, sizes, offset) +} + +func (d *natDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) { + for _, buffer := range bufs { + handled, err := d.rewriter.WriteBack(buffer[offset:]) + if handled { + if err != nil { + return 0, err + } + } else { + d.buffer = append(d.buffer, buffer) + } + } + if len(d.buffer) > 0 { + _, err := d.Device.Write(d.buffer, offset) + if err != nil { + return 0, err + } + d.buffer = d.buffer[:0] + } + return 0, nil +} + +func (d *natDeviceWrapper) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + ctx := log.ContextWithNewID(d.ctx) + session := tun.DirectRouteSession{ + Source: metadata.Source.Addr, + Destination: metadata.Destination.Addr, + } + d.rewriter.CreateSession(session, routeContext) + d.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) + return &natDestination{device: d, session: session}, nil +} + +var _ tun.DirectRouteDestination = (*natDestination)(nil) + +type natDestination struct { + device *natDeviceWrapper + session tun.DirectRouteSession + closed atomic.Bool +} + +func (d *natDestination) WritePacket(buffer *buf.Buffer) error { + d.device.rewriter.RewritePacket(buffer.Bytes()) + d.device.packetOutbound <- buffer + return nil +} + +func (d *natDestination) Close() error { + d.closed.Store(true) + d.device.rewriter.DeleteSession(d.session) + return nil +} + +func (d *natDestination) IsClosed() bool { + return d.closed.Load() +} diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go new file mode 100644 index 00000000..a190baba --- /dev/null +++ b/transport/wireguard/device_stack.go @@ -0,0 +1,342 @@ +//go:build with_gvisor + +package wireguard + +import ( + "context" + "net" + "net/netip" + "os" + "time" + + "github.com/sagernet/gvisor/pkg/buffer" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/wireguard-go/device" + wgTun "github.com/sagernet/wireguard-go/tun" +) + +var _ NatDevice = (*stackDevice)(nil) + +type stackDevice struct { + ctx context.Context + logger log.ContextLogger + stack *stack.Stack + mtu uint32 + events chan wgTun.Event + outbound chan *stack.PacketBuffer + packetOutbound chan *buf.Buffer + done chan struct{} + dispatcher stack.NetworkDispatcher + inet4Address netip.Addr + inet6Address netip.Addr +} + +func newStackDevice(options DeviceOptions) (*stackDevice, error) { + tunDevice := &stackDevice{ + ctx: options.Context, + logger: options.Logger, + mtu: options.MTU, + events: make(chan wgTun.Event, 1), + outbound: make(chan *stack.PacketBuffer, 256), + packetOutbound: make(chan *buf.Buffer, 256), + done: make(chan struct{}), + } + ipStack, err := tun.NewGVisorStackWithOptions((*wireEndpoint)(tunDevice), stack.NICOptions{}, true) + if err != nil { + return nil, err + } + var ( + inet4Address netip.Addr + inet6Address netip.Addr + ) + for _, prefix := range options.Address { + addr := tun.AddressFromAddr(prefix.Addr()) + protoAddr := tcpip.ProtocolAddress{ + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: addr, + PrefixLen: prefix.Bits(), + }, + } + if prefix.Addr().Is4() { + inet4Address = prefix.Addr() + tunDevice.inet4Address = inet4Address + protoAddr.Protocol = ipv4.ProtocolNumber + } else { + inet6Address = prefix.Addr() + tunDevice.inet6Address = inet6Address + protoAddr.Protocol = ipv6.ProtocolNumber + } + gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) + if gErr != nil { + return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String()) + } + } + tunDevice.stack = ipStack + if options.Handler != nil { + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) + icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout) + icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + } + return tunDevice, nil +} + +func (w *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + addr := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + Port: destination.Port, + Addr: tun.AddressFromAddr(destination.Addr), + } + bind := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + if !w.inet4Address.IsValid() { + return nil, E.New("missing IPv4 local address") + } + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = tun.AddressFromAddr(w.inet4Address) + } else { + if !w.inet6Address.IsValid() { + return nil, E.New("missing IPv6 local address") + } + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = tun.AddressFromAddr(w.inet6Address) + } + switch N.NetworkName(network) { + case N.NetworkTCP: + tcpConn, err := DialTCPWithBind(ctx, w.stack, bind, addr, networkProtocol) + if err != nil { + return nil, err + } + return tcpConn, nil + case N.NetworkUDP: + udpConn, err := gonet.DialUDP(w.stack, &bind, &addr, networkProtocol) + if err != nil { + return nil, err + } + return udpConn, nil + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + bind := tcpip.FullAddress{ + NIC: tun.DefaultNIC, + } + var networkProtocol tcpip.NetworkProtocolNumber + if destination.IsIPv4() { + networkProtocol = header.IPv4ProtocolNumber + bind.Addr = tun.AddressFromAddr(w.inet4Address) + } else { + networkProtocol = header.IPv6ProtocolNumber + bind.Addr = tun.AddressFromAddr(w.inet4Address) + } + udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol) + if err != nil { + return nil, err + } + return udpConn, nil +} + +func (w *stackDevice) Inet4Address() netip.Addr { + return w.inet4Address +} + +func (w *stackDevice) Inet6Address() netip.Addr { + return w.inet6Address +} + +func (w *stackDevice) SetDevice(device *device.Device) { +} + +func (w *stackDevice) Start() error { + w.events <- wgTun.EventUp + return nil +} + +func (w *stackDevice) File() *os.File { + return nil +} + +func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { + select { + case packet, ok := <-w.outbound: + if !ok { + return 0, os.ErrClosed + } + defer packet.DecRef() + var copyN int + /*rangeIterate(packet.Data().AsRange(), func(view *buffer.View) { + copyN += copy(bufs[0][offset+copyN:], view.AsSlice()) + })*/ + for _, view := range packet.AsSlices() { + copyN += copy(bufs[0][offset+copyN:], view) + } + sizes[0] = copyN + return 1, nil + case packet := <-w.packetOutbound: + defer packet.Release() + sizes[0] = copy(bufs[0][offset:], packet.Bytes()) + return 1, nil + case <-w.done: + return 0, os.ErrClosed + } +} + +func (w *stackDevice) Write(bufs [][]byte, offset int) (count int, err error) { + for _, b := range bufs { + b = b[offset:] + if len(b) == 0 { + continue + } + var networkProtocol tcpip.NetworkProtocolNumber + switch header.IPVersion(b) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + } + packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(b), + }) + w.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer) + packetBuffer.DecRef() + count++ + } + return +} + +func (w *stackDevice) Flush() error { + return nil +} + +func (w *stackDevice) MTU() (int, error) { + return int(w.mtu), nil +} + +func (w *stackDevice) Name() (string, error) { + return "sing-box", nil +} + +func (w *stackDevice) Events() <-chan wgTun.Event { + return w.events +} + +func (w *stackDevice) Close() error { + close(w.done) + close(w.events) + w.stack.Close() + for _, endpoint := range w.stack.CleanupEndpoints() { + endpoint.Abort() + } + w.stack.Wait() + return nil +} + +func (w *stackDevice) BatchSize() int { + return 1 +} + +func (w *stackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + ctx := log.ContextWithNewID(w.ctx) + destination, err := ping.ConnectGVisor( + ctx, w.logger, + metadata.Source.Addr, metadata.Destination.Addr, + routeContext, + w.stack, + w.inet4Address, w.inet6Address, + timeout, + ) + if err != nil { + return nil, err + } + w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) + return destination, nil +} + +var _ stack.LinkEndpoint = (*wireEndpoint)(nil) + +type wireEndpoint stackDevice + +func (ep *wireEndpoint) MTU() uint32 { + return ep.mtu +} + +func (ep *wireEndpoint) SetMTU(mtu uint32) { +} + +func (ep *wireEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (ep *wireEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { +} + +func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + ep.dispatcher = dispatcher +} + +func (ep *wireEndpoint) IsAttached() bool { + return ep.dispatcher != nil +} + +func (ep *wireEndpoint) Wait() { +} + +func (ep *wireEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (ep *wireEndpoint) AddHeader(buffer *stack.PacketBuffer) { +} + +func (ep *wireEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (ep *wireEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { + for _, packetBuffer := range list.AsSlice() { + packetBuffer.IncRef() + select { + case <-ep.done: + return 0, &tcpip.ErrClosedForSend{} + case ep.outbound <- packetBuffer: + } + } + return list.Len(), nil +} + +func (ep *wireEndpoint) Close() { +} + +func (ep *wireEndpoint) SetOnCloseAction(f func()) { +} diff --git a/transport/wireguard/device_stack_gonet.go b/transport/wireguard/device_stack_gonet.go new file mode 100644 index 00000000..bc695205 --- /dev/null +++ b/transport/wireguard/device_stack_gonet.go @@ -0,0 +1,79 @@ +//go:build with_gvisor + +package wireguard + +import ( + "context" + "errors" + "fmt" + "net" + "net/netip" + "time" + + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" + "github.com/sagernet/gvisor/pkg/waiter" + "github.com/sagernet/sing-tun" + M "github.com/sagernet/sing/common/metadata" +) + +func DialTCPWithBind(ctx context.Context, s *stack.Stack, localAddr, remoteAddr tcpip.FullAddress, network tcpip.NetworkProtocolNumber) (*gonet.TCPConn, error) { + // Create TCP endpoint, then connect. + var wq waiter.Queue + ep, err := s.NewEndpoint(tcp.ProtocolNumber, network, &wq) + if err != nil { + return nil, errors.New(err.String()) + } + + // Create wait queue entry that notifies a channel. + // + // We do this unconditionally as Connect will always return an error. + waitEntry, notifyCh := waiter.NewChannelEntry(waiter.WritableEvents) + wq.EventRegister(&waitEntry) + defer wq.EventUnregister(&waitEntry) + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Bind before connect if requested. + if localAddr != (tcpip.FullAddress{}) { + if err = ep.Bind(localAddr); err != nil { + return nil, fmt.Errorf("ep.Bind(%+v) = %s", localAddr, err) + } + } + + err = ep.Connect(remoteAddr) + if _, ok := err.(*tcpip.ErrConnectStarted); ok { + select { + case <-ctx.Done(): + ep.Close() + return nil, ctx.Err() + case <-notifyCh: + } + + err = ep.LastError() + } + if err != nil { + ep.Close() + return nil, &net.OpError{ + Op: "connect", + Net: "tcp", + Addr: M.SocksaddrFromNetIP(netip.AddrPortFrom(tun.AddrFromAddress(remoteAddr.Addr), remoteAddr.Port)).TCPAddr(), + Err: errors.New(err.String()), + } + } + + // sing-box added: set keepalive + ep.SocketOptions().SetKeepAlive(true) + keepAliveIdle := tcpip.KeepaliveIdleOption(15 * time.Second) + ep.SetSockOpt(&keepAliveIdle) + keepAliveInterval := tcpip.KeepaliveIntervalOption(15 * time.Second) + ep.SetSockOpt(&keepAliveInterval) + + return gonet.NewTCPConn(&wq, ep), nil +} diff --git a/transport/wireguard/device_stack_stub.go b/transport/wireguard/device_stack_stub.go new file mode 100644 index 00000000..ea413559 --- /dev/null +++ b/transport/wireguard/device_stack_stub.go @@ -0,0 +1,13 @@ +//go:build !with_gvisor + +package wireguard + +import "github.com/sagernet/sing-tun" + +func newStackDevice(options DeviceOptions) (Device, error) { + return nil, tun.ErrGVisorNotIncluded +} + +func newSystemStackDevice(options DeviceOptions) (Device, error) { + return nil, tun.ErrGVisorNotIncluded +} diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go new file mode 100644 index 00000000..dcf2959b --- /dev/null +++ b/transport/wireguard/device_system.go @@ -0,0 +1,189 @@ +package wireguard + +import ( + "context" + "errors" + "net" + "net/netip" + "os" + "runtime" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + "github.com/sagernet/wireguard-go/device" + wgTun "github.com/sagernet/wireguard-go/tun" +) + +var _ Device = (*systemDevice)(nil) + +type systemDevice struct { + options DeviceOptions + dialer N.Dialer + device tun.Tun + batchDevice tun.LinuxTUN + events chan wgTun.Event + closeOnce sync.Once + inet4Address netip.Addr + inet6Address netip.Addr +} + +func newSystemDevice(options DeviceOptions) (*systemDevice, error) { + if options.Name == "" { + options.Name = tun.CalculateInterfaceName("wg") + } + var inet4Address netip.Addr + var inet6Address netip.Addr + if len(options.Address) > 0 { + if prefix := common.Find(options.Address, func(it netip.Prefix) bool { + return it.Addr().Is4() + }); prefix.IsValid() { + inet4Address = prefix.Addr() + } + } + if len(options.Address) > 0 { + if prefix := common.Find(options.Address, func(it netip.Prefix) bool { + return it.Addr().Is6() + }); prefix.IsValid() { + inet6Address = prefix.Addr() + } + } + return &systemDevice{ + options: options, + dialer: options.CreateDialer(options.Name), + events: make(chan wgTun.Event, 1), + inet4Address: inet4Address, + inet6Address: inet6Address, + }, nil +} + +func (w *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return w.dialer.DialContext(ctx, network, destination) +} + +func (w *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return w.dialer.ListenPacket(ctx, destination) +} + +func (w *systemDevice) Inet4Address() netip.Addr { + return w.inet4Address +} + +func (w *systemDevice) Inet6Address() netip.Addr { + return w.inet6Address +} + +func (w *systemDevice) SetDevice(device *device.Device) { +} + +func (w *systemDevice) Start() error { + networkManager := service.FromContext[adapter.NetworkManager](w.options.Context) + tunOptions := tun.Options{ + Name: w.options.Name, + Inet4Address: common.Filter(w.options.Address, func(it netip.Prefix) bool { + return it.Addr().Is4() + }), + Inet6Address: common.Filter(w.options.Address, func(it netip.Prefix) bool { + return it.Addr().Is6() + }), + MTU: w.options.MTU, + GSO: true, + InterfaceScope: true, + Inet4RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }), + Inet6RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { return it.Addr().Is6() }), + InterfaceMonitor: networkManager.InterfaceMonitor(), + InterfaceFinder: networkManager.InterfaceFinder(), + Logger: w.options.Logger, + } + // works with Linux, macOS with IFSCOPE routes, not tested on Windows + if runtime.GOOS == "darwin" { + tunOptions.AutoRoute = true + } + tunInterface, err := tun.New(tunOptions) + if err != nil { + return err + } + err = tunInterface.Start() + if err != nil { + return err + } + w.options.Logger.Info("started at ", w.options.Name) + w.device = tunInterface + batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) + if isBatchTUN && batchTUN.BatchSize() > 1 { + w.batchDevice = batchTUN + } + w.events <- wgTun.EventUp + return nil +} + +func (w *systemDevice) File() *os.File { + return nil +} + +func (w *systemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { + if w.batchDevice != nil { + count, err = w.batchDevice.BatchRead(bufs, offset-tun.PacketOffset, sizes) + } else { + sizes[0], err = w.device.Read(bufs[0][offset-tun.PacketOffset:]) + if err == nil { + count = 1 + } else if errors.Is(err, tun.ErrTooManySegments) { + err = wgTun.ErrTooManySegments + } + } + return +} + +func (w *systemDevice) Write(bufs [][]byte, offset int) (count int, err error) { + if w.batchDevice != nil { + return w.batchDevice.BatchWrite(bufs, offset) + } else { + for _, packet := range bufs { + if tun.PacketOffset > 0 { + common.ClearArray(packet[offset-tun.PacketOffset : offset]) + tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) + } + _, err = w.device.Write(packet[offset-tun.PacketOffset:]) + if err != nil { + return + } + } + } + // WireGuard will not read count + return +} + +func (w *systemDevice) Flush() error { + return nil +} + +func (w *systemDevice) MTU() (int, error) { + return int(w.options.MTU), nil +} + +func (w *systemDevice) Name() (string, error) { + return w.options.Name, nil +} + +func (w *systemDevice) Events() <-chan wgTun.Event { + return w.events +} + +func (w *systemDevice) Close() error { + close(w.events) + return w.device.Close() +} + +func (w *systemDevice) BatchSize() int { + if w.batchDevice != nil { + return w.batchDevice.BatchSize() + } + return 1 +} diff --git a/transport/wireguard/device_system_stack.go b/transport/wireguard/device_system_stack.go new file mode 100644 index 00000000..94fd6f4f --- /dev/null +++ b/transport/wireguard/device_system_stack.go @@ -0,0 +1,243 @@ +//go:build with_gvisor + +package wireguard + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/gvisor/pkg/buffer" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/wireguard-go/device" +) + +var _ Device = (*systemStackDevice)(nil) + +type systemStackDevice struct { + *systemDevice + ctx context.Context + logger logger.ContextLogger + stack *stack.Stack + endpoint *deviceEndpoint + writeBufs [][]byte +} + +func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { + system, err := newSystemDevice(options) + if err != nil { + return nil, err + } + endpoint := &deviceEndpoint{ + mtu: options.MTU, + done: make(chan struct{}), + } + ipStack, err := tun.NewGVisorStackWithOptions(endpoint, stack.NICOptions{}, true) + if err != nil { + return nil, err + } + var ( + inet4Address netip.Addr + inet6Address netip.Addr + ) + for _, prefix := range options.Address { + addr := tun.AddressFromAddr(prefix.Addr()) + protoAddr := tcpip.ProtocolAddress{ + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: addr, + PrefixLen: prefix.Bits(), + }, + } + if prefix.Addr().Is4() { + inet4Address = prefix.Addr() + protoAddr.Protocol = ipv4.ProtocolNumber + } else { + inet6Address = prefix.Addr() + protoAddr.Protocol = ipv6.ProtocolNumber + } + gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) + if gErr != nil { + return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String()) + } + } + if options.Handler != nil { + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) + icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout) + icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + } + return &systemStackDevice{ + ctx: options.Context, + logger: options.Logger, + systemDevice: system, + stack: ipStack, + endpoint: endpoint, + }, nil +} + +func (w *systemStackDevice) SetDevice(device *device.Device) { + w.endpoint.device = device +} + +func (w *systemStackDevice) Write(bufs [][]byte, offset int) (count int, err error) { + if w.batchDevice != nil { + w.writeBufs = w.writeBufs[:0] + for _, packet := range bufs { + if !w.writeStack(packet[offset:]) { + w.writeBufs = append(w.writeBufs, packet) + } + } + if len(w.writeBufs) > 0 { + return w.batchDevice.BatchWrite(bufs, offset) + } + } else { + for _, packet := range bufs { + if !w.writeStack(packet[offset:]) { + if tun.PacketOffset > 0 { + common.ClearArray(packet[offset-tun.PacketOffset : offset]) + tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) + } + _, err = w.device.Write(packet[offset-tun.PacketOffset:]) + } + if err != nil { + return + } + } + } + // WireGuard will not read count + return +} + +func (w *systemStackDevice) Close() error { + close(w.endpoint.done) + w.stack.Close() + for _, endpoint := range w.stack.CleanupEndpoints() { + endpoint.Abort() + } + w.stack.Wait() + return w.systemDevice.Close() +} + +func (w *systemStackDevice) writeStack(packet []byte) bool { + var ( + networkProtocol tcpip.NetworkProtocolNumber + destination netip.Addr + ) + switch header.IPVersion(packet) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + destination = netip.AddrFrom4(header.IPv4(packet).DestinationAddress().As4()) + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + destination = netip.AddrFrom16(header.IPv6(packet).DestinationAddress().As16()) + } + for _, prefix := range w.options.Address { + if prefix.Contains(destination) { + return false + } + } + packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + }) + w.endpoint.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer) + packetBuffer.DecRef() + return true +} + +func (w *systemStackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + ctx := log.ContextWithNewID(w.ctx) + destination, err := ping.ConnectGVisor( + ctx, w.logger, + metadata.Source.Addr, metadata.Destination.Addr, + routeContext, + w.stack, + w.inet4Address, w.inet6Address, + timeout, + ) + if err != nil { + return nil, err + } + w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) + return destination, nil +} + +type deviceEndpoint struct { + mtu uint32 + done chan struct{} + device *device.Device + dispatcher stack.NetworkDispatcher +} + +func (ep *deviceEndpoint) MTU() uint32 { + return ep.mtu +} + +func (ep *deviceEndpoint) SetMTU(mtu uint32) { +} + +func (ep *deviceEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (ep *deviceEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (ep *deviceEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { +} + +func (ep *deviceEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (ep *deviceEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + ep.dispatcher = dispatcher +} + +func (ep *deviceEndpoint) IsAttached() bool { + return ep.dispatcher != nil +} + +func (ep *deviceEndpoint) Wait() { +} + +func (ep *deviceEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (ep *deviceEndpoint) AddHeader(buffer *stack.PacketBuffer) { +} + +func (ep *deviceEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (ep *deviceEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { + for _, packetBuffer := range list.AsSlice() { + destination := packetBuffer.Network().DestinationAddress() + ep.device.InputPacket(destination.AsSlice(), packetBuffer.AsSlices()) + } + return list.Len(), nil +} + +func (ep *deviceEndpoint) Close() { +} + +func (ep *deviceEndpoint) SetOnCloseAction(f func()) { +} diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go new file mode 100644 index 00000000..3a02e17a --- /dev/null +++ b/transport/wireguard/endpoint.go @@ -0,0 +1,290 @@ +package wireguard + +import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "net" + "net/netip" + "os" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" + "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/device" + + "go4.org/netipx" +) + +type Endpoint struct { + options EndpointOptions + peers []peerConfig + ipcConf string + allowedAddress []netip.Prefix + tunDevice Device + natDevice NatDevice + device *device.Device + allowedIPs *device.AllowedIPs + pause pause.Manager + pauseCallback *list.Element[pause.Callback] +} + +func NewEndpoint(options EndpointOptions) (*Endpoint, error) { + if options.PrivateKey == "" { + return nil, E.New("missing private key") + } + privateKeyBytes, err := base64.StdEncoding.DecodeString(options.PrivateKey) + if err != nil { + return nil, E.Cause(err, "decode private key") + } + privateKey := hex.EncodeToString(privateKeyBytes) + ipcConf := "private_key=" + privateKey + if options.ListenPort != 0 { + ipcConf += "\nlisten_port=" + F.ToString(options.ListenPort) + } + var peers []peerConfig + for peerIndex, rawPeer := range options.Peers { + peer := peerConfig{ + allowedIPs: rawPeer.AllowedIPs, + keepalive: rawPeer.PersistentKeepaliveInterval, + } + if rawPeer.Endpoint.Addr.IsValid() { + peer.endpoint = rawPeer.Endpoint.AddrPort() + } else if rawPeer.Endpoint.IsDomain() { + peer.destination = rawPeer.Endpoint + } + publicKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public key for peer ", peerIndex) + } + peer.publicKeyHex = hex.EncodeToString(publicKeyBytes) + if rawPeer.PreSharedKey != "" { + preSharedKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PreSharedKey) + if err != nil { + return nil, E.Cause(err, "decode pre shared key for peer ", peerIndex) + } + peer.preSharedKeyHex = hex.EncodeToString(preSharedKeyBytes) + } + if len(rawPeer.AllowedIPs) == 0 { + return nil, E.New("missing allowed ips for peer ", peerIndex) + } + if len(rawPeer.Reserved) > 0 { + if len(rawPeer.Reserved) != 3 { + return nil, E.New("invalid reserved value for peer ", peerIndex, ", required 3 bytes, got ", len(peer.reserved)) + } + copy(peer.reserved[:], rawPeer.Reserved[:]) + } + peers = append(peers, peer) + } + var allowedPrefixBuilder netipx.IPSetBuilder + for _, peer := range options.Peers { + for _, prefix := range peer.AllowedIPs { + allowedPrefixBuilder.AddPrefix(prefix) + } + } + allowedIPSet, err := allowedPrefixBuilder.IPSet() + if err != nil { + return nil, err + } + allowedAddresses := allowedIPSet.Prefixes() + if options.MTU == 0 { + options.MTU = 1408 + } + deviceOptions := DeviceOptions{ + Context: options.Context, + Logger: options.Logger, + System: options.System, + Handler: options.Handler, + UDPTimeout: options.UDPTimeout, + CreateDialer: options.CreateDialer, + Name: options.Name, + MTU: options.MTU, + Address: options.Address, + AllowedAddress: allowedAddresses, + } + tunDevice, err := NewDevice(deviceOptions) + if err != nil { + return nil, E.Cause(err, "create WireGuard device") + } + natDevice, isNatDevice := tunDevice.(NatDevice) + if !isNatDevice { + natDevice = NewNATDevice(options.Context, options.Logger, tunDevice) + } + return &Endpoint{ + options: options, + peers: peers, + ipcConf: ipcConf, + allowedAddress: allowedAddresses, + tunDevice: tunDevice, + natDevice: natDevice, + }, nil +} + +func (e *Endpoint) Start(resolve bool) error { + if common.Any(e.peers, func(peer peerConfig) bool { + return !peer.endpoint.IsValid() && peer.destination.IsDomain() + }) { + if !resolve { + return nil + } + for peerIndex, peer := range e.peers { + if peer.endpoint.IsValid() || !peer.destination.IsDomain() { + continue + } + destinationAddress, err := e.options.ResolvePeer(peer.destination.Fqdn) + if err != nil { + return E.Cause(err, "resolve endpoint domain for peer[", peerIndex, "]: ", peer.destination) + } + e.peers[peerIndex].endpoint = netip.AddrPortFrom(destinationAddress, peer.destination.Port) + } + } else if resolve { + return nil + } + var bind conn.Bind + wgListener, isWgListener := common.Cast[dialer.WireGuardListener](e.options.Dialer) + if isWgListener { + bind = conn.NewStdNetBind(wgListener.WireGuardControl()) + } else { + var ( + isConnect bool + connectAddr netip.AddrPort + reserved [3]uint8 + ) + if len(e.peers) == 1 && e.peers[0].endpoint.IsValid() { + isConnect = true + connectAddr = e.peers[0].endpoint + reserved = e.peers[0].reserved + } + bind = NewClientBind(e.options.Context, e.options.Logger, e.options.Dialer, isConnect, connectAddr, reserved) + } + if isWgListener || len(e.peers) > 1 { + for _, peer := range e.peers { + if peer.reserved != [3]uint8{} { + bind.SetReservedForEndpoint(peer.endpoint, peer.reserved) + } + } + } + err := e.tunDevice.Start() + if err != nil { + return err + } + logger := &device.Logger{ + Verbosef: func(format string, args ...interface{}) { + e.options.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + }, + Errorf: func(format string, args ...interface{}) { + e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + }, + } + var deviceInput Device + if e.natDevice != nil { + deviceInput = e.natDevice + } else { + deviceInput = e.tunDevice + } + wgDevice := device.NewDevice(e.options.Context, deviceInput, bind, logger, e.options.Workers) + e.tunDevice.SetDevice(wgDevice) + ipcConf := e.ipcConf + for _, peer := range e.peers { + ipcConf += peer.GenerateIpcLines() + } + err = wgDevice.IpcSet(ipcConf) + if err != nil { + return E.Cause(err, "setup wireguard: \n", ipcConf) + } + e.device = wgDevice + e.pause = service.FromContext[pause.Manager](e.options.Context) + if e.pause != nil { + e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated) + } + e.allowedIPs = (*device.AllowedIPs)(unsafe.Pointer(reflect.Indirect(reflect.ValueOf(wgDevice)).FieldByName("allowedips").UnsafeAddr())) + return nil +} + +func (e *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if !destination.Addr.IsValid() { + return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination") + } + return e.tunDevice.DialContext(ctx, network, destination) +} + +func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if !destination.Addr.IsValid() { + return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination") + } + return e.tunDevice.ListenPacket(ctx, destination) +} + +func (e *Endpoint) Close() error { + if e.pauseCallback != nil { + e.pause.UnregisterCallback(e.pauseCallback) + } + if e.device != nil { + e.device.Down() + e.device.Close() + } + return nil +} + +func (e *Endpoint) Lookup(address netip.Addr) *device.Peer { + if e.allowedIPs == nil { + return nil + } + return e.allowedIPs.Lookup(address.AsSlice()) +} + +func (e *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + if e.natDevice == nil { + return nil, os.ErrInvalid + } + return e.natDevice.CreateDestination(metadata, routeContext, timeout) +} + +func (e *Endpoint) onPauseUpdated(event int) { + switch event { + case pause.EventDevicePaused, pause.EventNetworkPause: + e.device.Down() + case pause.EventDeviceWake, pause.EventNetworkWake: + e.device.Up() + } +} + +type peerConfig struct { + destination M.Socksaddr + endpoint netip.AddrPort + publicKeyHex string + preSharedKeyHex string + allowedIPs []netip.Prefix + keepalive uint16 + reserved [3]uint8 +} + +func (c peerConfig) GenerateIpcLines() string { + ipcLines := "\npublic_key=" + c.publicKeyHex + if c.endpoint.IsValid() { + ipcLines += "\nendpoint=" + c.endpoint.String() + } + if c.preSharedKeyHex != "" { + ipcLines += "\npreshared_key=" + c.preSharedKeyHex + } + for _, allowedIP := range c.allowedIPs { + ipcLines += "\nallowed_ip=" + allowedIP.String() + } + if c.keepalive > 0 { + ipcLines += "\npersistent_keepalive_interval=" + F.ToString(c.keepalive) + } + return ipcLines +} diff --git a/transport/wireguard/endpoint_options.go b/transport/wireguard/endpoint_options.go new file mode 100644 index 00000000..bb9a46e6 --- /dev/null +++ b/transport/wireguard/endpoint_options.go @@ -0,0 +1,39 @@ +package wireguard + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type EndpointOptions struct { + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + Dialer N.Dialer + CreateDialer func(interfaceName string) N.Dialer + Name string + MTU uint32 + Address []netip.Prefix + PrivateKey string + ListenPort uint16 + ResolvePeer func(domain string) (netip.Addr, error) + Peers []PeerOptions + Workers int +} + +type PeerOptions struct { + Endpoint M.Socksaddr + PublicKey string + PreSharedKey string + AllowedIPs []netip.Prefix + PersistentKeepaliveInterval uint16 + Reserved []uint8 +}