mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
139 Commits
v1.12.0-be
...
dev-dns-ti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d8c15932f | ||
|
|
70371c3cbe | ||
|
|
32bc8b48f1 | ||
|
|
b5df508bc9 | ||
|
|
4789846113 | ||
|
|
ad94f94cfb | ||
|
|
1f9de9f321 | ||
|
|
344bbf9494 | ||
|
|
b7e16e70ab | ||
|
|
391153ecb8 | ||
|
|
3d821db0b2 | ||
|
|
344ecd3798 | ||
|
|
a8a3f863cd | ||
|
|
ea190ca428 | ||
|
|
e65b78d1e4 | ||
|
|
84f7d80da7 | ||
|
|
2b57eb0b30 | ||
|
|
9884f81298 | ||
|
|
8493b4f1da | ||
|
|
8f1a8add85 | ||
|
|
85ee6bb266 | ||
|
|
c4387f7c37 | ||
|
|
dcb7a5caed | ||
|
|
ae77cdeedd | ||
|
|
ec05d4e5e3 | ||
|
|
1f766d2b89 | ||
|
|
a5a6c1f7d4 | ||
|
|
cd43786279 | ||
|
|
43a211db28 | ||
|
|
d4f6bdf792 | ||
|
|
99cf27bedd | ||
|
|
0a14c5ab1f | ||
|
|
533b31e1f6 | ||
|
|
8ed6523872 | ||
|
|
e5c162222d | ||
|
|
88babbf3a7 | ||
|
|
49a03e0b23 | ||
|
|
9a8e9a34c0 | ||
|
|
17551db7be | ||
|
|
83201bb088 | ||
|
|
f0f1942f1f | ||
|
|
c7e318be61 | ||
|
|
2a5e0d0c92 | ||
|
|
956e485342 | ||
|
|
23ede74e74 | ||
|
|
03317e61dd | ||
|
|
fc81bd9a5b | ||
|
|
4d406cad84 | ||
|
|
1f0282de9c | ||
|
|
b097912418 | ||
|
|
056c29e73a | ||
|
|
7aa0a57e60 | ||
|
|
2673e64bcb | ||
|
|
3d3c1709d7 | ||
|
|
9de29a590f | ||
|
|
a5282b08ec | ||
|
|
b26c2083bf | ||
|
|
c1f4c691dc | ||
|
|
c0ef6eb728 | ||
|
|
074d61021f | ||
|
|
246c9d4e40 | ||
|
|
3e3466c8d7 | ||
|
|
f73415a732 | ||
|
|
ecabe9ffe1 | ||
|
|
2dae1ee284 | ||
|
|
2afb24d698 | ||
|
|
ade83ee758 | ||
|
|
ac9c300ca7 | ||
|
|
e6eb3cec2b | ||
|
|
601b79371b | ||
|
|
56b957d30d | ||
|
|
eb87b1a708 | ||
|
|
c0a5561bd4 | ||
|
|
4a3fe1d41c | ||
|
|
4cbbcfb04d | ||
|
|
b3ad4e0e39 | ||
|
|
49f9c0011d | ||
|
|
c8c165af87 | ||
|
|
3f790ff8c9 | ||
|
|
f2272ae1e7 | ||
|
|
b40c264c0a | ||
|
|
29fc8d6a86 | ||
|
|
46ca27c926 | ||
|
|
243a5dd477 | ||
|
|
844e9f09a6 | ||
|
|
9aa673f79e | ||
|
|
5abd74ffe3 | ||
|
|
68a32960bd | ||
|
|
1e62e3e5d4 | ||
|
|
40c03a9913 | ||
|
|
2ea4029868 | ||
|
|
66f5cdd014 | ||
|
|
89da2b6355 | ||
|
|
ee2b8498e6 | ||
|
|
b1eaf537bd | ||
|
|
63739c1621 | ||
|
|
3c8ddee029 | ||
|
|
23aad70045 | ||
|
|
0d67b51267 | ||
|
|
c02f939265 | ||
|
|
36b84e25c2 | ||
|
|
9fd4b0e9ae | ||
|
|
e68cdcc98a | ||
|
|
2691617c5e | ||
|
|
04056a1357 | ||
|
|
faac858e5d | ||
|
|
a818c8abeb | ||
|
|
15cbe9fc87 | ||
|
|
e186f2d31e | ||
|
|
e9323481a4 | ||
|
|
a0a41ff2bb | ||
|
|
549daf9d41 | ||
|
|
fa370f7d04 | ||
|
|
795cb17bfa | ||
|
|
00d8add761 | ||
|
|
36db31c55a | ||
|
|
4dbbf59c82 | ||
|
|
832eb4458d | ||
|
|
2cf989d306 | ||
|
|
7d3ee29bd0 | ||
|
|
cba0e46aba | ||
|
|
9b8ab3e61e | ||
|
|
47f18e823a | ||
|
|
2d1b824b62 | ||
|
|
d511698f3f | ||
|
|
cb435ea232 | ||
|
|
43a9016c83 | ||
|
|
255068fd40 | ||
|
|
098a00b025 | ||
|
|
dba0b5276b | ||
|
|
78ae935468 | ||
|
|
3ea5f76470 | ||
|
|
b4d294c05e | ||
|
|
83cf5f5c6a | ||
|
|
e7b3a8eebe | ||
|
|
ee3a42a67e | ||
|
|
50227c0f5f | ||
|
|
bc5eb1e1a5 | ||
|
|
995267a042 |
@@ -8,6 +8,7 @@
|
||||
--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
|
||||
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
if: ${{ ! matrix.legacy_go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Cache Legacy Go
|
||||
if: matrix.require_legacy_go
|
||||
id: cache-legacy-go
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -472,15 +472,15 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Setup Xcode stable
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||
run: |-
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
- name: Setup Xcode beta
|
||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
@@ -615,7 +615,7 @@ jobs:
|
||||
path: 'dist'
|
||||
upload:
|
||||
name: Upload builds
|
||||
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
|
||||
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
|
||||
6
.github/workflows/linux.yml
vendored
6
.github/workflows/linux.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api'
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
25
Makefile
25
Makefile
@@ -1,11 +1,10 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_clash_api,with_quic,with_utls,with_tailscale
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
@@ -109,6 +108,16 @@ 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:
|
||||
@@ -176,6 +185,16 @@ 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:
|
||||
|
||||
@@ -3,6 +3,7 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -36,6 +37,7 @@ type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
@@ -53,6 +55,7 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
Timeout: time.Duration(options.Timeout),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
@@ -70,6 +73,7 @@ type DNSTransport interface {
|
||||
Type() string
|
||||
Tag() string
|
||||
Dependencies() []string
|
||||
HasDetour() bool
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,11 +53,11 @@ type InboundContext struct {
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
SniffError error
|
||||
|
||||
// cache
|
||||
|
||||
|
||||
2
box.go
2
box.go
@@ -498,7 +498,7 @@ func (s *Box) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
err := common.Close(
|
||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
|
||||
Submodule clients/android updated: cec05bf693...eb2e13a6f9
@@ -105,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
tag := tagVersion.VersionString()
|
||||
client := createClient(10 * time.Minute)
|
||||
client := createClient(20 * time.Minute)
|
||||
|
||||
log.Info(tag, " list build IDs")
|
||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||
@@ -145,7 +145,7 @@ func publishTestflight(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
build := builds.Data[0]
|
||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
|
||||
if 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
|
||||
|
||||
@@ -16,15 +16,17 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
debugEnabled bool
|
||||
target string
|
||||
platform string
|
||||
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() {
|
||||
@@ -151,7 +153,9 @@ func buildApple() {
|
||||
"-v",
|
||||
"-target", bindTarget,
|
||||
"-libname=box",
|
||||
"-tags-macos=" + strings.Join(memcTags, ","),
|
||||
}
|
||||
if !withTailscale {
|
||||
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
@@ -161,6 +165,9 @@ func buildApple() {
|
||||
}
|
||||
|
||||
tags := append(sharedTags, iosTags...)
|
||||
if withTailscale {
|
||||
tags = append(tags, memcTags...)
|
||||
}
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -68,6 +67,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||
configPaths = append(configPaths, "config.json")
|
||||
}
|
||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
|
||||
globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
|
||||
"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"
|
||||
@@ -54,7 +54,7 @@ func convertRuleSet(sourcePath string) error {
|
||||
var rules []option.HeadlessRule
|
||||
switch flagRuleSetConvertType {
|
||||
case "adguard":
|
||||
rules, err = adguard.Convert(reader)
|
||||
rules, err = adguard.ToOptions(reader, log.StdLogger())
|
||||
case "":
|
||||
return E.New("source type is required")
|
||||
default:
|
||||
|
||||
@@ -6,7 +6,10 @@ import (
|
||||
"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"
|
||||
@@ -50,6 +53,11 @@ func decompileRuleSet(sourcePath string) error {
|
||||
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") {
|
||||
@@ -75,3 +83,19 @@ func decompileRuleSet(sourcePath string) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package adguard
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -9,10 +10,10 @@ import (
|
||||
"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/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
@@ -27,7 +28,7 @@ type agdguardRuleLine struct {
|
||||
isImportant bool
|
||||
}
|
||||
|
||||
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
|
||||
func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, error) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var (
|
||||
ruleLines []agdguardRuleLine
|
||||
@@ -36,7 +37,10 @@ func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
|
||||
parseLine:
|
||||
for scanner.Scan() {
|
||||
ruleLine := scanner.Text()
|
||||
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
|
||||
if ruleLine == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") {
|
||||
continue
|
||||
}
|
||||
originRuleLine := ruleLine
|
||||
@@ -92,7 +96,7 @@ parseLine:
|
||||
}
|
||||
if !ignored {
|
||||
ignoredLines++
|
||||
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
|
||||
logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine)
|
||||
continue parseLine
|
||||
}
|
||||
}
|
||||
@@ -120,27 +124,35 @@ parseLine:
|
||||
ruleLine = ruleLine[1 : len(ruleLine)-1]
|
||||
if ignoreIPCIDRRegexp(ruleLine) {
|
||||
ignoredLines++
|
||||
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
|
||||
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++
|
||||
log.Debug("ignored unsupported rule with path: ", ruleLine)
|
||||
logger.Debug("ignored unsupported rule with path: ", originRuleLine)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(ruleLine, "##") {
|
||||
if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") {
|
||||
ignoredLines++
|
||||
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
||||
logger.Debug("ignored unsupported rule with query: ", originRuleLine)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(ruleLine, "#$#") {
|
||||
if strings.Contains(ruleLine, "[") || strings.Contains(ruleLine, "]") ||
|
||||
strings.Contains(ruleLine, "(") || strings.Contains(ruleLine, ")") ||
|
||||
strings.Contains(ruleLine, "!") || strings.Contains(ruleLine, "#") {
|
||||
ignoredLines++
|
||||
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
||||
logger.Debug("ignored unsupported cosmetic filter: ", originRuleLine)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(ruleLine, "~") {
|
||||
ignoredLines++
|
||||
logger.Debug("ignored unsupported rule modifier: ", originRuleLine)
|
||||
continue
|
||||
}
|
||||
var domainCheck string
|
||||
@@ -151,7 +163,7 @@ parseLine:
|
||||
}
|
||||
if ruleLine == "" {
|
||||
ignoredLines++
|
||||
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
|
||||
logger.Debug("ignored unsupported rule with empty domain", originRuleLine)
|
||||
continue
|
||||
} else {
|
||||
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
|
||||
@@ -159,13 +171,13 @@ parseLine:
|
||||
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
|
||||
if ipErr == nil {
|
||||
ignoredLines++
|
||||
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
|
||||
logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine)
|
||||
continue
|
||||
}
|
||||
if M.ParseSocksaddr(domainCheck).Port != 0 {
|
||||
log.Debug("ignored unsupported rule with port: ", ruleLine)
|
||||
logger.Debug("ignored unsupported rule with port: ", originRuleLine)
|
||||
} else {
|
||||
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
|
||||
logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine)
|
||||
}
|
||||
ignoredLines++
|
||||
continue
|
||||
@@ -283,10 +295,112 @@ parseLine:
|
||||
},
|
||||
}
|
||||
}
|
||||
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
|
||||
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:]
|
||||
@@ -294,11 +408,9 @@ func ignoreIPCIDRRegexp(ruleLine string) bool {
|
||||
ruleLine = ruleLine[13:]
|
||||
} else if strings.HasPrefix(ruleLine, "^") {
|
||||
ruleLine = ruleLine[1:]
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
|
||||
return parseErr == nil
|
||||
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) {
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
|
||||
"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()
|
||||
rules, err := Convert(strings.NewReader(`
|
||||
ruleString := `||sagernet.org^$important
|
||||
@@|sing-box.sagernet.org^$important
|
||||
||example.org^
|
||||
|example.com^
|
||||
example.net^
|
||||
@@ -21,10 +23,9 @@ example.net^
|
||||
||example.edu.tw^
|
||||
|example.gov
|
||||
example.arpa
|
||||
@@|sagernet.example.org|
|
||||
||sagernet.org^$important
|
||||
@@|sing-box.sagernet.org^$important
|
||||
`))
|
||||
@@|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])
|
||||
@@ -75,15 +76,18 @@ example.arpa
|
||||
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 := Convert(strings.NewReader(`
|
||||
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])
|
||||
@@ -110,10 +114,10 @@ func TestHosts(t *testing.T) {
|
||||
|
||||
func TestSimpleHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
rules, err := Convert(strings.NewReader(`
|
||||
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])
|
||||
@@ -97,10 +97,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
} else if networkManager.AutoDetectInterface() {
|
||||
if platformInterface != nil {
|
||||
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
||||
if networkStrategy == nil {
|
||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
||||
defaultNetworkStrategy = true
|
||||
}
|
||||
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 {
|
||||
@@ -112,6 +108,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
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)
|
||||
|
||||
@@ -89,6 +89,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
Timeout: time.Duration(dialOptions.DomainResolver.Timeout),
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
|
||||
@@ -10,9 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
@@ -26,7 +24,9 @@ type slowOpenConn struct {
|
||||
destination M.Socksaddr
|
||||
conn net.Conn
|
||||
create chan struct{}
|
||||
done chan struct{}
|
||||
access sync.Mutex
|
||||
closeOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
|
||||
network: network,
|
||||
destination: destination,
|
||||
create: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -55,8 +56,8 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
}
|
||||
return c.conn.Read(b)
|
||||
@@ -74,12 +75,15 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
return 0, c.err
|
||||
}
|
||||
return c.conn.Write(b)
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
default:
|
||||
}
|
||||
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
if err != nil {
|
||||
c.conn = nil
|
||||
c.err = E.Cause(err, "dial tcp fast open")
|
||||
c.err = err
|
||||
} else {
|
||||
c.conn = conn
|
||||
}
|
||||
n = len(b)
|
||||
close(c.create)
|
||||
@@ -87,7 +91,13 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Close() error {
|
||||
return common.Close(c.conn)
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.done)
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||
@@ -152,8 +162,8 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, c.err
|
||||
}
|
||||
}
|
||||
return bufio.Copy(w, c.conn)
|
||||
|
||||
@@ -56,7 +56,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
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, M.ParseSocksaddr(address).IsIPv6(), false)
|
||||
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
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, M.ParseSocksaddr(address).IsIPv6(), true)
|
||||
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
// rup8(sizeof(xtcpcb_n))
|
||||
itemSize += 208
|
||||
}
|
||||
|
||||
var fallbackUDPProcess string
|
||||
// skip the first xinpgen(24 bytes) block
|
||||
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
||||
// offset of xinpcb_n and xsocket_n
|
||||
@@ -90,10 +92,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
flag := buf[inp+44]
|
||||
|
||||
var srcIP netip.Addr
|
||||
srcIsIPv4 := false
|
||||
switch {
|
||||
case flag&0x1 > 0 && isIPv4:
|
||||
// ipv4
|
||||
srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80]))
|
||||
srcIsIPv4 = true
|
||||
case flag&0x2 > 0 && !isIPv4:
|
||||
// ipv6
|
||||
srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80]))
|
||||
@@ -101,13 +105,21 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ip != srcIP {
|
||||
continue
|
||||
if ip == srcIP {
|
||||
// xsocket_n.so_last_pid
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
return getExecPathFromPID(pid)
|
||||
}
|
||||
|
||||
// xsocket_n.so_last_pid
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
return getExecPathFromPID(pid)
|
||||
// udp packet connection may be not equal with srcIP
|
||||
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
fallbackUDPProcess, _ = getExecPathFromPID(pid)
|
||||
}
|
||||
}
|
||||
|
||||
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
|
||||
return fallbackUDPProcess, nil
|
||||
}
|
||||
|
||||
return "", ErrNotFound
|
||||
|
||||
@@ -215,16 +215,15 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
||||
case ruleItemWIFIBSSID:
|
||||
rule.WIFIBSSID, err = readRuleItemString(reader)
|
||||
case ruleItemAdGuardDomain:
|
||||
if recover {
|
||||
err = E.New("unable to decompile binary AdGuard rules to rule-set")
|
||||
return
|
||||
}
|
||||
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:
|
||||
|
||||
@@ -5,13 +5,13 @@ package tls
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"os"
|
||||
"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/alidns"
|
||||
@@ -37,7 +37,38 @@ func (w *acmeWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
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 encoderConfig() zapcore.EncoderConfig {
|
||||
config := zap.NewProductionEncoderConfig()
|
||||
config.TimeKey = zapcore.OmitKey
|
||||
return config
|
||||
}
|
||||
|
||||
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
var acmeServer string
|
||||
switch options.Provider {
|
||||
case "", "letsencrypt":
|
||||
@@ -58,14 +89,15 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
|
||||
} else {
|
||||
storage = certmagic.Default.Storage
|
||||
}
|
||||
zapLogger := zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig()),
|
||||
&acmeLogWriter{logger: logger},
|
||||
zap.DebugLevel,
|
||||
))
|
||||
config := &certmagic.Config{
|
||||
DefaultServerName: options.DefaultServerName,
|
||||
Storage: storage,
|
||||
Logger: zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
|
||||
os.Stderr,
|
||||
zap.InfoLevel,
|
||||
)),
|
||||
Logger: zapLogger,
|
||||
}
|
||||
acmeConfig := certmagic.ACMEIssuer{
|
||||
CA: acmeServer,
|
||||
@@ -75,7 +107,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
|
||||
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
|
||||
AltHTTPPort: int(options.AlternativeHTTPPort),
|
||||
AltTLSALPNPort: int(options.AlternativeTLSPort),
|
||||
Logger: config.Logger,
|
||||
Logger: zapLogger,
|
||||
}
|
||||
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
|
||||
var solver certmagic.DNS01Solver
|
||||
@@ -103,6 +135,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
|
||||
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
|
||||
return config, nil
|
||||
},
|
||||
Logger: zapLogger,
|
||||
})
|
||||
config = certmagic.New(cache, *config)
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
"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, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
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`)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||
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"))
|
||||
@@ -45,12 +45,12 @@ func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions
|
||||
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
||||
return nil, E.New("invalid ECH configs pem")
|
||||
}
|
||||
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
|
||||
return &STDClientConfig{tlsConfig}, nil
|
||||
clientConfig.SetECHConfigList(block.Bytes)
|
||||
return clientConfig, nil
|
||||
} else {
|
||||
return &STDECHClientConfig{
|
||||
STDClientConfig: STDClientConfig{tlsConfig},
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
return &ECHClientConfig{
|
||||
ECHCapableConfig: clientConfig,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -102,15 +102,15 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type STDECHClientConfig struct {
|
||||
STDClientConfig
|
||||
type ECHClientConfig struct {
|
||||
ECHCapableConfig
|
||||
access sync.Mutex
|
||||
dnsRouter adapter.DNSRouter
|
||||
lastTTL time.Duration
|
||||
lastUpdate time.Time
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
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
|
||||
@@ -122,17 +122,17 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
|
||||
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
|
||||
message := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: mDNS.Fqdn(s.config.ServerName),
|
||||
Name: mDNS.Fqdn(s.ServerName()),
|
||||
Qtype: mDNS.TypeHTTPS,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
@@ -157,21 +157,21 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
|
||||
}
|
||||
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
|
||||
s.lastUpdate = time.Now()
|
||||
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||
s.SetECHConfigList(echConfigList)
|
||||
break match
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
||||
if len(s.ECHConfigList()) == 0 {
|
||||
return nil, E.New("no ECH config found in DNS records")
|
||||
}
|
||||
}
|
||||
return s.Client(conn)
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) Clone() Config {
|
||||
return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
|
||||
func (s *ECHClientConfig) Clone() Config {
|
||||
return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
|
||||
}
|
||||
|
||||
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||
|
||||
@@ -11,6 +11,12 @@ import (
|
||||
"github.com/cloudflare/circl/kem"
|
||||
)
|
||||
|
||||
type ECHCapableConfig interface {
|
||||
Config
|
||||
ECHConfigList() []byte
|
||||
SetECHConfigList([]byte)
|
||||
}
|
||||
|
||||
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
|
||||
cipherSuites := []echCipherSuite{
|
||||
{
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
|
||||
return nil, E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
|
||||
if decodedLen > 8 {
|
||||
return nil, E.New("invalid short_id")
|
||||
}
|
||||
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
|
||||
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) ServerName() string {
|
||||
|
||||
@@ -7,43 +7,60 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
type STDClientConfig struct {
|
||||
config *tls.Config
|
||||
ctx context.Context
|
||||
config *tls.Config
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) ServerName() string {
|
||||
return s.config.ServerName
|
||||
func (c *STDClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) SetServerName(serverName string) {
|
||||
s.config.ServerName = serverName
|
||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) NextProtos() []string {
|
||||
return s.config.NextProtos
|
||||
func (c *STDClientConfig) NextProtos() []string {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
s.config.NextProtos = nextProto
|
||||
func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) Config() (*STDConfig, error) {
|
||||
return s.config, nil
|
||||
func (c *STDClientConfig) Config() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
return tls.Client(conn, s.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 (s *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{s.config.Clone()}
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, 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, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
@@ -60,9 +77,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
@@ -127,8 +142,10 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
}
|
||||
stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return parseECHClientConfig(ctx, options, &tlsConfig)
|
||||
return parseECHClientConfig(ctx, stdConfig, options)
|
||||
} else {
|
||||
return stdConfig, nil
|
||||
}
|
||||
return &STDClientConfig{&tlsConfig}, nil
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
var err error
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
//nolint:staticcheck
|
||||
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
|
||||
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"crypto/x509"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
@@ -22,46 +23,60 @@ import (
|
||||
)
|
||||
|
||||
type UTLSClientConfig struct {
|
||||
config *utls.Config
|
||||
id utls.ClientHelloID
|
||||
ctx context.Context
|
||||
config *utls.Config
|
||||
id utls.ClientHelloID
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) ServerName() string {
|
||||
return e.config.ServerName
|
||||
func (c *UTLSClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) SetServerName(serverName string) {
|
||||
e.config.ServerName = serverName
|
||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) NextProtos() []string {
|
||||
return e.config.NextProtos
|
||||
func (c *UTLSClientConfig) NextProtos() []string {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS {
|
||||
nextProto = append(nextProto, "http/1.1")
|
||||
}
|
||||
e.config.NextProtos = nextProto
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) Config() (*STDConfig, error) {
|
||||
func (c *UTLSClientConfig) Config() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
|
||||
e.config.SessionIDGenerator = generator
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) Clone() Config {
|
||||
return &UTLSClientConfig{
|
||||
config: e.config.Clone(),
|
||||
id: e.id,
|
||||
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 {
|
||||
@@ -116,14 +131,12 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
return c.UConn.HandshakeContext(ctx)
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
|
||||
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
||||
serverName = serverAddress
|
||||
}
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
@@ -132,11 +145,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
tlsConfig.ServerName = serverName
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
@@ -192,7 +201,15 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UTLSClientConfig{&tlsConfig, id}, nil
|
||||
uConfig := &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")
|
||||
}
|
||||
return parseECHClientConfig(ctx, uConfig, options)
|
||||
} else {
|
||||
return uConfig, nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -220,7 +237,7 @@ func init() {
|
||||
|
||||
func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
|
||||
switch name {
|
||||
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq":
|
||||
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk":
|
||||
fallthrough
|
||||
case "chrome", "":
|
||||
return utls.HelloChrome_Auto, nil
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
@@ -19,16 +20,21 @@ type Conn struct {
|
||||
tcpConn *net.TCPConn
|
||||
ctx context.Context
|
||||
firstPacketWritten bool
|
||||
splitPacket bool
|
||||
splitRecord bool
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
|
||||
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,
|
||||
}
|
||||
@@ -41,7 +47,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
}()
|
||||
serverName := indexTLSServerName(b)
|
||||
if serverName != nil {
|
||||
if !c.splitRecord {
|
||||
if c.splitPacket {
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(true)
|
||||
if err != nil {
|
||||
@@ -81,33 +87,41 @@ func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
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)
|
||||
} else if c.tcpConn != nil && i != len(splitIndexes) {
|
||||
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
|
||||
if err != nil {
|
||||
return
|
||||
if c.splitPacket {
|
||||
payload = buffer.Bytes()
|
||||
}
|
||||
} else {
|
||||
_, err = c.Conn.Write(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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 c.splitRecord {
|
||||
if c.splitRecord && !c.splitPacket {
|
||||
_, err = c.Conn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return len(b), nil
|
||||
|
||||
@@ -15,7 +15,7 @@ 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(), false, 0), &tls.Config{
|
||||
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{
|
||||
ServerName: "www.cloudflare.com",
|
||||
})
|
||||
require.NoError(t, tlsConn.Handshake())
|
||||
@@ -25,7 +25,17 @@ 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(), true, 0), &tls.Config{
|
||||
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())
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
TCPTimeout = 15 * time.Second
|
||||
ReadPayloadTimeout = 300 * time.Millisecond
|
||||
DNSTimeout = 10 * time.Second
|
||||
DirectDNSTimeout = 5 * time.Second
|
||||
UDPTimeout = 5 * time.Minute
|
||||
DefaultURLTestInterval = 3 * time.Minute
|
||||
DefaultURLTestIdleTimeout = 30 * time.Minute
|
||||
|
||||
@@ -30,10 +30,10 @@ var (
|
||||
var _ adapter.DNSClient = (*Client)(nil)
|
||||
|
||||
type Client struct {
|
||||
timeout time.Duration
|
||||
disableCache bool
|
||||
disableExpire bool
|
||||
independentCache bool
|
||||
clientSubnet netip.Prefix
|
||||
rdrc adapter.RDRCStore
|
||||
initRDRCFunc func() adapter.RDRCStore
|
||||
logger logger.ContextLogger
|
||||
@@ -42,27 +42,24 @@ type Client struct {
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
IndependentCache bool
|
||||
CacheCapacity uint32
|
||||
ClientSubnet netip.Prefix
|
||||
RDRC func() adapter.RDRCStore
|
||||
Logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func NewClient(options ClientOptions) *Client {
|
||||
client := &Client{
|
||||
timeout: options.Timeout,
|
||||
disableCache: options.DisableCache,
|
||||
disableExpire: options.DisableExpire,
|
||||
independentCache: options.IndependentCache,
|
||||
clientSubnet: options.ClientSubnet,
|
||||
initRDRCFunc: options.RDRC,
|
||||
logger: options.Logger,
|
||||
}
|
||||
if client.timeout == 0 {
|
||||
client.timeout = C.DNSTimeout
|
||||
}
|
||||
cacheCapacity := options.CacheCapacity
|
||||
if cacheCapacity < 1024 {
|
||||
cacheCapacity = 1024
|
||||
@@ -104,8 +101,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return &responseMessage, nil
|
||||
}
|
||||
question := message.Question[0]
|
||||
if options.ClientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, options.ClientSubnet)
|
||||
clientSubnet := options.ClientSubnet
|
||||
if !clientSubnet.IsValid() {
|
||||
clientSubnet = c.clientSubnet
|
||||
}
|
||||
if clientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, clientSubnet)
|
||||
}
|
||||
isSimpleRequest := len(message.Question) == 1 &&
|
||||
len(message.Ns) == 0 &&
|
||||
@@ -146,7 +147,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return nil, ErrResponseRejectedCached
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
timeout := options.Timeout
|
||||
if timeout == 0 {
|
||||
if transport.HasDetour() {
|
||||
timeout = C.DNSTimeout
|
||||
} else {
|
||||
timeout = C.DirectDNSTimeout
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
cancel()
|
||||
if err != nil {
|
||||
|
||||
@@ -55,6 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
IndependentCache: options.DNSClientOptions.IndependentCache,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
|
||||
RDRC: func() adapter.RDRCStore {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
@@ -157,6 +158,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
if action.Strategy != C.DomainStrategyAsIS {
|
||||
options.Strategy = action.Strategy
|
||||
}
|
||||
if action.Timeout > 0 {
|
||||
options.Timeout = action.Timeout
|
||||
}
|
||||
if isFakeIP || action.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
@@ -179,6 +183,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
if action.Strategy != C.DomainStrategyAsIS {
|
||||
options.Strategy = action.Strategy
|
||||
}
|
||||
if action.Timeout > 0 {
|
||||
options.Timeout = action.Timeout
|
||||
}
|
||||
if action.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ type Transport struct {
|
||||
dns.TransportAdapter
|
||||
ctx context.Context
|
||||
dialer N.Dialer
|
||||
hasDetour bool
|
||||
logger logger.ContextLogger
|
||||
networkManager adapter.NetworkManager
|
||||
interfaceName string
|
||||
@@ -59,6 +60,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions),
|
||||
ctx: ctx,
|
||||
dialer: transportDialer,
|
||||
hasDetour: options.Detour != "",
|
||||
logger: logger,
|
||||
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
||||
interfaceName: options.Interface,
|
||||
@@ -89,6 +91,10 @@ func (t *Transport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) HasDetour() bool {
|
||||
return t.hasDetour
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
err := t.fetchServers()
|
||||
if err != nil {
|
||||
|
||||
@@ -3,11 +3,15 @@ package transport
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@@ -39,11 +43,13 @@ func RegisterHTTPS(registry *dns.TransportRegistry) {
|
||||
|
||||
type HTTPSTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
transport *http.Transport
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
transportAccess sync.Mutex
|
||||
transport *http.Transport
|
||||
transportResetAt time.Time
|
||||
}
|
||||
|
||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
@@ -161,12 +167,33 @@ func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
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) 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, os.ErrDeadlineExceeded) {
|
||||
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
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
)
|
||||
|
||||
func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
|
||||
if C.res_init() != 0 {
|
||||
var state C.res_state
|
||||
if C.res_ninit(state) != 0 {
|
||||
return &dnsConfig{
|
||||
servers: defaultNS,
|
||||
search: dnsDefaultSearch(),
|
||||
@@ -33,10 +34,10 @@ func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
|
||||
conf := &dnsConfig{
|
||||
ndots: 1,
|
||||
timeout: 5 * time.Second,
|
||||
attempts: int(C._res.retry),
|
||||
attempts: int(state.retry),
|
||||
}
|
||||
for i := 0; i < int(C._res.nscount); i++ {
|
||||
ns := C._res.nsaddr_list[i]
|
||||
for i := 0; i < int(state.nscount); i++ {
|
||||
ns := state.nsaddr_list[i]
|
||||
addr := C.inet_ntoa(ns.sin_addr)
|
||||
if addr == nil {
|
||||
continue
|
||||
@@ -44,7 +45,7 @@ func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
|
||||
conf.servers = append(conf.servers, C.GoString(addr))
|
||||
}
|
||||
for i := 0; ; i++ {
|
||||
search := C._res.dnsrch[i]
|
||||
search := state.dnsrch[i]
|
||||
if search == nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type TransportAdapter struct {
|
||||
transportType string
|
||||
transportTag string
|
||||
dependencies []string
|
||||
hasDetour bool
|
||||
strategy C.DomainStrategy
|
||||
clientSubnet netip.Prefix
|
||||
}
|
||||
@@ -35,6 +36,7 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
hasDetour: localOptions.Detour != "",
|
||||
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
|
||||
clientSubnet: localOptions.LegacyClientSubnet,
|
||||
}
|
||||
@@ -69,6 +71,10 @@ func (a *TransportAdapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) HasDetour() bool {
|
||||
return a.hasDetour
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
||||
return a.strategy
|
||||
}
|
||||
|
||||
@@ -2,6 +2,67 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.0-beta.30
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! 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)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-decagram: [servers](#servers)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [cache_capacity](#cache_capacity)
|
||||
|
||||
@@ -25,7 +25,7 @@ icon: material/new-box
|
||||
|
||||
| 类型 | 格式 |
|
||||
|-------------|---------------------------|
|
||||
| `wireguard` | [WireGuard](./wiregaurd/) |
|
||||
| `wireguard` | [WireGuard](./wireguard/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"external_controller": "0.0.0.0:9090",
|
||||
"external_ui": "dashboard"
|
||||
// external_ui_download_detour: "direct"
|
||||
// "external_ui_download_detour": "direct"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"external_controller": "0.0.0.0:9090",
|
||||
"external_ui": "dashboard"
|
||||
// external_ui_download_detour: "direct"
|
||||
// "external_ui_download_detour": "direct"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! 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)
|
||||
@@ -56,9 +60,12 @@ icon: material/alert-decagram
|
||||
"auto_route": true,
|
||||
"iproute2_table_index": 2022,
|
||||
"iproute2_rule_index": 9000,
|
||||
"auto_redirect": false,
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
],
|
||||
"strict_route": true,
|
||||
"route_address": [
|
||||
"0.0.0.0/1",
|
||||
@@ -66,7 +73,6 @@ icon: material/alert-decagram
|
||||
"::/1",
|
||||
"8000::/1"
|
||||
],
|
||||
|
||||
"route_exclude_address": [
|
||||
"192.168.0.0/16",
|
||||
"fc00::/7"
|
||||
@@ -117,7 +123,6 @@ icon: material/alert-decagram
|
||||
"match_domain": []
|
||||
}
|
||||
},
|
||||
|
||||
// Deprecated
|
||||
"gso": false,
|
||||
"inet4_address": [
|
||||
@@ -140,8 +145,8 @@ icon: material/alert-decagram
|
||||
"inet6_route_exclude_address": [
|
||||
"fc00::/7"
|
||||
],
|
||||
|
||||
... // Listen Fields
|
||||
...
|
||||
// Listen Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -273,6 +278,16 @@ Connection output mark used by `auto_redirect`.
|
||||
|
||||
`0x2024` is used 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:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-delete-alert: [gso](#gso)
|
||||
@@ -56,9 +60,12 @@ icon: material/alert-decagram
|
||||
"auto_route": true,
|
||||
"iproute2_table_index": 2022,
|
||||
"iproute2_rule_index": 9000,
|
||||
"auto_redirect": false,
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
],
|
||||
"strict_route": true,
|
||||
"route_address": [
|
||||
"0.0.0.0/1",
|
||||
@@ -270,6 +277,16 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
默认使用 `0x2024`。
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
环回地址是用于使指向指定地址的 TCP 连接连接到来源地址的。
|
||||
|
||||
将选项值设置为 `10.7.0.1` 可实现与 SideStore/StosVPN 相同的行为。
|
||||
|
||||
当启用 `auto_redirect` 时,可以作为网关为局域网设备(而不仅仅是本地)实现相同的行为。
|
||||
|
||||
#### strict_route
|
||||
|
||||
当启用 `auto_route` 时,强制执行严格的路由规则:
|
||||
@@ -398,11 +415,11 @@ UDP NAT 过期时间。
|
||||
|
||||
TCP/IP 栈。
|
||||
|
||||
| 栈 | 描述 |
|
||||
|--------|------------------------------------------------------------------|
|
||||
| system | 基于系统网络栈执行 L3 到 L4 转换 |
|
||||
| gVisor | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
|
||||
| mixed | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
|
||||
| 栈 | 描述 |
|
||||
|----------|-------------------------------------------------------------------------------------------------------|
|
||||
| `system` | 基于系统网络栈执行 L3 到 L4 转换 |
|
||||
| `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
|
||||
| `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
|
||||
|
||||
默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。
|
||||
|
||||
|
||||
@@ -172,14 +172,12 @@ 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
|
||||
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.
|
||||
|
||||
Conflict with `tls_record_fragment`.
|
||||
|
||||
#### tls_fragment_fallback_delay
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
@@ -194,11 +192,6 @@ The fallback value used when TLS segmentation cannot automatically determine the
|
||||
|
||||
Fragment TLS handshake into multiple TLS records 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.
|
||||
|
||||
Conflict with `tls_fragment`.
|
||||
|
||||
### sniff
|
||||
|
||||
```json
|
||||
|
||||
@@ -170,8 +170,6 @@ UDP 连接超时时间。
|
||||
|
||||
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
|
||||
|
||||
与 `tls_record_fragment` 冲突。
|
||||
|
||||
#### tls_fragment_fallback_delay
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
@@ -186,10 +184,6 @@ UDP 连接超时时间。
|
||||
|
||||
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
|
||||
|
||||
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
|
||||
|
||||
与 `tls_fragment` 冲突。
|
||||
|
||||
### sniff
|
||||
|
||||
```json
|
||||
|
||||
@@ -20,6 +20,7 @@ DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/
|
||||
"config_path": "",
|
||||
"verify_client_endpoint": [],
|
||||
"verify_client_url": [],
|
||||
"home": "",
|
||||
"mesh_with": [],
|
||||
"mesh_psk": "",
|
||||
"mesh_psk_file": "",
|
||||
@@ -69,6 +70,10 @@ Setting Array value to a string `__URL__` is equivalent to configuring:
|
||||
{ "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.
|
||||
|
||||
@@ -10,7 +10,7 @@ icon: material/new-box
|
||||
|
||||
```json
|
||||
{
|
||||
"endpoints": [
|
||||
"services": [
|
||||
{
|
||||
"type": "",
|
||||
"tag": ""
|
||||
@@ -25,6 +25,7 @@ icon: material/new-box
|
||||
|------------|------------------------|
|
||||
| `derp` | [DERP](./derp) |
|
||||
| `resolved` | [Resolved](./resolved) |
|
||||
| `ssm-api` | [SSM API](./ssm-api) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadow
|
||||
... // Listen Fields
|
||||
|
||||
"servers": {},
|
||||
"cache_path": "",
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
@@ -37,7 +38,7 @@ A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inb
|
||||
|
||||
Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled.
|
||||
|
||||
Example:
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -47,6 +48,11 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
#### 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).
|
||||
|
||||
@@ -4,6 +4,9 @@ icon: material/alert-decagram
|
||||
|
||||
!!! 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)
|
||||
|
||||
@@ -82,6 +85,9 @@ icon: material/alert-decagram
|
||||
"cipher_suites": [],
|
||||
"certificate": "",
|
||||
"certificate_path": "",
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -313,6 +319,44 @@ The path to ECH configuration, in PEM format.
|
||||
|
||||
If empty, load from DNS will be attempted.
|
||||
|
||||
#### 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
|
||||
|
||||
#### domain
|
||||
|
||||
@@ -4,6 +4,9 @@ icon: material/alert-decagram
|
||||
|
||||
!!! 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-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
|
||||
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
|
||||
|
||||
@@ -82,6 +85,9 @@ icon: material/alert-decagram
|
||||
"cipher_suites": [],
|
||||
"certificate": [],
|
||||
"certificate_path": "",
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"pq_signature_schemes_enabled": false,
|
||||
@@ -305,6 +311,41 @@ ECH PEM 配置路径
|
||||
如果为 true,则始终使用最大可能的 TLS 记录大小。
|
||||
如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
|
||||
|
||||
#### 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 记录来绕过防火墙检测。
|
||||
|
||||
### ACME 字段
|
||||
|
||||
#### domain
|
||||
|
||||
@@ -94,18 +94,13 @@ flowchart TB
|
||||
"servers": [
|
||||
{
|
||||
"tag": "google",
|
||||
"address": "tls://8.8.8.8"
|
||||
"type": "tls",
|
||||
"server": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "223.5.5.5",
|
||||
"detour": "direct"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "local"
|
||||
"type": "udp",
|
||||
"server": "223.5.5.5"
|
||||
}
|
||||
],
|
||||
"strategy": "ipv4_only"
|
||||
@@ -115,7 +110,8 @@ flowchart TB
|
||||
"type": "tun",
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
"auto_route": true,
|
||||
"strict_route": false
|
||||
// "auto_redirect": true, // On linux
|
||||
"strict_route": true
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
@@ -123,25 +119,23 @@ flowchart TB
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
"action": "sniff"
|
||||
},
|
||||
{
|
||||
"geoip": [
|
||||
"private"
|
||||
],
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
},
|
||||
{
|
||||
"ip_is_private": true,
|
||||
"outbound": "direct"
|
||||
}
|
||||
],
|
||||
"default_domain_resolver": "local",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -155,18 +149,13 @@ flowchart TB
|
||||
"servers": [
|
||||
{
|
||||
"tag": "google",
|
||||
"address": "tls://8.8.8.8"
|
||||
"type": "tls",
|
||||
"server": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "223.5.5.5",
|
||||
"detour": "direct"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "local"
|
||||
"type": "udp",
|
||||
"server": "223.5.5.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -176,7 +165,8 @@ flowchart TB
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
"inet6_address": "fdfe:dcba:9876::1/126",
|
||||
"auto_route": true,
|
||||
"strict_route": false
|
||||
// "auto_redirect": true, // On linux
|
||||
"strict_route": true
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
@@ -184,25 +174,23 @@ flowchart TB
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
"action": "sniff"
|
||||
},
|
||||
{
|
||||
"geoip": [
|
||||
"private"
|
||||
],
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
},
|
||||
{
|
||||
"ip_is_private": true,
|
||||
"outbound": "direct"
|
||||
}
|
||||
],
|
||||
"default_domain_resolver": "local",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -216,23 +204,22 @@ flowchart TB
|
||||
"servers": [
|
||||
{
|
||||
"tag": "google",
|
||||
"address": "tls://8.8.8.8"
|
||||
"type": "tls",
|
||||
"server": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "223.5.5.5",
|
||||
"detour": "direct"
|
||||
"type": "udp",
|
||||
"server": "223.5.5.5"
|
||||
},
|
||||
{
|
||||
"tag": "remote",
|
||||
"address": "fakeip"
|
||||
"type": "fakeip",
|
||||
"inet4_range": "198.18.0.0/15",
|
||||
"inet6_range": "fc00::/18"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"query_type": [
|
||||
"A",
|
||||
@@ -241,11 +228,6 @@ flowchart TB
|
||||
"server": "remote"
|
||||
}
|
||||
],
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"inet4_range": "198.18.0.0/15",
|
||||
"inet6_range": "fc00::/18"
|
||||
},
|
||||
"independent_cache": true
|
||||
},
|
||||
"inbounds": [
|
||||
@@ -254,6 +236,7 @@ flowchart TB
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
"inet6_address": "fdfe:dcba:9876::1/126",
|
||||
"auto_route": true,
|
||||
// "auto_redirect": true, // On linux
|
||||
"strict_route": true
|
||||
}
|
||||
],
|
||||
@@ -262,25 +245,23 @@ flowchart TB
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
"action": "sniff"
|
||||
},
|
||||
{
|
||||
"geoip": [
|
||||
"private"
|
||||
],
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
},
|
||||
{
|
||||
"ip_is_private": true,
|
||||
"outbound": "direct"
|
||||
}
|
||||
],
|
||||
"default_domain_resolver": "local",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -290,54 +271,6 @@ flowchart TB
|
||||
|
||||
=== ":material-dns: DNS rules"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "google",
|
||||
"address": "tls://8.8.8.8"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "223.5.5.5",
|
||||
"detour": "direct"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Direct",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"server": "google"
|
||||
},
|
||||
{
|
||||
"rule_set": "geosite-geolocation-cn",
|
||||
"server": "local"
|
||||
}
|
||||
]
|
||||
},
|
||||
"route": {
|
||||
"rule_set": [
|
||||
{
|
||||
"type": "remote",
|
||||
"tag": "geosite-geolocation-cn",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-dns: DNS rules (Enhanced, but slower) (1.9.0+)"
|
||||
|
||||
=== ":material-shield-off: With DNS leaks"
|
||||
|
||||
```json
|
||||
@@ -346,35 +279,20 @@ flowchart TB
|
||||
"servers": [
|
||||
{
|
||||
"tag": "google",
|
||||
"address": "tls://8.8.8.8"
|
||||
"type": "tls",
|
||||
"server": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "https://223.5.5.5/dns-query",
|
||||
"detour": "direct"
|
||||
"type": "https",
|
||||
"server": "223.5.5.5"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Direct",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"server": "google"
|
||||
},
|
||||
{
|
||||
"rule_set": "geosite-geolocation-cn",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Default",
|
||||
"server": "google"
|
||||
},
|
||||
{
|
||||
"type": "logical",
|
||||
"mode": "and",
|
||||
@@ -392,6 +310,7 @@ flowchart TB
|
||||
]
|
||||
},
|
||||
"route": {
|
||||
"default_domain_resolver": "local",
|
||||
"rule_set": [
|
||||
{
|
||||
"type": "remote",
|
||||
@@ -425,35 +344,24 @@ flowchart TB
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-security: Without DNS leaks, but slower (1.9.0-alpha.2+)"
|
||||
|
||||
=== ":material-security: Without DNS leaks, but slower"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "google",
|
||||
"address": "tls://8.8.8.8"
|
||||
"type": "tls",
|
||||
"server": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "https://223.5.5.5/dns-query",
|
||||
"detour": "direct"
|
||||
"type": "https",
|
||||
"server": "223.5.5.5"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Direct",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"server": "google"
|
||||
},
|
||||
{
|
||||
"rule_set": "geosite-geolocation-cn",
|
||||
"server": "local"
|
||||
@@ -476,6 +384,7 @@ flowchart TB
|
||||
]
|
||||
},
|
||||
"route": {
|
||||
"default_domain_resolver": "local",
|
||||
"rule_set": [
|
||||
{
|
||||
"type": "remote",
|
||||
@@ -517,14 +426,13 @@ flowchart TB
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"action": "sniff"
|
||||
},
|
||||
{
|
||||
"type": "logical",
|
||||
"mode": "or",
|
||||
@@ -536,20 +444,12 @@ flowchart TB
|
||||
"port": 53
|
||||
}
|
||||
],
|
||||
"outbound": "dns"
|
||||
"action": "hijack-dns"
|
||||
},
|
||||
{
|
||||
"ip_is_private": true,
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Direct",
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"outbound": "default"
|
||||
},
|
||||
{
|
||||
"type": "logical",
|
||||
"mode": "or",
|
||||
@@ -565,12 +465,23 @@ flowchart TB
|
||||
"protocol": "stun"
|
||||
}
|
||||
],
|
||||
"outbound": "block"
|
||||
"action": "reject"
|
||||
},
|
||||
{
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-geolocation-cn"
|
||||
"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"
|
||||
}
|
||||
@@ -591,4 +502,4 @@ flowchart TB
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
10
go.mod
10
go.mod
@@ -30,12 +30,12 @@ require (
|
||||
github.com/sagernet/quic-go v0.52.0-beta.1
|
||||
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b
|
||||
github.com/sagernet/sing-mux v0.3.2
|
||||
github.com/sagernet/sing-quic v0.5.0-beta.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
||||
github.com/sagernet/sing-quic v0.5.0-beta.2
|
||||
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.6.6-0.20250428031943-0686f8c4f210
|
||||
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8
|
||||
github.com/sagernet/sing-tun v0.6.10-0.20250630100036-8763c24e4935
|
||||
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88
|
||||
github.com/sagernet/smux v1.5.34-mod.2
|
||||
github.com/sagernet/tailscale v1.80.3-mod.5
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.7
|
||||
|
||||
24
go.sum
24
go.sum
@@ -165,8 +165,6 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
||||
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.51.0-beta.5 h1:/mME3sJvQ8k/JKP0oC/9XoWrm0znO7hWXviB5yiipJY=
|
||||
github.com/sagernet/quic-go v0.51.0-beta.5/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
|
||||
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
|
||||
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
|
||||
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
@@ -174,20 +172,18 @@ github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b h1:ZjTCYPb5f7aHdf
|
||||
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
|
||||
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
|
||||
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3 h1:1J+s1yyZ8+YAYaClI+az8YuFgV9NGXUUCZnriKmos6w=
|
||||
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3/go.mod h1:Mv7CdSyLepmqoLT8rd88Qn3QMv5AbsgjEm3DvEhDVNE=
|
||||
github.com/sagernet/sing-quic v0.5.0-beta.1 h1:nC0i/s8LhlZB8ev6laZCXF/uiwAE4kRdT4PcDdE4rI4=
|
||||
github.com/sagernet/sing-quic v0.5.0-beta.1/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-quic v0.5.0-beta.2 h1:j7KAbBuGmsKwSxVAQL5soJ+wDqxim4/llK2kxB0hSKk=
|
||||
github.com/sagernet/sing-quic v0.5.0-beta.2/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
|
||||
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.6.6-0.20250428031943-0686f8c4f210 h1:6H4BZaTqKI3YcDMyTV3E576LuJM4S4wY99xoq2T1ECw=
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8 h1:zW+zAOCxUIqBCgnZiPovt1uQ3S+zBS+w0NGp+1zITGA=
|
||||
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
|
||||
github.com/sagernet/sing-tun v0.6.10-0.20250630100036-8763c24e4935 h1:wha4BG4mrEKaIoouVyiU5BcPfKD1n0LkiL4vqdjaVps=
|
||||
github.com/sagernet/sing-tun v0.6.10-0.20250630100036-8763c24e4935/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 h1:0pVm8sPOel+BoiCddW3pV3cKDKEaSioVTYDdTSKjyFI=
|
||||
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
|
||||
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
|
||||
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=
|
||||
|
||||
@@ -3,6 +3,7 @@ package include
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
@@ -39,6 +40,10 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func Context(ctx context.Context) context.Context {
|
||||
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
|
||||
}
|
||||
|
||||
func InboundRegistry() *inbound.Registry {
|
||||
registry := inbound.NewRegistry()
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ type DialerOptions struct {
|
||||
type _DomainResolveOptions struct {
|
||||
Server string `json:"server"`
|
||||
Strategy DomainStrategy `json:"strategy,omitempty"`
|
||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
|
||||
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
|
||||
@@ -102,6 +103,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) {
|
||||
if o.Server == "" {
|
||||
return []byte("{}"), nil
|
||||
} else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) &&
|
||||
o.Timeout == 0 &&
|
||||
!o.DisableCache &&
|
||||
o.RewriteTTL == nil &&
|
||||
o.ClientSubnet == nil {
|
||||
|
||||
@@ -180,6 +180,7 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
|
||||
type DNSRouteActionOptions struct {
|
||||
Server string `json:"server,omitempty"`
|
||||
Strategy DomainStrategy `json:"strategy,omitempty"`
|
||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
|
||||
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
|
||||
@@ -187,6 +188,7 @@ type DNSRouteActionOptions struct {
|
||||
|
||||
type _DNSRouteOptionsActionOptions struct {
|
||||
Strategy DomainStrategy `json:"strategy,omitempty"`
|
||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
|
||||
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
type SSMAPIServiceOptions struct {
|
||||
ListenOptions
|
||||
Servers *badjson.TypedMap[string, string] `json:"servers"`
|
||||
Servers *badjson.TypedMap[string, string] `json:"servers"`
|
||||
CachePath string `json:"cache_path,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ type DERPServiceOptions struct {
|
||||
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"`
|
||||
|
||||
@@ -37,19 +37,22 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
|
||||
}
|
||||
|
||||
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"`
|
||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||
Reality *OutboundRealityOptions `json:"reality,omitempty"`
|
||||
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"`
|
||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
Fragment bool `json:"fragment,omitempty"`
|
||||
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
|
||||
RecordFragment bool `json:"record_fragment,omitempty"`
|
||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||
Reality *OutboundRealityOptions `json:"reality,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundTLSOptionsContainer struct {
|
||||
|
||||
@@ -20,6 +20,7 @@ type TunInboundOptions struct {
|
||||
AutoRedirect bool `json:"auto_redirect,omitempty"`
|
||||
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
|
||||
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,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"`
|
||||
|
||||
@@ -221,6 +221,14 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
|
||||
udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
|
||||
|
||||
@@ -190,6 +190,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
IPRoute2RuleIndex: ruleIndex,
|
||||
AutoRedirectInputMark: inputMark,
|
||||
AutoRedirectOutputMark: outputMark,
|
||||
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,
|
||||
|
||||
@@ -205,6 +205,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
//nolint:staticcheck
|
||||
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
|
||||
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
|
||||
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
@@ -219,6 +219,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||
//nolint:staticcheck
|
||||
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
|
||||
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
|
||||
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
3
release/config/sing-box.postinst
Normal file
3
release/config/sing-box.postinst
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
systemd-sysusers sing-box.conf
|
||||
@@ -90,14 +90,8 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
|
||||
m.logger.ErrorContext(ctx, err)
|
||||
return
|
||||
}
|
||||
if metadata.TLSFragment {
|
||||
fallbackDelay := metadata.TLSFragmentFallbackDelay
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = C.TLSFragmentFallbackDelay
|
||||
}
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, false, fallbackDelay)
|
||||
} else if metadata.TLSRecordFragment {
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, true, 0)
|
||||
if metadata.TLSFragment || metadata.TLSRecordFragment {
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay)
|
||||
}
|
||||
m.access.Lock()
|
||||
element := m.connections.PushBack(conn)
|
||||
|
||||
17
route/dns.go
17
route/dns.go
@@ -2,7 +2,6 @@ package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +9,7 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
|
||||
"github.com/sagernet/sing-tun"
|
||||
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"
|
||||
@@ -36,7 +35,7 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) error {
|
||||
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 {
|
||||
@@ -51,10 +50,12 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
|
||||
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")
|
||||
}
|
||||
@@ -63,7 +64,7 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
|
||||
|
||||
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 && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
|
||||
if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) {
|
||||
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
|
||||
}
|
||||
}
|
||||
@@ -93,8 +94,16 @@ type dnsHijacker struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp
|
||||
DomainResolver: defaultDomainResolver.Server,
|
||||
DomainResolveOptions: adapter.DNSQueryOptions{
|
||||
Strategy: C.DomainStrategy(defaultDomainResolver.Strategy),
|
||||
Timeout: time.Duration(defaultDomainResolver.Timeout),
|
||||
DisableCache: defaultDomainResolver.DisableCache,
|
||||
RewriteTTL: defaultDomainResolver.RewriteTTL,
|
||||
ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/sniff"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route/rule"
|
||||
R "github.com/sagernet/sing-box/route/rule"
|
||||
"github.com/sagernet/sing-mux"
|
||||
"github.com/sagernet/sing-vmess"
|
||||
"github.com/sagernet/sing/common"
|
||||
@@ -49,7 +49,7 @@ func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata
|
||||
err := r.routeConnection(ctx, conn, metadata, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
|
||||
r.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, err)
|
||||
@@ -99,7 +99,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
var selectedOutbound adapter.Outbound
|
||||
if selectedRule != nil {
|
||||
switch action := selectedRule.Action().(type) {
|
||||
case *rule.RuleActionRoute:
|
||||
case *R.RuleActionRoute:
|
||||
var loaded bool
|
||||
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
|
||||
if !loaded {
|
||||
@@ -110,14 +110,15 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
buf.ReleaseMulti(buffers)
|
||||
return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag())
|
||||
}
|
||||
case *rule.RuleActionReject:
|
||||
case *R.RuleActionReject:
|
||||
buf.ReleaseMulti(buffers)
|
||||
return action.Error(ctx)
|
||||
case *rule.RuleActionHijackDNS:
|
||||
case *R.RuleActionHijackDNS:
|
||||
for _, buffer := range buffers {
|
||||
conn = bufio.NewCachedConn(conn, buffer)
|
||||
}
|
||||
return r.hijackDNSStream(ctx, conn, metadata)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if selectedRule == nil {
|
||||
@@ -150,7 +151,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
}))
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
|
||||
r.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, err)
|
||||
@@ -167,13 +168,11 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
err := r.routePacketConnection(ctx, conn, metadata, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
|
||||
r.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
} else if onClose != nil {
|
||||
onClose(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +214,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
var selectReturn bool
|
||||
if selectedRule != nil {
|
||||
switch action := selectedRule.Action().(type) {
|
||||
case *rule.RuleActionRoute:
|
||||
case *R.RuleActionRoute:
|
||||
var loaded bool
|
||||
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
|
||||
if !loaded {
|
||||
@@ -226,12 +225,11 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
N.ReleaseMultiPacketBuffer(packetBuffers)
|
||||
return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag())
|
||||
}
|
||||
case *rule.RuleActionReject:
|
||||
case *R.RuleActionReject:
|
||||
N.ReleaseMultiPacketBuffer(packetBuffers)
|
||||
return action.Error(ctx)
|
||||
case *rule.RuleActionHijackDNS:
|
||||
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
|
||||
|
||||
case *R.RuleActionHijackDNS:
|
||||
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
|
||||
}
|
||||
}
|
||||
if selectedRule == nil || selectReturn {
|
||||
@@ -268,7 +266,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error {
|
||||
if selectedRule == nil {
|
||||
return nil
|
||||
}
|
||||
rejectAction, isReject := selectedRule.Action().(*rule.RuleActionReject)
|
||||
rejectAction, isReject := selectedRule.Action().(*R.RuleActionReject)
|
||||
if !isReject {
|
||||
return nil
|
||||
}
|
||||
@@ -344,7 +342,7 @@ func (r *Router) matchRule(
|
||||
//nolint:staticcheck
|
||||
if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() {
|
||||
if !preMatch && metadata.InboundOptions.SniffEnabled {
|
||||
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{
|
||||
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &R.RuleActionSniff{
|
||||
OverrideDestination: metadata.InboundOptions.SniffOverrideDestination,
|
||||
Timeout: time.Duration(metadata.InboundOptions.SniffTimeout),
|
||||
}, inputConn, inputPacketConn, nil)
|
||||
@@ -359,7 +357,7 @@ func (r *Router) matchRule(
|
||||
}
|
||||
}
|
||||
if C.DomainStrategy(metadata.InboundOptions.DomainStrategy) != C.DomainStrategyAsIS {
|
||||
fatalErr = r.actionResolve(ctx, metadata, &rule.RuleActionResolve{
|
||||
fatalErr = r.actionResolve(ctx, metadata, &R.RuleActionResolve{
|
||||
Strategy: C.DomainStrategy(metadata.InboundOptions.DomainStrategy),
|
||||
})
|
||||
if fatalErr != nil {
|
||||
@@ -396,11 +394,11 @@ match:
|
||||
}
|
||||
}
|
||||
}
|
||||
var routeOptions *rule.RuleActionRouteOptions
|
||||
var routeOptions *R.RuleActionRouteOptions
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *rule.RuleActionRoute:
|
||||
case *R.RuleActionRoute:
|
||||
routeOptions = &action.RuleActionRouteOptions
|
||||
case *rule.RuleActionRouteOptions:
|
||||
case *R.RuleActionRouteOptions:
|
||||
routeOptions = action
|
||||
}
|
||||
if routeOptions != nil {
|
||||
@@ -453,7 +451,7 @@ match:
|
||||
}
|
||||
}
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *rule.RuleActionSniff:
|
||||
case *R.RuleActionSniff:
|
||||
if !preMatch {
|
||||
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers)
|
||||
if newErr != nil {
|
||||
@@ -470,7 +468,7 @@ match:
|
||||
selectedRuleIndex = currentRuleIndex
|
||||
break match
|
||||
}
|
||||
case *rule.RuleActionResolve:
|
||||
case *R.RuleActionResolve:
|
||||
fatalErr = r.actionResolve(ctx, metadata, action)
|
||||
if fatalErr != nil {
|
||||
return
|
||||
@@ -490,7 +488,7 @@ match:
|
||||
}
|
||||
|
||||
func (r *Router) actionSniff(
|
||||
ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionSniff,
|
||||
ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff,
|
||||
inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer,
|
||||
) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) {
|
||||
if sniff.Skip(metadata) {
|
||||
@@ -501,7 +499,12 @@ func (r *Router) actionSniff(
|
||||
return
|
||||
}
|
||||
if inputConn != nil {
|
||||
sniffBuffer := buf.NewPacket()
|
||||
if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 {
|
||||
return
|
||||
} else if 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
|
||||
@@ -515,6 +518,7 @@ func (r *Router) actionSniff(
|
||||
sniff.RDP,
|
||||
}
|
||||
}
|
||||
sniffBuffer := buf.NewPacket()
|
||||
err := sniff.PeekStream(
|
||||
ctx,
|
||||
metadata,
|
||||
@@ -524,6 +528,7 @@ func (r *Router) actionSniff(
|
||||
action.Timeout,
|
||||
streamSniffers...,
|
||||
)
|
||||
metadata.SniffError = err
|
||||
if err == nil {
|
||||
//goland:noinspection GoDeprecation
|
||||
if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
|
||||
@@ -546,9 +551,25 @@ func (r *Router) actionSniff(
|
||||
sniffBuffer.Release()
|
||||
}
|
||||
} else if inputPacketConn != nil {
|
||||
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
|
||||
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
|
||||
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
|
||||
return
|
||||
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
|
||||
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
|
||||
return
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
for {
|
||||
var (
|
||||
@@ -581,7 +602,7 @@ func (r *Router) actionSniff(
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if len(packetBuffers) > 0 || metadata.PacketSniffError != nil {
|
||||
if len(packetBuffers) > 0 || metadata.SniffError != nil {
|
||||
err = sniff.PeekPacket(
|
||||
ctx,
|
||||
metadata,
|
||||
@@ -589,20 +610,6 @@ func (r *Router) actionSniff(
|
||||
sniff.QUICClientHello,
|
||||
)
|
||||
} else {
|
||||
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,
|
||||
}
|
||||
}
|
||||
err = sniff.PeekPacket(
|
||||
ctx, metadata,
|
||||
sniffBuffer.Bytes(),
|
||||
@@ -615,7 +622,7 @@ func (r *Router) actionSniff(
|
||||
Destination: destination,
|
||||
}
|
||||
packetBuffers = append(packetBuffers, packetBuffer)
|
||||
metadata.PacketSniffError = err
|
||||
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")
|
||||
@@ -646,7 +653,7 @@ func (r *Router) actionSniff(
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionResolve) error {
|
||||
func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionResolve) error {
|
||||
if metadata.Destination.IsFqdn() {
|
||||
var transport adapter.DNSTransport
|
||||
if action.Server != "" {
|
||||
@@ -659,6 +666,7 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon
|
||||
addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: action.Strategy,
|
||||
Timeout: action.Timeout,
|
||||
DisableCache: action.DisableCache,
|
||||
RewriteTTL: action.RewriteTTL,
|
||||
ClientSubnet: action.ClientSubnet,
|
||||
|
||||
@@ -2,6 +2,7 @@ package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -112,6 +113,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
|
||||
Server: action.RouteOptions.Server,
|
||||
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
|
||||
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
|
||||
Timeout: time.Duration(action.RouteOptions.Timeout),
|
||||
DisableCache: action.RouteOptions.DisableCache,
|
||||
RewriteTTL: action.RouteOptions.RewriteTTL,
|
||||
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
|
||||
@@ -120,6 +122,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
return &RuleActionDNSRouteOptions{
|
||||
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
|
||||
Timeout: time.Duration(action.RouteOptionsOptions.Timeout),
|
||||
DisableCache: action.RouteOptionsOptions.DisableCache,
|
||||
RewriteTTL: action.RouteOptionsOptions.RewriteTTL,
|
||||
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
|
||||
@@ -234,20 +237,13 @@ func (r *RuleActionDNSRoute) Type() string {
|
||||
func (r *RuleActionDNSRoute) String() string {
|
||||
var descriptions []string
|
||||
descriptions = append(descriptions, r.Server)
|
||||
if r.DisableCache {
|
||||
descriptions = append(descriptions, "disable-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))
|
||||
}
|
||||
descriptions = append(descriptions, r.Descriptions()...)
|
||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||
}
|
||||
|
||||
type RuleActionDNSRouteOptions struct {
|
||||
Strategy C.DomainStrategy
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
@@ -258,7 +254,17 @@ func (r *RuleActionDNSRouteOptions) Type() string {
|
||||
}
|
||||
|
||||
func (r *RuleActionDNSRouteOptions) String() string {
|
||||
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
|
||||
}
|
||||
|
||||
func (r *RuleActionDNSRouteOptions) Descriptions() []string {
|
||||
var descriptions []string
|
||||
if r.Strategy != C.DomainStrategyAsIS {
|
||||
descriptions = append(descriptions, F.ToString("strategy=", option.DomainStrategy(r.Strategy)))
|
||||
}
|
||||
if r.Timeout > 0 {
|
||||
descriptions = append(descriptions, F.ToString("timeout=", r.Timeout.String()))
|
||||
}
|
||||
if r.DisableCache {
|
||||
descriptions = append(descriptions, "disable-cache")
|
||||
}
|
||||
@@ -268,7 +274,7 @@ func (r *RuleActionDNSRouteOptions) String() string {
|
||||
if r.ClientSubnet.IsValid() {
|
||||
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
|
||||
}
|
||||
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
||||
return descriptions
|
||||
}
|
||||
|
||||
type RuleActionDirect struct {
|
||||
@@ -284,6 +290,23 @@ 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 RuleActionReject struct {
|
||||
Method string
|
||||
NoDrop bool
|
||||
@@ -307,9 +330,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
|
||||
var returnErr error
|
||||
switch r.Method {
|
||||
case C.RuleActionRejectMethodDefault:
|
||||
returnErr = syscall.ECONNREFUSED
|
||||
returnErr = &RejectedError{syscall.ECONNREFUSED}
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return tun.ErrDrop
|
||||
return &RejectedError{tun.ErrDrop}
|
||||
default:
|
||||
panic(F.ToString("unknown reject method: ", r.Method))
|
||||
}
|
||||
@@ -327,7 +350,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
|
||||
if ctx != nil {
|
||||
r.logger.DebugContext(ctx, "dropped due to flooding")
|
||||
}
|
||||
return tun.ErrDrop
|
||||
return &RejectedError{tun.ErrDrop}
|
||||
}
|
||||
return returnErr
|
||||
}
|
||||
@@ -403,6 +426,7 @@ func (r *RuleActionSniff) String() string {
|
||||
type RuleActionResolve struct {
|
||||
Server string
|
||||
Strategy C.DomainStrategy
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
@@ -420,6 +444,9 @@ func (r *RuleActionResolve) String() string {
|
||||
if r.Strategy != C.DomainStrategyAsIS {
|
||||
options = append(options, F.ToString(option.DomainStrategy(r.Strategy)))
|
||||
}
|
||||
if r.Timeout > 0 {
|
||||
options = append(options, F.ToString("timeout=", r.Timeout.String()))
|
||||
}
|
||||
if r.DisableCache {
|
||||
options = append(options, "disable_cache")
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
configPath: configPath,
|
||||
verifyClientEndpoint: options.VerifyClientEndpoint,
|
||||
verifyClientURL: options.VerifyClientURL,
|
||||
home: options.Home,
|
||||
meshKey: options.MeshPSK,
|
||||
meshKeyPath: options.MeshPSKFile,
|
||||
meshWith: options.MeshWith,
|
||||
@@ -133,7 +134,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
func (d *Service) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
config, err := readDERPConfig(d.configPath)
|
||||
config, err := readDERPConfig(filemanager.BasePath(d.ctx, d.configPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
222
service/ssmapi/cache.go
Normal file
222
service/ssmapi/cache.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package ssmapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/sagernet/sing/common/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
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) saveCache() error {
|
||||
if s.cachePath == "" {
|
||||
return nil
|
||||
}
|
||||
basePath := filemanager.BasePath(s.ctx, s.cachePath)
|
||||
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheBinary, err := s.encodeCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.cachePath, cacheBinary, 0o644)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -33,6 +33,9 @@ type Service struct {
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
httpServer *http.Server
|
||||
traffics map[string]*TrafficManager
|
||||
users map[string]*UserManager
|
||||
cachePath string
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
|
||||
@@ -50,6 +53,9 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
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 {
|
||||
@@ -58,7 +64,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
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")
|
||||
return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, " not found")
|
||||
}
|
||||
managedServer, isManaged := inbound.(adapter.ManagedSSMServer)
|
||||
if !isManaged {
|
||||
@@ -68,6 +74,8 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
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))
|
||||
@@ -83,8 +91,12 @@ 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"))
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
err = s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
@@ -109,6 +121,10 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
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),
|
||||
|
||||
@@ -22,7 +22,7 @@ func NewUserManager(inbound adapter.ManagedSSMServer, trafficManager *TrafficMan
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserManager) postUpdate() error {
|
||||
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 {
|
||||
@@ -33,7 +33,9 @@ func (m *UserManager) postUpdate() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.trafficManager.UpdateUsers(users)
|
||||
if updated {
|
||||
m.trafficManager.UpdateUsers(users)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -55,10 +57,10 @@ 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")
|
||||
return E.New("user ", username, " already exists")
|
||||
}
|
||||
m.usersMap[username] = password
|
||||
return m.postUpdate()
|
||||
return m.postUpdate(true)
|
||||
}
|
||||
|
||||
func (m *UserManager) Get(username string) (string, bool) {
|
||||
@@ -74,12 +76,12 @@ func (m *UserManager) Update(username string, password string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.usersMap[username] = password
|
||||
return m.postUpdate()
|
||||
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()
|
||||
return m.postUpdate(true)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestMain(m *testing.M) {
|
||||
var globalCtx context.Context
|
||||
|
||||
func init() {
|
||||
globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
|
||||
globalCtx = include.Context(context.Background())
|
||||
}
|
||||
|
||||
func startInstance(t *testing.T, options option.Options) *box.Box {
|
||||
|
||||
@@ -47,6 +47,7 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
|
||||
server := &Server{
|
||||
ctx: ctx,
|
||||
tlsConfig: tlsConfig,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
h2Server: &http2.Server{
|
||||
IdleTimeout: time.Duration(options.IdleTimeout),
|
||||
|
||||
@@ -3,6 +3,7 @@ package v2raywebsocket
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
@@ -67,9 +68,10 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) {
|
||||
err = wrapWsError(err)
|
||||
return
|
||||
}
|
||||
header, err = c.reader.NextFrame()
|
||||
header, err = wrapWsError0(c.reader.NextFrame())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -78,14 +80,14 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
|
||||
err = wsutil.ErrFrameTooLarge
|
||||
return
|
||||
}
|
||||
err = c.controlHandler(header, c.reader)
|
||||
err = wrapWsError(c.controlHandler(header, c.reader))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if header.OpCode&ws.OpBinary == 0 {
|
||||
err = c.reader.Discard()
|
||||
err = wrapWsError(c.reader.Discard())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -95,7 +97,7 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *WebsocketConn) Write(p []byte) (n int, err error) {
|
||||
err = wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p)
|
||||
err = wrapWsError(wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -146,7 +148,7 @@ func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) {
|
||||
return 0, c.err
|
||||
}
|
||||
}
|
||||
return c.conn.Read(b)
|
||||
return wrapWsError0(c.conn.Read(b))
|
||||
}
|
||||
|
||||
func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
|
||||
@@ -191,7 +193,7 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
|
||||
|
||||
func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
|
||||
if c.conn != nil {
|
||||
return c.conn.Write(b)
|
||||
return wrapWsError0(c.conn.Write(b))
|
||||
}
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
@@ -199,7 +201,7 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
|
||||
return 0, c.err
|
||||
}
|
||||
if c.conn != nil {
|
||||
return c.conn.Write(b)
|
||||
return wrapWsError0(c.conn.Write(b))
|
||||
}
|
||||
err = c.writeRequest(b)
|
||||
c.err = err
|
||||
@@ -212,12 +214,12 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
|
||||
|
||||
func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
if c.conn != nil {
|
||||
return c.conn.WriteBuffer(buffer)
|
||||
return wrapWsError(c.conn.WriteBuffer(buffer))
|
||||
}
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if c.conn != nil {
|
||||
return c.conn.WriteBuffer(buffer)
|
||||
return wrapWsError(c.conn.WriteBuffer(buffer))
|
||||
}
|
||||
if c.err != nil {
|
||||
return c.err
|
||||
@@ -272,3 +274,23 @@ func (c *EarlyWebsocketConn) Upstream() any {
|
||||
func (c *EarlyWebsocketConn) LazyHeadroom() bool {
|
||||
return c.conn == 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package v2raywebsocket
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type deadConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *deadConn) SetDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *deadConn) SetReadDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *deadConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (w *Writer) WriteBuffer(buffer *buf.Buffer) error {
|
||||
ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0)
|
||||
}
|
||||
|
||||
return w.writer.WriteBuffer(buffer)
|
||||
return wrapWsError(w.writer.WriteBuffer(buffer))
|
||||
}
|
||||
|
||||
func (w *Writer) FrontHeadroom() int {
|
||||
|
||||
Reference in New Issue
Block a user