Compare commits

..

67 Commits

Author SHA1 Message Date
世界
470c605118 option: add round-trip test for DNSRuleAction with evaluate action 2026-03-31 17:52:54 +08:00
世界
3795ac1503 dns: add evaluate integration tests for response_rcode, response_ns, response_extra 2026-03-31 17:45:35 +08:00
世界
8be4f38eaf dns: remove redundant DNSResponse assignment in addressLimitResponseCheck
MatchAddressLimit internally copies metadata and sets DNSResponse,
making the prior assignment in the closure unnecessary.
2026-03-31 17:39:51 +08:00
世界
4152022b89 dns: remove redundant queryOptions variable 2026-03-31 17:31:09 +08:00
世界
5b01eaa149 dns: remove dead lookupStrategyAllowsQueryType helper 2026-03-31 17:28:52 +08:00
世界
3d32ad79cd dns: remove dead lookup strategy guard in lookupWithRulesType 2026-03-31 17:24:03 +08:00
世界
e49a7bcc86 adapter: remove unused DestinationAddressesForMatch 2026-03-31 17:21:06 +08:00
世界
c94696df9e dns: fix variable shadowing in matchDNSHeadlessRuleStatesForMatch 2026-03-31 17:17:26 +08:00
世界
a7c4096a07 dns: fix err shadowing in buildRules
Reuse the outer err variable in the rule-construction and rule-startup
loops instead of redeclaring it with :=, and declare dnsRule separately.
2026-03-31 17:13:18 +08:00
世界
0e87476ee5 dns: return immediately on context cancellation in evaluate exchange 2026-03-31 17:08:52 +08:00
世界
21f3acef81 dns: reject method reply is not supported for DNS rules
Add config-time validation in NewDNSRule that rejects
RejectMethodReply for both default and logical DNS rules,
matching the existing TCP/UDP validation in route/route.go.
2026-03-31 16:53:57 +08:00
世界
861fa897e0 dns: improve test coverage and cleanup
- Add t.Cleanup(router.Close) in newTestRouter for automatic cleanup
- Remove unnecessary testCase loop variable capture (Go 1.22+)
- Add tests for reject drop action, route_options effect, and
  chained evaluate response overwrite
2026-03-31 15:53:38 +08:00
世界
c471d7fee7 dns: fix test style issues in repro_test.go
- Rename addrs to addresses per naming conventions
- Replace errors.New with E.New per error-handling rules
2026-03-31 15:53:30 +08:00
世界
7de00c7cd1 fix: add missing EnvName, document Strategy invariant, improve rcode display
- Add EnvName to four new deprecation constants so users can suppress
  warnings via ENABLE_DEPRECATED_* environment variables
- Add comment explaining why applyDNSRouteOptions skips Strategy
- Use dns.RcodeToString in DNSResponseRCodeItem.String() for readability
- Remove redundant Fqdn(FqdnToDomain(domain)) round-trip
2026-03-31 15:47:29 +08:00
世界
29024191ee docs: fix strategy deprecation format, explain legacyDNSMode, unify CN/EN order
- Use standard !!! failure block for strategy deprecation notice
- Add Legacy DNS Mode section explaining automatic mode detection
- Reorder ip_accept_any/rule_set_ip_cidr_accept_empty in Chinese docs
  to match English
2026-03-31 15:43:04 +08:00
世界
16480095f7 dns: populate reverse mapping for legacy predefined responses
The legacy path returned predefined responses early, bypassing the
reverse mapping cache. Use goto to reach the shared post-exchange
block so both legacy and new paths record predefined A/AAAA answers.
2026-03-31 15:37:10 +08:00
世界
cf33f1f375 route/rule: remove dead IgnoreDestinationIPCIDRMatch field
The field was never set to true after the legacy pre-match refactor
in 3549c02b8. Remove the declaration, guard check, and redundant
false assignments.
2026-03-31 15:29:50 +08:00
世界
1d872a6835 dns: use refcounted snapshot to narrow rule lock scope
Exchange and Lookup held rulesAccess.RLock across all DNS network I/O,
blocking rebuildRules from swapping in new rules until every in-flight
query finished. Replace the RWMutex with an atomic pointer to a
refcounted rulesSnapshot so queries only hold a snapshot reference
during execution, allowing concurrent rule rebuilds.
2026-03-31 15:29:16 +08:00
世界
fb19bf6111 dns: serialize rebuilds and keep last good rules on failure 2026-03-31 13:15:25 +08:00
世界
da210af48d docs: fix grammar errors and typos 2026-03-31 10:24:40 +08:00
世界
6a351be73a Suppress SA1019 lint warnings for intentional deprecated field usage 2026-03-31 07:56:13 +08:00
世界
1913376113 docs: add evaluate action, response matching fields, and deprecation notices 2026-03-31 07:56:13 +08:00
世界
b05f58b469 Use typed SVCB hint structs instead of string parsing 2026-03-31 07:56:13 +08:00
世界
baf1da892b option: reject nested rule actions 2026-03-31 07:56:13 +08:00
世界
f3c8fe59ac dns: make rule strategy legacy-only 2026-03-31 07:56:13 +08:00
世界
117422db68 Make DNS match_response fail as a normal condition 2026-03-31 07:56:13 +08:00
世界
5b32dbf57f Fix DNS rule-set ref handling 2026-03-31 07:56:12 +08:00
世界
d103fc2aea Fix legacy DNS rule_set accept_empty matching 2026-03-31 07:56:12 +08:00
世界
532f350637 dns: restore lookup reject semantics 2026-03-31 07:56:12 +08:00
世界
d82e7cd4b6 Fix DNS record parser file inclusion and rule match log index
Remove SetIncludeAllowed(true) from the DNS record zone parser.
The $INCLUDE directive allows opening arbitrary files via os.Open,
which is unnecessary and dangerous when parsing a single record string
from configuration (especially remote profiles).

Fix displayRuleIndex arithmetic in dns/router.go that computed
2*index+1 instead of the correct 0-based index. This was a
reintroduction of a bug previously fixed in be8ee370a. Both
matchDNS and logRuleMatch now use the index directly, matching
the pattern in route/route.go.
2026-03-31 07:56:12 +08:00
世界
f628519333 Fix DNS record parsing and shutdown race 2026-03-31 07:56:12 +08:00
世界
7def08b5a1 dns: restore init validation and fix rule-set query type 2026-03-31 07:56:12 +08:00
世界
8ba8ad5f0c dns: make rule path selection rule-set aware 2026-03-31 07:56:12 +08:00
世界
07f2fd65b2 dns: complete lookup rule execution in new mode 2026-03-31 07:56:11 +08:00
世界
31c707f8e8 Fix legacy DNS negation expansion 2026-03-31 07:56:11 +08:00
世界
3549c02b8c dns: isolate legacy pre-match semantics 2026-03-31 07:56:11 +08:00
世界
e5aaf782c6 dns: preserve legacy address-filter pre-match semantics
Legacy DNS address-filter mode still accepts destination-side IP
predicates with a deprecation warning, but the recent evaluate/
match_response refactor started evaluating those predicates during
pre-response Match(). That broke rules whose transport selection must
be deferred until MatchAddressLimit() can inspect the upstream reply.

Restore the old defer behavior by reintroducing an internal
IgnoreDestinationIPCIDRMatch flag on InboundContext and using it only
for legacy pre-response DNS matching. Default and logical DNS rules now
carry the legacy mode bit, set the ignore flag on metadata copies while
performing pre-response Match(), and explicitly clear it again for
match_response and MatchAddressLimit() so response-phase matching still
checks the returned addresses.

Add regression coverage for direct legacy destination-IP rules,
rule_set-backed CIDR rules, logical wrappers, and the legacy Lookup
router path, including fallback after a rejected response. This keeps
legacy configs working without changing new-mode evaluate semantics.

Tests: go test ./route/rule ./dns
Tests: make
2026-03-31 07:56:11 +08:00
世界
5b08ae150f Remove legacy DNS server formats 2026-03-31 07:56:11 +08:00
世界
704482bb4a dns: document non-response rule_set address-filter semantics 2026-03-31 07:56:11 +08:00
世界
5436192ada Fix DNS pre-match CIDR fail-closed semantics 2026-03-31 07:56:10 +08:00
世界
d2f005aea3 Fix DNS evaluate regressions 2026-03-31 07:56:10 +08:00
世界
dc9b2089ea dns: use response-only address matching 2026-03-31 07:56:10 +08:00
世界
b16b6f8b18 Fix DNS match_response response address handling 2026-03-31 07:56:10 +08:00
世界
ab414f20f5 Fix DNS record parsing and matching regressions 2026-03-31 07:56:10 +08:00
世界
f8cbe27b39 Fix DNS evaluate routing regressions 2026-03-31 07:56:10 +08:00
世界
2544d26664 Reorder DNS rule item fields: match_response above address filter and response items, deprecated fields at bottom 2026-03-31 07:56:09 +08:00
世界
bcaba94c61 Add evaluate DNS rule action and related rule items 2026-03-31 07:56:09 +08:00
世界
ebf8a213b6 Bump version 2026-03-31 00:38:42 +08:00
世界
ab323e0eb9 Add BBR profile and hop interval randomization for Hysteria2 2026-03-31 00:38:42 +08:00
nekohasekai
2132e68d3a Refactor ACME support to certificate provider 2026-03-30 23:21:50 +08:00
世界
47742abe93 cronet-go: Update chromium to 145.0.7632.159 2026-03-30 23:21:50 +08:00
世界
77e51035bd documentation: Update descriptions for neighbor rules 2026-03-30 23:21:50 +08:00
世界
eeb5dead2a Add macOS support for MAC and hostname rule items 2026-03-30 23:21:50 +08:00
世界
45339d101b Add Android support for MAC and hostname rule items 2026-03-30 23:21:50 +08:00
世界
04c0490992 Add MAC and hostname rule items 2026-03-30 23:21:50 +08:00
世界
7ffdc48b49 Bump version 2026-03-30 23:03:43 +08:00
世界
e15bdf11eb sing: Minor fixes 2026-03-30 22:58:11 +08:00
世界
e3bcb06c3e platform: Add HTTPResponse.WriteToWithProgress 2026-03-30 22:42:36 +08:00
世界
84d2280960 quic: Fix protocol client close & Sync hysteria bbr fix 2026-03-30 22:42:36 +08:00
世界
4fd2532b0a Fix naive quic error message 2026-03-30 22:42:36 +08:00
Zhengchao Ding
02ccde6c71 fix(rpm): add vendor field to fpm config to avoid (none) vendor
Co-authored-by: Hyper <hypar@disroot.org>
2026-03-30 22:09:54 +08:00
世界
e98b4ad449 Fix WireGuard shutdown race crashing
Stop peer goroutines before closing the TUN device to prevent
RoutineSequentialReceiver from calling Write on a nil dispatcher.
2026-03-26 16:33:21 +08:00
世界
d09182614c Bump version 2026-03-26 13:28:33 +08:00
世界
6381de7bab route: Fix query_type never matching in rule_set headless rules 2026-03-26 13:26:18 +08:00
世界
b0c6762bc1 route: merge rule_set branches into outer rules
Treat rule_set items as merged branches instead of standalone boolean
sub-items.

Evaluate each branch inside a referenced rule-set as if it were merged
into the outer rule and keep OR semantics between branches. This lets
outer grouped fields satisfy matching groups inside a branch without
introducing a standalone outer fallback or cross-branch state union.

Keep inherited grouped state outside inverted default and logical
branches. Negated rule-set branches now evaluate !(...) against their
own conditions and only reapply the outer grouped match after negation
succeeds, so configs like outer-group && !inner-condition continue to
work.

Add regression tests for same-group merged matches, cross-group and
extra-AND failures, DNS merged-branch behaviour, and inverted merged
branches. Update the route and DNS rule docs to clarify that rule-set
branches merge into the outer rule while keeping OR semantics between
branches.
2026-03-25 14:00:29 +08:00
世界
7425100bac release: Refactor release tracks for Linux packages and Docker
Support 4 release tracks instead of 2:
- sing-box / latest (stable release)
- sing-box-beta / latest-beta (stable pre-release)
- sing-box-testing / latest-testing (testing branch)
- sing-box-oldstable / latest-oldstable (oldstable branch)

Track is detected via git branch --contains and git tag,
replacing the old version-string hyphen check.
2026-03-24 15:03:43 +08:00
世界
d454aa0fdf route: formalize nested rule_set group-state semantics
Before 795d1c289, nested rule-set evaluation reused the parent rule
match cache. In practice, this meant these fields leaked across nested
evaluation:

- SourceAddressMatch
- SourcePortMatch
- DestinationAddressMatch
- DestinationPortMatch
- DidMatch

That leak had two opposite effects.

First, it made included rule-sets partially behave like the docs'
"merged" semantics. For example, if an outer route rule had:

  rule_set = ["geosite-additional-!cn"]
  ip_cidr  = 104.26.10.0/24

and the inline rule-set matched `domain_suffix = speedtest.net`, the
inner match could set `DestinationAddressMatch = true` and the outer
rule would then pass its destination-address group check. This is why
some `rule_set + ip_cidr` combinations used to work.

But the same leak also polluted sibling rules and sibling rule-sets.
A branch could partially match one group, then fail later, and still
leave that group cache set for the next branch. This broke cases such
as gh-3485: with `rule_set = [test1, test2]`, `test1` could touch
destination-address cache before an AdGuard `@@` exclusion made the
whole branch fail, and `test2` would then run against dirty state.

795d1c289 fixed that by cloning metadata for nested rule-set/rule
evaluation and resetting the rule match cache for each branch. That
stopped sibling pollution, but it also removed the only mechanism by
which a successful nested branch could affect the parent rule's grouped
matching state.

As a result, nested rule-sets became pure boolean sub-items against the
outer rule. The previous example stopped working: the inner
`domain_suffix = speedtest.net` still matched, but the outer rule no
longer observed any destination-address-group success, so it fell
through to `final`.

This change makes the semantics explicit instead of relying on cache
side effects:

- `rule_set: ["a", "b"]` is OR
- rules inside one rule-set are OR
- each nested branch is evaluated in isolation
- failed branches contribute no grouped match state
- a successful branch contributes its grouped match state back to the
  parent rule
- grouped state from different rule-sets must not be combined together
  to satisfy one outer rule

In other words, rule-sets now behave as "OR branches whose successful
group matches merge into the outer rule", which matches the documented
intent without reintroducing cross-branch cache leakage.
2026-03-24 15:03:43 +08:00
87 changed files with 8917 additions and 1035 deletions

View File

@@ -4,6 +4,7 @@
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--vendor SagerNet
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes

33
.github/detect_track.sh vendored Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
branches=$(git branch -r --contains HEAD)
if echo "$branches" | grep -q 'origin/stable'; then
track=stable
elif echo "$branches" | grep -q 'origin/testing'; then
track=testing
elif echo "$branches" | grep -q 'origin/oldstable'; then
track=oldstable
else
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
exit 1
fi
if [[ "$track" == "stable" ]]; then
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
track=beta
fi
fi
case "$track" in
stable) name=sing-box; docker_tag=latest ;;
beta) name=sing-box-beta; docker_tag=latest-beta ;;
testing) name=sing-box-testing; docker_tag=latest-testing ;;
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
esac
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
echo "TRACK=${track}" >> "$GITHUB_ENV"
echo "NAME=${name}" >> "$GITHUB_ENV"
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"

View File

@@ -19,7 +19,6 @@ env:
jobs:
build_binary:
name: Build binary
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
strategy:
fail-fast: true
@@ -260,13 +259,13 @@ jobs:
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
if [[ $ref == *"-"* ]]; then
latest=latest-beta
else
latest=latest
fi
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Detect track
run: bash .github/detect_track.sh
- name: Download digests
uses: actions/download-artifact@v5
with:
@@ -286,11 +285,11 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
if: github.event_name != 'push'
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -11,11 +11,6 @@ on:
description: "Version name"
required: true
type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release:
types:
- published
@@ -23,7 +18,6 @@ on:
jobs:
calculate_version:
name: Calculate version
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
@@ -168,14 +162,8 @@ jobs:
- name: Set mtime
run: |-
TZ=UTC touch -t '197001010000' dist/sing-box
- name: Set name
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Detect track
run: bash .github/detect_track.sh
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"

View File

@@ -25,8 +25,8 @@ type DNSRouter interface {
type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
ClearCache()
}
@@ -72,11 +72,6 @@ type DNSTransport interface {
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}
type LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}
type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)

View File

@@ -10,6 +10,8 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
"github.com/miekg/dns"
)
type Inbound interface {
@@ -79,14 +81,16 @@ type InboundContext struct {
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
DestinationAddresses []netip.Addr
DNSResponse *dns.Msg
DestinationAddressMatchFromResponse bool
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
// rule cache
@@ -95,10 +99,9 @@ type InboundContext struct {
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DidMatch bool
IgnoreDestinationIPCIDRMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DidMatch bool
}
func (c *InboundContext) ResetRuleCache() {
@@ -115,6 +118,39 @@ func (c *InboundContext) ResetRuleMatchCache() {
c.DidMatch = false
}
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
return DNSResponseAddresses(c.DNSResponse)
}
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawRecord := range response.Answer {
switch record := rawRecord.(type) {
case *dns.A:
addresses = append(addresses, M.AddrFromIP(record.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(record.AAAA))
case *dns.HTTPS:
for _, value := range record.SVCB.Value {
switch hint := value.(type) {
case *dns.SVCBIPv4Hint:
for _, ip := range hint.Hint {
addresses = append(addresses, M.AddrFromIP(ip).Unmap())
}
case *dns.SVCBIPv6Hint:
for _, ip := range hint.Hint {
addresses = append(addresses, M.AddrFromIP(ip))
}
}
}
}
}
return addresses
}
type inboundContextKey struct{}
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {

45
adapter/inbound_test.go Normal file
View File

@@ -0,0 +1,45 @@
package adapter
import (
"net"
"net/netip"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
t.Parallel()
ipv4Hint := net.ParseIP("1.1.1.1")
require.NotNil(t, ipv4Hint)
response := &dns.Msg{
MsgHdr: dns.MsgHdr{
Response: true,
Rcode: dns.RcodeSuccess,
},
Answer: []dns.RR{
&dns.HTTPS{
SVCB: dns.SVCB{
Hdr: dns.RR_Header{
Name: dns.Fqdn("example.com"),
Rrtype: dns.TypeHTTPS,
Class: dns.ClassINET,
Ttl: 60,
},
Priority: 1,
Target: ".",
Value: []dns.SVCBKeyValue{
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
},
},
},
},
}
addresses := DNSResponseAddresses(response)
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
require.True(t, addresses[0].Is4())
}

View File

@@ -66,10 +66,14 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
// Rule-set metadata only exposes headless-rule capabilities that outer routers
// need before evaluating nested matches. Headless rules do not support
// ip_version, so there is intentionally no ContainsIPVersionRule flag here.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsDNSQueryTypeRule bool
}
type HTTPStartContext struct {
ctx context.Context

View File

@@ -2,6 +2,8 @@ package adapter
import (
C "github.com/sagernet/sing-box/constant"
"github.com/miekg/dns"
)
type HeadlessRule interface {
@@ -18,8 +20,9 @@ type Rule interface {
type DNSRule interface {
Rule
LegacyPreMatch(metadata *InboundContext) bool
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
}
type RuleAction interface {

2
box.go
View File

@@ -486,7 +486,7 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
if err != nil {
return err
}

View File

@@ -15,19 +15,18 @@ const (
)
const (
DNSTypeLegacy = "legacy"
DNSTypeLegacyRcode = "legacy_rcode"
DNSTypeUDP = "udp"
DNSTypeTCP = "tcp"
DNSTypeTLS = "tls"
DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3"
DNSTypeLocal = "local"
DNSTypeHosts = "hosts"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
DNSTypeLegacy = "legacy"
DNSTypeUDP = "udp"
DNSTypeTCP = "tcp"
DNSTypeTLS = "tls"
DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3"
DNSTypeLocal = "local"
DNSTypeHosts = "hosts"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
)
const (

View File

@@ -29,6 +29,7 @@ const (
const (
RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options"
RuleActionTypeEvaluate = "evaluate"
RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject"

View File

@@ -5,7 +5,6 @@ import (
"errors"
"net"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -14,7 +13,6 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
@@ -109,7 +107,7 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
if len(message.Question) == 0 {
if c.logger != nil {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
@@ -239,13 +237,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
} else if len(response.Answer) == 0 {
rejected = !responseChecker(nil)
} else {
rejected = !responseChecker(MessageToAddresses(response))
rejected = !responseChecker(response)
}
if rejected {
if !disableCache && c.rdrc != nil {
@@ -315,7 +310,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, nil
}
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
var strategy C.DomainStrategy
@@ -400,7 +395,7 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
}
}
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := dns.Question{
Name: name,
Qtype: qType,
@@ -515,25 +510,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
}
func MessageToAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) {
case *dns.A:
addresses = append(addresses, M.AddrFromIP(answer.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
case *dns.HTTPS:
for _, value := range answer.SVCB.Value {
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
}
}
}
}
return addresses
return adapter.DNSResponseAddresses(response)
}
func wrapError(err error) error {

111
dns/repro_test.go Normal file
View File

@@ -0,0 +1,111 @@
package dns
import (
"context"
"net/netip"
"testing"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
mDNS "github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) {
t.Parallel()
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
var qTypes []uint16
router := newTestRouter(t, nil, &fakeDNSTransportManager{
defaultTransport: defaultTransport,
transports: map[string]adapter.DNSTransport{
"default": defaultTransport,
},
}, &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
qTypes = append(qTypes, message.Question[0].Qtype)
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
}
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
},
})
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
Strategy: C.DomainStrategyIPv4Only,
})
require.NoError(t, err)
require.Equal(t, []uint16{mDNS.TypeA}, qTypes)
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
}
func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
t.Parallel()
transportManager := &fakeDNSTransportManager{
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
transports: map[string]adapter.DNSTransport{
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
},
}
client := &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
switch transport.Tag() {
case "upstream":
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
case "selected":
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
default:
return nil, E.New("unexpected transport")
}
},
}
rules := []option.DNSRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: badoption.Listable[string]{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
},
},
},
{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalDNSRule{
RawLogicalDNSRule: option.RawLogicalDNSRule{
Mode: C.LogicalTypeOr,
Rules: []option.DNSRule{{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
MatchResponse: true,
IPCIDR: badoption.Listable[string]{"1.1.1.0/24"},
},
},
}},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
},
},
},
}
router := newTestRouter(t, rules, transportManager, client)
response, err := router.Exchange(context.Background(), &mDNS.Msg{
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
}, adapter.DNSQueryOptions{})
require.NoError(t, err)
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
}

File diff suppressed because it is too large Load Diff

3147
dns/router_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,13 @@
package dns
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
)
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
type TransportAdapter struct {
transportType string
transportTag string
dependencies []string
strategy C.DomainStrategy
clientSubnet netip.Prefix
}
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
@@ -35,8 +27,6 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
clientSubnet: localOptions.LegacyClientSubnet,
}
}
@@ -45,15 +35,10 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
}
if remoteOptions.LegacyAddressResolver != "" {
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
}
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
clientSubnet: remoteOptions.LegacyClientSubnet,
}
}
@@ -68,11 +53,3 @@ func (a *TransportAdapter) Tag() string {
func (a *TransportAdapter) Dependencies() []string {
return a.dependencies
}
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
return a.strategy
}
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
return a.clientSubnet
}

View File

@@ -2,104 +2,25 @@ package dns
import (
"context"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
if options.LegacyDefaultDialer {
return dialer.NewDefaultOutbound(ctx), nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
})
}
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
if options.LegacyDefaultDialer {
transportDialer := dialer.NewDefaultOutbound(ctx)
if options.LegacyAddressResolver != "" {
transport := service.FromContext[adapter.DNSTransportManager](ctx)
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
if !loaded {
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
}
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
} else if options.ServerIsDomain() {
return nil, E.New("missing address resolver for server: ", options.Server)
}
return transportDialer, nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
}
type legacyTransportDialer struct {
dialer N.Dialer
dnsRouter adapter.DNSRouter
transport adapter.DNSTransport
strategy C.DomainStrategy
fallbackDelay time.Duration
}
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
return &legacyTransportDialer{
dialer,
dnsRouter,
transport,
strategy,
fallbackDelay,
}
}
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if destination.IsIP() {
return d.dialer.DialContext(ctx, network, destination)
}
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
Transport: d.transport,
Strategy: d.strategy,
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
})
if err != nil {
return nil, err
}
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
}
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIP() {
return d.dialer.ListenPacket(ctx, destination)
}
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
Transport: d.transport,
Strategy: d.strategy,
})
if err != nil {
return nil, err
}
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
return conn, err
}
func (d *legacyTransportDialer) Upstream() any {
return d.dialer
}

View File

@@ -2,13 +2,49 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.8
* Add BBR profile and hop interval randomization for Hysteria2 **1**
* Fixes and improvements
**1**:
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
#### 1.14.0-alpha.8
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.14.0-alpha.7
* Fixes and improvements
#### 1.13.4
* Fixes and improvements
#### 1.14.0-alpha.4
* Refactor ACME support to certificate provider system **1**
* Add Cloudflare Origin CA certificate provider **2**
* Add Tailscale certificate provider **3**
* Fixes and improvements
#### 1.13.4-beta.1
**1**:
* Fixes and improvements
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
**2**:
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
**3**:
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
#### 1.13.3

View File

@@ -1,10 +1,10 @@
---
icon: material/delete-clock
icon: material/note-remove
---
!!! failure "Deprecated in sing-box 1.12.0"
!!! failure "Removed in sing-box 1.14.0"
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
### Structure

View File

@@ -1,10 +1,10 @@
---
icon: material/delete-clock
icon: material/note-remove
---
!!! failure "已在 sing-box 1.12.0 废弃"
!!! failure "已在 sing-box 1.14.0 移除"
旧的 fake-ip 配置已废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
### 结构

View File

@@ -39,7 +39,7 @@ icon: material/alert-decagram
|----------|---------------------------------|
| `server` | List of [DNS Server](./server/) |
| `rules` | List of [DNS Rule](./rule/) |
| `fakeip` | [FakeIP](./fakeip/) |
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
#### final
@@ -88,4 +88,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.
Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`.

View File

@@ -88,6 +88,6 @@ LRU 缓存容量。
可以被 `servers.[].client_subnet``rules.[].client_subnet` 覆盖。
#### fakeip
#### fakeip :material-note-remove:
[FakeIP](./fakeip/) 设置。

View File

@@ -4,8 +4,15 @@ icon: material/alert-decagram
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [match_response](#match_response)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [source_hostname](#source_hostname)
:material-delete-clock: [ip_accept_any](#ip_accept_any)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "Changes in sing-box 1.13.0"
@@ -94,12 +101,6 @@ icon: material/alert-decagram
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"ip_accept_any": false,
"source_port": [
12345
],
@@ -171,7 +172,16 @@ icon: material/alert-decagram
"geosite-cn"
],
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"match_response": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"response_rcode": "",
"response_answer": [],
"response_ns": [],
"response_extra": [],
"invert": false,
"outbound": [
"direct"
@@ -180,7 +190,9 @@ icon: material/alert-decagram
"server": "local",
// Deprecated
"ip_accept_any": false,
"rule_set_ip_cidr_accept_empty": false,
"rule_set_ipcidr_match_source": false,
"geosite": [
"cn"
@@ -220,7 +232,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
#### inbound
@@ -477,6 +489,17 @@ Make `ip_cidr` rule items in rule-sets match the source IP.
Make `ip_cidr` rule items in rule-sets match the source IP.
#### match_response
!!! question "Since sing-box 1.14.0"
Enable response-based matching. When enabled, this rule matches against DNS response data
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
instead of only matching the original query.
Required for `response_rcode`, `response_answer`, `response_ns`, `response_extra` fields.
Also required for `ip_cidr` and `ip_is_private` when `legacyDNSMode` is disabled.
#### invert
Invert match result.
@@ -521,6 +544,15 @@ See [DNS Rule Actions](../rule_action/) for details.
Moved to [DNS Rule Action](../rule_action#route).
### Legacy DNS Mode
`legacyDNSMode` is an internal compatibility mode that is automatically detected from your DNS rule
configuration. It is disabled when any rule uses features introduced in sing-box 1.14.0 such as
`evaluate`, `match_response`, response fields (`response_rcode`, `response_answer`, etc.),
`query_type`, or `ip_version`. When disabled, `ip_cidr` and `ip_is_private` require `match_response`
to be set, and deprecated fields like `strategy`, `ip_accept_any`, and `rule_set_ip_cidr_accept_empty`
are no longer accepted.
### Address Filter Fields
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
@@ -547,24 +579,69 @@ Match GeoIP with query response.
Match IP CIDR with query response.
When `legacyDNSMode` is disabled, `match_response` must be set to `true`.
#### ip_is_private
!!! question "Since sing-box 1.9.0"
Match private IP with query response.
When `legacyDNSMode` is disabled, `match_response` must be set to `true`.
#### rule_set_ip_cidr_accept_empty
!!! question "Since sing-box 1.10.0"
!!! failure "Deprecated in sing-box 1.14.0"
`rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0.
Only supported in `legacyDNSMode`.
Make `ip_cidr` rules in rule-sets accept empty query response.
#### ip_accept_any
!!! question "Since sing-box 1.12.0"
!!! failure "Deprecated in sing-box 1.14.0"
`ip_accept_any` is deprecated and will be removed in sing-box 1.16.0.
Only supported in `legacyDNSMode`. Use `match_response` with response items instead.
Match any IP with query response.
### Response Fields
!!! question "Since sing-box 1.14.0"
Match fields for DNS response data. Require `match_response` to be set to `true`
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
#### response_rcode
Match DNS response code.
Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode).
#### response_answer
Match DNS answer records.
Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer).
#### response_ns
Match DNS name server records.
Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns).
#### response_extra
Match DNS extra records.
Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra).
### Logical Fields
#### type
@@ -577,4 +654,4 @@ Match any IP with query response.
#### rules
Included rules.
Included rules.

View File

@@ -4,8 +4,15 @@ icon: material/alert-decagram
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [match_response](#match_response)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [source_hostname](#source_hostname)
:material-delete-clock: [ip_accept_any](#ip_accept_any)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "sing-box 1.13.0 中的更改"
@@ -94,12 +101,6 @@ icon: material/alert-decagram
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"ip_accept_any": false,
"source_port": [
12345
],
@@ -171,7 +172,16 @@ icon: material/alert-decagram
"geosite-cn"
],
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"match_response": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"response_rcode": "",
"response_answer": [],
"response_ns": [],
"response_extra": [],
"invert": false,
"outbound": [
"direct"
@@ -180,6 +190,9 @@ icon: material/alert-decagram
"server": "local",
// 已弃用
"ip_accept_any": false,
"rule_set_ip_cidr_accept_empty": false,
"rule_set_ipcidr_match_source": false,
"geosite": [
"cn"
@@ -219,7 +232,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
另外,引用规则集可视为被合并,而不是作为一个单独的规则子项
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
#### inbound
@@ -476,6 +489,15 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### match_response
!!! question "自 sing-box 1.14.0 起"
启用响应匹配。启用后,此规则将匹配 DNS 响应数据(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
`response_rcode``response_answer``response_ns``response_extra` 字段需要此选项。
`legacyDNSMode` 未启用时,`ip_cidr``ip_is_private` 也需要此选项。
#### invert
反选匹配结果。
@@ -520,6 +542,14 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
已移动到 [DNS 规则动作](../rule_action#route).
### Legacy DNS Mode
`legacyDNSMode` 是一种内部兼容模式,会根据 DNS 规则配置自动检测。
当任何规则使用了 sing-box 1.14.0 引入的特性(如 `evaluate``match_response`
响应字段(`response_rcode``response_answer` 等)、`query_type``ip_version`)时,
该模式将被自动禁用。禁用后,`ip_cidr``ip_is_private` 需要设置 `match_response`
且已废弃的字段(如 `strategy``ip_accept_any``rule_set_ip_cidr_accept_empty`)将不再被接受。
### 地址筛选字段
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
@@ -547,24 +577,69 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
与查询响应匹配 IP CIDR。
`legacyDNSMode` 未启用时,`match_response` 必须设为 `true`
#### ip_is_private
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配非公开 IP。
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
匹配任意 IP。
`legacyDNSMode` 未启用时,`match_response` 必须设为 `true`
#### rule_set_ip_cidr_accept_empty
!!! question "自 sing-box 1.10.0 起"
!!! failure "已在 sing-box 1.14.0 废弃"
`rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除。
仅在 `legacyDNSMode` 中可用。
使规则集中的 `ip_cidr` 规则接受空查询响应。
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
!!! failure "已在 sing-box 1.14.0 废弃"
`ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除。
仅在 `legacyDNSMode` 中可用。请使用 `match_response` 和响应项替代。
匹配任意 IP。
### 响应字段
!!! question "自 sing-box 1.14.0 起"
DNS 响应数据的匹配字段。需要将 `match_response` 设为 `true`
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
#### response_rcode
匹配 DNS 响应码。
接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。
#### response_answer
匹配 DNS 应答记录。
记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。
#### response_ns
匹配 DNS 名称服务器记录。
记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。
#### response_extra
匹配 DNS 额外记录。
记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。
### 逻辑字段
#### type
@@ -581,4 +656,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
==必填==
包括的规则。
包括的规则。

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [evaluate](#evaluate)
:material-delete-clock: [strategy](#strategy)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [strategy](#strategy)
@@ -34,7 +39,11 @@ Tag of target server.
!!! question "Since sing-box 1.12.0"
Set domain strategy for this query.
!!! failure "Deprecated in sing-box 1.14.0"
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
Set domain strategy for this query. Only supported when `legacyDNSMode` is active.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
@@ -52,7 +61,49 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet`.
Will override `dns.client_subnet`.
### evaluate
!!! question "Since sing-box 1.14.0"
```json
{
"action": "evaluate",
"server": "",
"disable_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
```
`evaluate` sends a DNS query to the specified server and saves the response for subsequent rules
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
Unlike `route`, it does **not** terminate rule evaluation.
Only allowed on top-level DNS rules (not inside logical sub-rules).
#### server
==Required==
Tag of target server.
#### disable_cache
Disable cache and save cache in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
#### client_subnet
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will override `dns.client_subnet`.
### route-options

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [evaluate](#evaluate)
:material-delete-clock: [strategy](#strategy)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [strategy](#strategy)
@@ -34,7 +39,11 @@ icon: material/new-box
!!! question "自 sing-box 1.12.0 起"
为此查询设置域名策略。
!!! failure "已在 sing-box 1.14.0 废弃"
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
为此查询设置域名策略。仅在 `legacyDNSMode` 启用时可用。
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
@@ -54,6 +63,46 @@ icon: material/new-box
将覆盖 `dns.client_subnet`.
### evaluate
!!! question "自 sing-box 1.14.0 起"
```json
{
"action": "evaluate",
"server": "",
"disable_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
```
`evaluate` 向指定服务器发送 DNS 查询并保存响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
#### server
==必填==
目标 DNS 服务器的标签。
#### disable_cache
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
#### client_subnet
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet`.
### route-options
```json
@@ -84,7 +133,7 @@ icon: material/new-box
- `default`: 返回 REFUSED。
- `drop`: 丢弃请求。
默认使用 `defualt`
默认使用 `default`
#### no_drop

View File

@@ -29,7 +29,7 @@ The type of the DNS server.
| Type | Format |
|-----------------|---------------------------|
| empty (default) | [Legacy](./legacy/) |
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |

View File

@@ -29,7 +29,7 @@ DNS 服务器的类型。
| 类型 | 格式 |
|-----------------|---------------------------|
| empty (default) | [Legacy](./legacy/) |
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |

View File

@@ -1,10 +1,10 @@
---
icon: material/delete-clock
icon: material/note-remove
---
!!! failure "Deprecated in sing-box 1.12.0"
!!! failure "Removed in sing-box 1.14.0"
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
!!! quote "Changes in sing-box 1.9.0"
@@ -108,6 +108,6 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Can be overrides by `rules.[].client_subnet`.
Can be overridden by `rules.[].client_subnet`.
Will overrides `dns.client_subnet`.
Will override `dns.client_subnet`.

View File

@@ -1,10 +1,10 @@
---
icon: material/delete-clock
icon: material/note-remove
---
!!! failure "Deprecated in sing-box 1.12.0"
!!! failure "已在 sing-box 1.14.0 移除"
旧的 DNS 服务器配置已废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
!!! quote "sing-box 1.9.0 中的更改"

View File

@@ -2,6 +2,10 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "Changes in sing-box 1.11.0"
:material-alert: [masquerade](#masquerade)
@@ -31,6 +35,7 @@ icon: material/alert-decagram
"ignore_client_bandwidth": false,
"tls": {},
"masquerade": "", // or {}
"bbr_profile": "",
"brutal_debug": false
}
```
@@ -141,6 +146,14 @@ Fixed response headers.
Fixed response content.
#### bbr_profile
!!! question "Since sing-box 1.14.0"
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
`standard` is used by default.
#### brutal_debug
Enable debug information logging for Hysteria Brutal CC.

View File

@@ -2,6 +2,10 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "sing-box 1.11.0 中的更改"
:material-alert: [masquerade](#masquerade)
@@ -31,6 +35,7 @@ icon: material/alert-decagram
"ignore_client_bandwidth": false,
"tls": {},
"masquerade": "", // 或 {}
"bbr_profile": "",
"brutal_debug": false
}
```
@@ -138,6 +143,14 @@ HTTP3 服务器认证失败时的行为 (对象配置)。
固定响应内容。
#### bbr_profile
!!! question "自 sing-box 1.14.0 起"
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`
默认使用 `standard`
#### brutal_debug
启用 Hysteria Brutal CC 的调试信息日志记录。

View File

@@ -1,3 +1,8 @@
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [hop_interval_max](#hop_interval_max)
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "Changes in sing-box 1.11.0"
:material-plus: [server_ports](#server_ports)
@@ -9,13 +14,14 @@
{
"type": "hysteria2",
"tag": "hy2-out",
"server": "127.0.0.1",
"server_port": 1080,
"server_ports": [
"2080:3000"
],
"hop_interval": "",
"hop_interval_max": "",
"up_mbps": 100,
"down_mbps": 100,
"obfs": {
@@ -25,8 +31,9 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
"bbr_profile": "",
"brutal_debug": false,
... // Dial Fields
}
```
@@ -75,6 +82,14 @@ Port hopping interval.
`30s` is used by default.
#### hop_interval_max
!!! question "Since sing-box 1.14.0"
Maximum port hopping interval, used for randomization.
If set, the actual hop interval will be randomly chosen between `hop_interval` and `hop_interval_max`.
#### up_mbps, down_mbps
Max bandwidth, in Mbps.
@@ -109,6 +124,14 @@ Both is enabled by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
#### bbr_profile
!!! question "Since sing-box 1.14.0"
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
`standard` is used by default.
#### brutal_debug
Enable debug information logging for Hysteria Brutal CC.

View File

@@ -1,3 +1,8 @@
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [hop_interval_max](#hop_interval_max)
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [server_ports](#server_ports)
@@ -16,6 +21,7 @@
"2080:3000"
],
"hop_interval": "",
"hop_interval_max": "",
"up_mbps": 100,
"down_mbps": 100,
"obfs": {
@@ -25,8 +31,9 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
"bbr_profile": "",
"brutal_debug": false,
... // 拨号字段
}
```
@@ -73,6 +80,14 @@
默认使用 `30s`
#### hop_interval_max
!!! question "自 sing-box 1.14.0 起"
最大端口跳跃间隔,用于随机化。
如果设置,实际跳跃间隔将在 `hop_interval``hop_interval_max` 之间随机选择。
#### up_mbps, down_mbps
最大带宽。
@@ -107,6 +122,14 @@ QUIC 流量混淆器密码.
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
#### bbr_profile
!!! question "自 sing-box 1.14.0 起"
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`
默认使用 `standard`
#### brutal_debug
启用 Hysteria Brutal CC 的调试信息日志记录。

View File

@@ -153,7 +153,7 @@ Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea
See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details.
Can be overrides by `outbound.domain_resolver`.
Can be overridden by `outbound.domain_resolver`.
#### default_network_strategy
@@ -163,7 +163,7 @@ See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set.
Can be overrides by `outbound.network_strategy`.
Can be overridden by `outbound.network_strategy`.
Conflicts with `default_interface`.

View File

@@ -210,7 +210,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
#### inbound

View File

@@ -208,7 +208,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) &&
`other fields`
另外,引用规则集可视为被合并,而不是作为一个单独的规则子项
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
#### inbound
@@ -532,4 +532,4 @@ icon: material/new-box
==必填==
包括的规则。
包括的规则。

View File

@@ -316,4 +316,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet`.
Will override `dns.client_subnet`.

View File

@@ -14,6 +14,36 @@ check [Migration](../migration/#migrate-inline-acme-to-certificate-provider).
Old fields will be removed in sing-box 1.16.0.
#### `strategy` in DNS rule actions
`strategy` in DNS rule actions is deprecated
and only supported in `legacyDNSMode`.
Old fields will be removed in sing-box 1.16.0.
#### `ip_accept_any` in DNS rules
`ip_accept_any` in DNS rules is deprecated
and only supported in `legacyDNSMode`.
Use `match_response` with response items instead.
Old fields will be removed in sing-box 1.16.0.
#### `rule_set_ip_cidr_accept_empty` in DNS rules
`rule_set_ip_cidr_accept_empty` in DNS rules is deprecated
and only supported in `legacyDNSMode`.
Old fields will be removed in sing-box 1.16.0.
#### Legacy address filter DNS rule items
Legacy address filter DNS rule items (`ip_cidr`, `ip_is_private` without `match_response`)
are deprecated and only supported in `legacyDNSMode`.
Use `match_response` with the `evaluate` action instead.
Old behavior will be removed in sing-box 1.16.0.
## 1.12.0
#### Legacy DNS server formats
@@ -21,7 +51,7 @@ Old fields will be removed in sing-box 1.16.0.
DNS servers are refactored,
check [Migration](../migration/#migrate-to-new-dns-servers).
Compatibility for old formats will be removed in sing-box 1.14.0.
Old formats were removed in sing-box 1.14.0.
#### `outbound` DNS rule item

View File

@@ -14,6 +14,36 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
旧字段将在 sing-box 1.16.0 中被移除。
#### DNS 规则动作中的 `strategy`
DNS 规则动作中的 `strategy` 已废弃,
且仅在 `legacyDNSMode` 中可用。
旧字段将在 sing-box 1.16.0 中被移除。
#### DNS 规则中的 `ip_accept_any`
DNS 规则中的 `ip_accept_any` 已废弃,
且仅在 `legacyDNSMode` 中可用。
请使用 `match_response` 和响应项替代。
旧字段将在 sing-box 1.16.0 中被移除。
#### DNS 规则中的 `rule_set_ip_cidr_accept_empty`
DNS 规则中的 `rule_set_ip_cidr_accept_empty` 已废弃,
且仅在 `legacyDNSMode` 中可用。
旧字段将在 sing-box 1.16.0 中被移除。
#### 旧的地址筛选 DNS 规则项
旧的地址筛选 DNS 规则项(不使用 `match_response``ip_cidr``ip_is_private`)已废弃,
且仅在 `legacyDNSMode` 中可用。
请使用 `match_response``evaluate` 动作替代。
旧行为将在 sing-box 1.16.0 中被移除。
## 1.12.0
#### 旧的 DNS 服务器格式
@@ -21,7 +51,7 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
DNS 服务器已重构,
参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式).
旧格式的兼容性将在 sing-box 1.14.0 中被移除。
旧格式在 sing-box 1.14.0 中被移除。
#### `outbound` DNS 规则项

View File

@@ -57,24 +57,6 @@ func (n Note) MessageWithLink() string {
}
}
var OptionLegacyDNSTransport = Note{
Name: "legacy-dns-transport",
Description: "legacy DNS servers",
DeprecatedVersion: "1.12.0",
ScheduledVersion: "1.14.0",
EnvName: "LEGACY_DNS_SERVERS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats",
}
var OptionLegacyDNSFakeIPOptions = Note{
Name: "legacy-dns-fakeip-options",
Description: "legacy DNS fakeip options",
DeprecatedVersion: "1.12.0",
ScheduledVersion: "1.14.0",
EnvName: "LEGACY_DNS_FAKEIP_OPTIONS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats",
}
var OptionOutboundDNSRuleItem = Note{
Name: "outbound-dns-rule-item",
Description: "outbound DNS rule item",
@@ -111,11 +93,49 @@ var OptionInlineACME = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider",
}
var OptionIPAcceptAny = Note{
Name: "dns-rule-ip-accept-any",
Description: "`ip_accept_any` in DNS rules",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "DNS_RULE_IP_ACCEPT_ANY",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
}
var OptionRuleSetIPCIDRAcceptEmpty = Note{
Name: "dns-rule-rule-set-ip-cidr-accept-empty",
Description: "`rule_set_ip_cidr_accept_empty` in DNS rules",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "DNS_RULE_RULE_SET_IP_CIDR_ACCEPT_EMPTY",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
}
var OptionLegacyDNSAddressFilter = Note{
Name: "legacy-dns-address-filter",
Description: "legacy address filter DNS rule items",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "LEGACY_DNS_ADDRESS_FILTER",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
}
var OptionLegacyDNSRuleStrategy = Note{
Name: "legacy-dns-rule-strategy",
Description: "`strategy` in DNS rule actions",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "LEGACY_DNS_RULE_STRATEGY",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule_action/",
}
var Options = []Note{
OptionLegacyDNSTransport,
OptionLegacyDNSFakeIPOptions,
OptionOutboundDNSRuleItem,
OptionMissingDomainResolver,
OptionLegacyDomainStrategyOptions,
OptionInlineACME,
OptionIPAcceptAny,
OptionRuleSetIPCIDRAcceptEmpty,
OptionLegacyDNSAddressFilter,
OptionLegacyDNSRuleStrategy,
}

View File

@@ -52,6 +52,11 @@ type HTTPRequest interface {
type HTTPResponse interface {
GetContent() (*StringBox, error)
WriteTo(path string) error
WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error
}
type HTTPResponseWriteToProgressHandler interface {
Update(progress int64, total int64)
}
var (
@@ -239,3 +244,31 @@ func (h *httpResponse) WriteTo(path string) error {
defer file.Close()
return common.Error(bufio.Copy(file, h.Body))
}
func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error {
defer h.Body.Close()
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return common.Error(bufio.Copy(&progressWriter{
writer: file,
handler: handler,
total: h.ContentLength,
}, h.Body))
}
type progressWriter struct {
writer io.Writer
handler HTTPResponseWriteToProgressHandler
total int64
written int64
}
func (w *progressWriter) Write(p []byte) (int, error) {
n, err := w.writer.Write(p)
w.written += int64(n)
w.handler.Update(w.written, w.total)
return n, err
}

8
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/anthropics/anthropic-sdk-go v1.26.0
github.com/anytls/sing-anytls v0.0.11
github.com/caddyserver/certmagic v0.25.2
github.com/caddyserver/zerossl v0.1.5
github.com/coder/websocket v1.8.14
github.com/cretz/bine v0.2.0
github.com/database64128/tfo-go/v2 v2.3.2
@@ -19,6 +20,7 @@ require (
github.com/libdns/acmedns v0.5.0
github.com/libdns/alidns v1.0.6
github.com/libdns/cloudflare v0.2.2
github.com/libdns/libdns v1.1.1
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mdlayher/netlink v1.9.0
github.com/metacubex/utls v1.8.4
@@ -35,9 +37,9 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde
github.com/sagernet/sing v0.8.3
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.0
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
@@ -69,7 +71,6 @@ require (
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/database64128/netx-go v0.1.1 // indirect
@@ -96,7 +97,6 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect

8
go.sum
View File

@@ -236,12 +236,12 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.3 h1:zGMy9M1deBPEew9pCYIUHKeE+/lDQ5A2CBqjBjjzqkA=
github.com/sagernet/sing v0.8.3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM=
github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=

View File

@@ -3,19 +3,14 @@ package option
import (
"context"
"net/netip"
"net/url"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
type RawDNSOptions struct {
@@ -26,80 +21,29 @@ type RawDNSOptions struct {
DNSClientOptions
}
type LegacyDNSOptions struct {
FakeIP *LegacyDNSFakeIPOptions `json:"fakeip,omitempty"`
}
type DNSOptions struct {
RawDNSOptions
LegacyDNSOptions
}
type contextKeyDontUpgrade struct{}
const (
legacyDNSFakeIPRemovedMessage = "legacy DNS fakeip options are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats"
legacyDNSServerRemovedMessage = "legacy DNS server formats are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats"
)
func ContextWithDontUpgrade(ctx context.Context) context.Context {
return context.WithValue(ctx, (*contextKeyDontUpgrade)(nil), true)
}
func dontUpgradeFromContext(ctx context.Context) bool {
return ctx.Value((*contextKeyDontUpgrade)(nil)) == true
type removedLegacyDNSOptions struct {
FakeIP json.RawMessage `json:"fakeip,omitempty"`
}
func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, &o.LegacyDNSOptions)
var legacyOptions removedLegacyDNSOptions
err := json.UnmarshalContext(ctx, content, &legacyOptions)
if err != nil {
return err
}
dontUpgrade := dontUpgradeFromContext(ctx)
legacyOptions := o.LegacyDNSOptions
if !dontUpgrade {
if o.FakeIP != nil && o.FakeIP.Enabled {
deprecated.Report(ctx, deprecated.OptionLegacyDNSFakeIPOptions)
ctx = context.WithValue(ctx, (*LegacyDNSFakeIPOptions)(nil), o.FakeIP)
}
o.LegacyDNSOptions = LegacyDNSOptions{}
if len(legacyOptions.FakeIP) != 0 {
return E.New(legacyDNSFakeIPRemovedMessage)
}
err = badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
if err != nil {
return err
}
if !dontUpgrade {
rcodeMap := make(map[string]int)
o.Servers = common.Filter(o.Servers, func(it DNSServerOptions) bool {
if it.Type == C.DNSTypeLegacyRcode {
rcodeMap[it.Tag] = it.Options.(int)
return false
}
return true
})
if len(rcodeMap) > 0 {
for i := 0; i < len(o.Rules); i++ {
rewriteRcode(rcodeMap, &o.Rules[i])
}
}
}
return nil
}
func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) {
switch rule.Type {
case C.RuleTypeDefault:
rewriteRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction)
case C.RuleTypeLogical:
rewriteRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction)
}
}
func rewriteRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) {
if ruleAction.Action != C.RuleActionTypeRoute {
return
}
rcode, loaded := rcodeMap[ruleAction.RouteOptions.Server]
if !loaded {
return
}
ruleAction.Action = C.RuleActionTypePredefined
ruleAction.PredefinedOptions.Rcode = common.Ptr(DNSRCode(rcode))
return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
}
type DNSClientOptions struct {
@@ -111,12 +55,6 @@ type DNSClientOptions struct {
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type LegacyDNSFakeIPOptions struct {
Enabled bool `json:"enabled,omitempty"`
Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"`
Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"`
}
type DNSTransportOptionsRegistry interface {
CreateOptions(transportType string) (any, bool)
}
@@ -129,10 +67,6 @@ type _DNSServerOptions struct {
type DNSServerOptions _DNSServerOptions
func (o *DNSServerOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
switch o.Type {
case C.DNSTypeLegacy:
o.Type = ""
}
return badjson.MarshallObjectsContext(ctx, (*_DNSServerOptions)(o), o.Options)
}
@@ -148,9 +82,7 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
var options any
switch o.Type {
case "", C.DNSTypeLegacy:
o.Type = C.DNSTypeLegacy
options = new(LegacyDNSServerOptions)
deprecated.Report(ctx, deprecated.OptionLegacyDNSTransport)
return E.New(legacyDNSServerRemovedMessage)
default:
var loaded bool
options, loaded = registry.CreateOptions(o.Type)
@@ -163,169 +95,6 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
return err
}
o.Options = options
if o.Type == C.DNSTypeLegacy && !dontUpgradeFromContext(ctx) {
err = o.Upgrade(ctx)
if err != nil {
return err
}
}
return nil
}
func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
if o.Type != C.DNSTypeLegacy {
return nil
}
options := o.Options.(*LegacyDNSServerOptions)
serverURL, _ := url.Parse(options.Address)
var serverType string
if serverURL != nil && serverURL.Scheme != "" {
serverType = serverURL.Scheme
} else {
switch options.Address {
case "local", "fakeip":
serverType = options.Address
default:
serverType = C.DNSTypeUDP
}
}
remoteOptions := RemoteDNSServerOptions{
RawLocalDNSServerOptions: RawLocalDNSServerOptions{
DialerOptions: DialerOptions{
Detour: options.Detour,
DomainResolver: &DomainResolveOptions{
Server: options.AddressResolver,
Strategy: options.AddressStrategy,
},
FallbackDelay: options.AddressFallbackDelay,
},
Legacy: true,
LegacyStrategy: options.Strategy,
LegacyDefaultDialer: options.Detour == "",
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
},
LegacyAddressResolver: options.AddressResolver,
LegacyAddressStrategy: options.AddressStrategy,
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
}
switch serverType {
case C.DNSTypeLocal:
o.Type = C.DNSTypeLocal
o.Options = &LocalDNSServerOptions{
RawLocalDNSServerOptions: remoteOptions.RawLocalDNSServerOptions,
}
case C.DNSTypeUDP:
o.Type = C.DNSTypeUDP
o.Options = &remoteOptions
var serverAddr M.Socksaddr
if serverURL == nil || serverURL.Scheme == "" {
serverAddr = M.ParseSocksaddr(options.Address)
} else {
serverAddr = M.ParseSocksaddr(serverURL.Host)
}
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
remoteOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 53 {
remoteOptions.ServerPort = serverAddr.Port
}
case C.DNSTypeTCP:
o.Type = C.DNSTypeTCP
o.Options = &remoteOptions
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
remoteOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 53 {
remoteOptions.ServerPort = serverAddr.Port
}
case C.DNSTypeTLS, C.DNSTypeQUIC:
o.Type = serverType
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
remoteOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 853 {
remoteOptions.ServerPort = serverAddr.Port
}
o.Options = &RemoteTLSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
}
case C.DNSTypeHTTPS, C.DNSTypeHTTP3:
o.Type = serverType
httpsOptions := RemoteHTTPSDNSServerOptions{
RemoteTLSDNSServerOptions: RemoteTLSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
},
}
o.Options = &httpsOptions
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
httpsOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 443 {
httpsOptions.ServerPort = serverAddr.Port
}
if serverURL.Path != "/dns-query" {
httpsOptions.Path = serverURL.Path
}
case "rcode":
var rcode int
if serverURL == nil {
return E.New("invalid server address")
}
switch serverURL.Host {
case "success":
rcode = dns.RcodeSuccess
case "format_error":
rcode = dns.RcodeFormatError
case "server_failure":
rcode = dns.RcodeServerFailure
case "name_error":
rcode = dns.RcodeNameError
case "not_implemented":
rcode = dns.RcodeNotImplemented
case "refused":
rcode = dns.RcodeRefused
default:
return E.New("unknown rcode: ", serverURL.Host)
}
o.Type = C.DNSTypeLegacyRcode
o.Options = rcode
case C.DNSTypeDHCP:
o.Type = C.DNSTypeDHCP
dhcpOptions := DHCPDNSServerOptions{}
if serverURL == nil {
return E.New("invalid server address")
}
if serverURL.Host != "" && serverURL.Host != "auto" {
dhcpOptions.Interface = serverURL.Host
}
o.Options = &dhcpOptions
case C.DNSTypeFakeIP:
o.Type = C.DNSTypeFakeIP
fakeipOptions := FakeIPDNSServerOptions{}
if legacyOptions, loaded := ctx.Value((*LegacyDNSFakeIPOptions)(nil)).(*LegacyDNSFakeIPOptions); loaded {
fakeipOptions.Inet4Range = legacyOptions.Inet4Range
fakeipOptions.Inet6Range = legacyOptions.Inet6Range
}
o.Options = &fakeipOptions
default:
return E.New("unsupported DNS server scheme: ", serverType)
}
return nil
}
@@ -350,16 +119,6 @@ func (o *DNSServerAddressOptions) ReplaceServerOptions(options ServerOptions) {
*o = DNSServerAddressOptions(options)
}
type LegacyDNSServerOptions struct {
Address string `json:"address"`
AddressResolver string `json:"address_resolver,omitempty"`
AddressStrategy DomainStrategy `json:"address_strategy,omitempty"`
AddressFallbackDelay badoption.Duration `json:"address_fallback_delay,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
Detour string `json:"detour,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type HostsDNSServerOptions struct {
Path badoption.Listable[string] `json:"path,omitempty"`
Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"`
@@ -367,10 +126,6 @@ type HostsDNSServerOptions struct {
type RawLocalDNSServerOptions struct {
DialerOptions
Legacy bool `json:"-"`
LegacyStrategy DomainStrategy `json:"-"`
LegacyDefaultDialer bool `json:"-"`
LegacyClientSubnet netip.Prefix `json:"-"`
}
type LocalDNSServerOptions struct {
@@ -381,9 +136,6 @@ type LocalDNSServerOptions struct {
type RemoteDNSServerOptions struct {
RawLocalDNSServerOptions
DNSServerAddressOptions
LegacyAddressResolver string `json:"-"`
LegacyAddressStrategy DomainStrategy `json:"-"`
LegacyAddressFallbackDelay badoption.Duration `json:"-"`
}
type RemoteTLSDNSServerOptions struct {

View File

@@ -2,6 +2,7 @@ package option
import (
"encoding/base64"
"strings"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
@@ -11,6 +12,8 @@ import (
"github.com/miekg/dns"
)
const defaultDNSRecordTTL uint32 = 3600
type DNSRCode int
func (r DNSRCode) MarshalJSON() ([]byte, error) {
@@ -76,10 +79,13 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
if err == nil {
return o.unmarshalBase64(binary)
}
record, err := dns.NewRR(stringValue)
record, err := parseDNSRecord(stringValue)
if err != nil {
return err
}
if record == nil {
return E.New("empty DNS record")
}
if a, isA := record.(*dns.A); isA {
a.A = M.AddrFromIP(a.A).Unmap().AsSlice()
}
@@ -87,6 +93,16 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
return nil
}
func parseDNSRecord(stringValue string) (dns.RR, error) {
if len(stringValue) > 0 && stringValue[len(stringValue)-1] != '\n' {
stringValue += "\n"
}
parser := dns.NewZoneParser(strings.NewReader(stringValue), "", "")
parser.SetDefaultTTL(defaultDNSRecordTTL)
record, _ := parser.Next()
return record, parser.Err()
}
func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error {
record, _, err := dns.UnpackRR(binary, 0)
if err != nil {
@@ -100,3 +116,10 @@ func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error {
func (o DNSRecordOptions) Build() dns.RR {
return o.RR
}
func (o DNSRecordOptions) Match(record dns.RR) bool {
if o.RR == nil || record == nil {
return false
}
return dns.IsDuplicate(o.RR, record)
}

52
option/dns_record_test.go Normal file
View File

@@ -0,0 +1,52 @@
package option
import (
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func mustRecordOptions(t *testing.T, record string) DNSRecordOptions {
t.Helper()
var value DNSRecordOptions
require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`)))
return value
}
func TestDNSRecordOptionsUnmarshalJSONAcceptsFullyQualifiedNames(t *testing.T) {
t.Parallel()
for _, record := range []string{
"example.com. A 1.1.1.1",
"www.example.com. IN CNAME example.com.",
} {
value := mustRecordOptions(t, record)
require.NotNil(t, value.RR)
}
}
func TestDNSRecordOptionsUnmarshalJSONRejectsRelativeNames(t *testing.T) {
t.Parallel()
for _, record := range []string{
"@ IN A 1.1.1.1",
"www IN CNAME example.com.",
"example.com. IN CNAME @",
"example.com. IN CNAME www",
} {
var value DNSRecordOptions
err := value.UnmarshalJSON([]byte(`"` + record + `"`))
require.Error(t, err)
}
}
func TestDNSRecordOptionsMatchIgnoresTTL(t *testing.T) {
t.Parallel()
expected := mustRecordOptions(t, "example.com. 600 IN A 1.1.1.1")
record, err := dns.NewRR("example.com. 60 IN A 1.1.1.1")
require.NoError(t, err)
require.True(t, expected.Match(record))
}

91
option/dns_test.go Normal file
View File

@@ -0,0 +1,91 @@
package option
import (
"context"
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/stretchr/testify/require"
)
type stubDNSTransportOptionsRegistry struct{}
func (stubDNSTransportOptionsRegistry) CreateOptions(transportType string) (any, bool) {
switch transportType {
case C.DNSTypeUDP:
return new(RemoteDNSServerOptions), true
case C.DNSTypeFakeIP:
return new(FakeIPDNSServerOptions), true
default:
return nil, false
}
}
func TestDNSOptionsRejectsLegacyFakeIPOptions(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
var options DNSOptions
err := json.UnmarshalContext(ctx, []byte(`{
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15"
}
}`), &options)
require.EqualError(t, err, legacyDNSFakeIPRemovedMessage)
}
func TestDNSServerOptionsRejectsLegacyFormats(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
testCases := []string{
`{"address":"1.1.1.1"}`,
`{"type":"legacy","address":"1.1.1.1"}`,
}
for _, content := range testCases {
var options DNSServerOptions
err := json.UnmarshalContext(ctx, []byte(content), &options)
require.EqualError(t, err, legacyDNSServerRemovedMessage)
}
}
func TestDNSOptionsAcceptsTypedServers(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
var options DNSOptions
err := json.UnmarshalContext(ctx, []byte(`{
"servers": [
{"type": "udp", "tag": "default", "server": "1.1.1.1"},
{"type": "fakeip", "tag": "fake", "inet4_range": "198.18.0.0/15"}
]
}`), &options)
require.NoError(t, err)
require.Len(t, options.Servers, 2)
require.Equal(t, C.DNSTypeUDP, options.Servers[0].Type)
require.Equal(t, "1.1.1.1", options.Servers[0].Options.(*RemoteDNSServerOptions).Server)
require.Equal(t, C.DNSTypeFakeIP, options.Servers[1].Type)
}
func TestDNSRuleActionEvaluateRoundTrip(t *testing.T) {
t.Parallel()
action := DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: DNSRouteActionOptions{
Server: "default",
},
}
content, err := json.Marshal(action)
require.NoError(t, err)
var decoded DNSRuleAction
err = json.UnmarshalContext(context.Background(), content, &decoded)
require.NoError(t, err)
require.Equal(t, action, decoded)
}

View File

@@ -19,6 +19,7 @@ type Hysteria2InboundOptions struct {
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
InboundTLSOptionsContainer
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
}
@@ -112,13 +113,15 @@ type Hysteria2MasqueradeString struct {
type Hysteria2OutboundOptions struct {
DialerOptions
ServerOptions
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
HopIntervalMax badoption.Duration `json:"hop_interval_max,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
BrutalDebug bool `json:"brutal_debug,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
}

View File

@@ -1,6 +1,7 @@
package option
import (
"context"
"reflect"
C "github.com/sagernet/sing-box/constant"
@@ -33,26 +34,24 @@ func (r Rule) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects((_Rule)(r), v)
}
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
func (r *Rule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.UnmarshalContext(ctx, bytes, (*_Rule)(r))
if err != nil {
return err
}
payload, err := rulePayloadWithoutType(ctx, bytes)
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions)
case C.RuleTypeLogical:
v = &r.LogicalOptions
return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions)
default:
return E.New("unknown rule type: " + r.Type)
}
err = badjson.UnmarshallExcluded(bytes, (*_Rule)(r), v)
if err != nil {
return err
}
return nil
}
func (r Rule) IsValid() bool {
@@ -160,6 +159,64 @@ func (r *LogicalRule) UnmarshalJSON(data []byte) error {
return badjson.UnmarshallExcluded(data, &r.RawLogicalRule, &r.RuleAction)
}
func rulePayloadWithoutType(ctx context.Context, data []byte) ([]byte, error) {
var content badjson.JSONObject
err := content.UnmarshalJSONContext(ctx, data)
if err != nil {
return nil, err
}
content.Remove("type")
return content.MarshalJSONContext(ctx)
}
func unmarshalDefaultRuleContext(ctx context.Context, data []byte, rule *DefaultRule) error {
rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data)
if err != nil {
return err
}
err = rejectNestedRouteRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(ctx, data, &rule.RawDefaultRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawDefaultRule, &rule.RuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) {
rule.RuleAction = RuleAction{}
}
return nil
}
func unmarshalLogicalRuleContext(ctx context.Context, data []byte, rule *LogicalRule) error {
rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data)
if err != nil {
return err
}
err = rejectNestedRouteRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &rule.RawLogicalRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawLogicalRule, &rule.RuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) {
rule.RuleAction = RuleAction{}
}
return nil
}
func (r *LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid)
}

View File

@@ -115,6 +115,8 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
case C.RuleActionTypeRoute:
r.Action = ""
v = r.RouteOptions
case C.RuleActionTypeEvaluate:
v = r.RouteOptions
case C.RuleActionTypeRouteOptions:
v = r.RouteOptionsOptions
case C.RuleActionTypeReject:
@@ -137,6 +139,8 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
case "", C.RuleActionTypeRoute:
r.Action = C.RuleActionTypeRoute
v = &r.RouteOptions
case C.RuleActionTypeEvaluate:
v = &r.RouteOptions
case C.RuleActionTypeRouteOptions:
v = &r.RouteOptionsOptions
case C.RuleActionTypeReject:

View File

@@ -35,7 +35,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) {
}
func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.Unmarshal(bytes, (*_DNSRule)(r))
err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r))
if err != nil {
return err
}
@@ -78,12 +78,6 @@ type RawDefaultDNSRule struct {
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
@@ -110,9 +104,23 @@ type RawDefaultDNSRule struct {
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
MatchResponse bool `json:"match_response,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
ResponseRcode *DNSRCode `json:"response_rcode,omitempty"`
ResponseAnswer badoption.Listable[DNSRecordOptions] `json:"response_answer,omitempty"`
ResponseNs badoption.Listable[DNSRecordOptions] `json:"response_ns,omitempty"`
ResponseExtra badoption.Listable[DNSRecordOptions] `json:"response_extra,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: removed in sing-box 1.12.0
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
// Deprecated: use match_response with response items
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
// Deprecated: removed in sing-box 1.11.0
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
}
@@ -127,11 +135,27 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) {
}
func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
if err != nil {
return err
}
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
err = rejectNestedDNSRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) {
r.DNSRuleAction = DNSRuleAction{}
}
return nil
}
func (r DefaultDNSRule) IsValid() bool {
@@ -156,11 +180,27 @@ func (r LogicalDNSRule) MarshalJSON() ([]byte, error) {
}
func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
err := json.Unmarshal(data, &r.RawLogicalDNSRule)
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
if err != nil {
return err
}
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
err = rejectNestedDNSRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &r.RawLogicalDNSRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) {
r.DNSRuleAction = DNSRuleAction{}
}
return nil
}
func (r *LogicalDNSRule) IsValid() bool {

133
option/rule_nested.go Normal file
View File

@@ -0,0 +1,133 @@
package option
import (
"context"
"reflect"
"strings"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
)
type nestedRuleDepthContextKey struct{}
const (
routeRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules"
dnsRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules"
)
var (
routeRuleActionKeys = jsonFieldNames(reflect.TypeFor[_RuleAction](), reflect.TypeFor[RouteActionOptions]())
dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions]())
)
func nestedRuleChildContext(ctx context.Context) context.Context {
return context.WithValue(ctx, nestedRuleDepthContextKey{}, nestedRuleDepth(ctx)+1)
}
func rejectNestedRouteRuleAction(ctx context.Context, content []byte) error {
return rejectNestedRuleAction(ctx, content, routeRuleActionKeys, routeRuleActionNestedUnsupportedMessage)
}
func rejectNestedDNSRuleAction(ctx context.Context, content []byte) error {
return rejectNestedRuleAction(ctx, content, dnsRuleActionKeys, dnsRuleActionNestedUnsupportedMessage)
}
func nestedRuleDepth(ctx context.Context) int {
depth, _ := ctx.Value(nestedRuleDepthContextKey{}).(int)
return depth
}
func rejectNestedRuleAction(ctx context.Context, content []byte, keys []string, message string) error {
if nestedRuleDepth(ctx) == 0 {
return nil
}
hasActionKey, err := hasAnyJSONKey(ctx, content, keys...)
if err != nil {
return err
}
if hasActionKey {
return E.New(message)
}
return nil
}
func hasAnyJSONKey(ctx context.Context, content []byte, keys ...string) (bool, error) {
var object badjson.JSONObject
err := object.UnmarshalJSONContext(ctx, content)
if err != nil {
return false, err
}
for _, key := range keys {
if object.ContainsKey(key) {
return true, nil
}
}
return false, nil
}
func inspectRouteRuleAction(ctx context.Context, content []byte) (string, RouteActionOptions, error) {
var rawAction _RuleAction
err := json.UnmarshalContext(ctx, content, &rawAction)
if err != nil {
return "", RouteActionOptions{}, err
}
var routeOptions RouteActionOptions
err = json.UnmarshalContext(ctx, content, &routeOptions)
if err != nil {
return "", RouteActionOptions{}, err
}
return rawAction.Action, routeOptions, nil
}
func inspectDNSRuleAction(ctx context.Context, content []byte) (string, DNSRouteActionOptions, error) {
var rawAction _DNSRuleAction
err := json.UnmarshalContext(ctx, content, &rawAction)
if err != nil {
return "", DNSRouteActionOptions{}, err
}
var routeOptions DNSRouteActionOptions
err = json.UnmarshalContext(ctx, content, &routeOptions)
if err != nil {
return "", DNSRouteActionOptions{}, err
}
return rawAction.Action, routeOptions, nil
}
func jsonFieldNames(types ...reflect.Type) []string {
fieldMap := make(map[string]struct{})
for _, fieldType := range types {
appendJSONFieldNames(fieldMap, fieldType)
}
fieldNames := make([]string, 0, len(fieldMap))
for fieldName := range fieldMap {
fieldNames = append(fieldNames, fieldName)
}
return fieldNames
}
func appendJSONFieldNames(fieldMap map[string]struct{}, fieldType reflect.Type) {
for fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if fieldType.Kind() != reflect.Struct {
return
}
for i := range fieldType.NumField() {
field := fieldType.Field(i)
tagValue := field.Tag.Get("json")
tagName, _, _ := strings.Cut(tagValue, ",")
if tagName == "-" {
continue
}
if field.Anonymous && tagName == "" {
appendJSONFieldNames(fieldMap, field.Type)
continue
}
if tagName == "" {
tagName = field.Name
}
fieldMap[tagName] = struct{}{}
}
}

271
option/rule_nested_test.go Normal file
View File

@@ -0,0 +1,271 @@
package option
import (
"context"
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/json"
"github.com/stretchr/testify/require"
)
func TestRuleRejectsNestedDefaultRuleAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "outbound": "direct"}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedLogicalRuleAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "route",
"outbound": "direct",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedDefaultRuleZeroValueOutbound(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "outbound": ""}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedDefaultRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "udp_connect": false}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedLogicalRuleZeroValueAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedLogicalRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"override_port": 0,
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleAllowsTopLevelLogicalAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"outbound": "direct",
"rules": [{"domain": "example.com"}]
}`), &rule)
require.NoError(t, err)
require.Equal(t, C.RuleActionTypeRoute, rule.LogicalOptions.Action)
require.Equal(t, "direct", rule.LogicalOptions.RouteOptions.Outbound)
}
func TestRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "foo": "bar"}
]
}`), &rule)
require.ErrorContains(t, err, "unknown field")
require.NotContains(t, err.Error(), routeRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedDefaultRuleAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "server": "default"}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedLogicalRuleAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "route",
"server": "default",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedDefaultRuleZeroValueServer(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "server": ""}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedDefaultRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "disable_cache": false}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedLogicalRuleZeroValueAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedLogicalRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"disable_cache": false,
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleAllowsTopLevelLogicalAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"server": "default",
"rules": [{"domain": "example.com"}]
}`), &rule)
require.NoError(t, err)
require.Equal(t, C.RuleActionTypeRoute, rule.LogicalOptions.Action)
require.Equal(t, "default", rule.LogicalOptions.RouteOptions.Server)
}
func TestDNSRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "foo": "bar"}
]
}`), &rule)
require.ErrorContains(t, err, "unknown field")
require.NotContains(t, err.Error(), dnsRuleActionNestedUnsupportedMessage)
}

View File

@@ -125,6 +125,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
UDPTimeout: udpTimeout,
Handler: inbound,
MasqueradeHandler: masqueradeHandler,
BBRProfile: options.BBRProfile,
})
if err != nil {
return nil, err

View File

@@ -73,12 +73,14 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
ServerAddress: options.ServerOptions.Build(),
ServerPorts: options.ServerPorts,
HopInterval: time.Duration(options.HopInterval),
HopIntervalMax: time.Duration(options.HopIntervalMax),
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
SalamanderPassword: salamanderPassword,
Password: options.Password,
TLSConfig: tlsConfig,
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
BBRProfile: options.BBRProfile,
})
if err != nil {
return nil, err

View File

@@ -29,7 +29,10 @@ import (
"golang.org/x/net/http2/h2c"
)
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
var (
ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
WrapError func(error) error
)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound)

View File

@@ -179,18 +179,18 @@ type naiveConn struct {
func (c *naiveConn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.Conn, p)
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveConn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.Conn, p)
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
err := c.writeBufferWithPadding(c.Conn, buffer)
return baderror.WrapH2(err)
return wrapError(err)
}
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
@@ -210,7 +210,7 @@ type naiveH2Conn struct {
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.reader, p)
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
@@ -218,7 +218,7 @@ func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
if err == nil {
c.flusher.Flush()
}
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
@@ -227,7 +227,15 @@ func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
if err == nil {
c.flusher.Flush()
}
return baderror.WrapH2(err)
return wrapError(err)
}
func wrapError(err error) error {
err = baderror.WrapH2(err)
if WrapError != nil {
err = WrapError(err)
}
return err
}
func (c *naiveH2Conn) Close() error {

View File

@@ -124,4 +124,5 @@ func init() {
return quicListener, nil
}
naive.WrapError = qtls.WrapError
}

View File

@@ -70,6 +70,10 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) error {
for i, options := range rules {
err := R.ValidateNoNestedRuleActions(options)
if err != nil {
return E.Cause(err, "parse rule[", i, "]")
}
rule, err := R.NewRule(r.ctx, r.logger, options, false)
if err != nil {
return E.Cause(err, "parse rule[", i, "]")

126
route/rule/match_state.go Normal file
View File

@@ -0,0 +1,126 @@
package rule
import "github.com/sagernet/sing-box/adapter"
type ruleMatchState uint8
const (
ruleMatchSourceAddress ruleMatchState = 1 << iota
ruleMatchSourcePort
ruleMatchDestinationAddress
ruleMatchDestinationPort
)
type ruleMatchStateSet uint16
func singleRuleMatchState(state ruleMatchState) ruleMatchStateSet {
return 1 << state
}
func emptyRuleMatchState() ruleMatchStateSet {
return singleRuleMatchState(0)
}
func (s ruleMatchStateSet) isEmpty() bool {
return s == 0
}
func (s ruleMatchStateSet) contains(state ruleMatchState) bool {
return s&(1<<state) != 0
}
func (s ruleMatchStateSet) add(state ruleMatchState) ruleMatchStateSet {
return s | singleRuleMatchState(state)
}
func (s ruleMatchStateSet) merge(other ruleMatchStateSet) ruleMatchStateSet {
return s | other
}
func (s ruleMatchStateSet) combine(other ruleMatchStateSet) ruleMatchStateSet {
if s.isEmpty() || other.isEmpty() {
return 0
}
var combined ruleMatchStateSet
for left := ruleMatchState(0); left < 16; left++ {
if !s.contains(left) {
continue
}
for right := ruleMatchState(0); right < 16; right++ {
if !other.contains(right) {
continue
}
combined = combined.add(left | right)
}
}
return combined
}
func (s ruleMatchStateSet) withBase(base ruleMatchState) ruleMatchStateSet {
if s.isEmpty() {
return 0
}
var withBase ruleMatchStateSet
for state := ruleMatchState(0); state < 16; state++ {
if !s.contains(state) {
continue
}
withBase = withBase.add(state | base)
}
return withBase
}
func (s ruleMatchStateSet) filter(allowed func(ruleMatchState) bool) ruleMatchStateSet {
var filtered ruleMatchStateSet
for state := ruleMatchState(0); state < 16; state++ {
if !s.contains(state) {
continue
}
if allowed(state) {
filtered = filtered.add(state)
}
}
return filtered
}
type ruleStateMatcher interface {
matchStates(metadata *adapter.InboundContext) ruleMatchStateSet
}
type ruleStateMatcherWithBase interface {
matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet
}
func matchHeadlessRuleStates(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
return matchHeadlessRuleStatesWithBase(rule, metadata, 0)
}
func matchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
if matcher, isStateMatcher := rule.(ruleStateMatcherWithBase); isStateMatcher {
return matcher.matchStatesWithBase(metadata, base)
}
if matcher, isStateMatcher := rule.(ruleStateMatcher); isStateMatcher {
return matcher.matchStates(metadata).withBase(base)
}
if rule.Match(metadata) {
return emptyRuleMatchState().withBase(base)
}
return 0
}
func matchRuleItemStates(item RuleItem, metadata *adapter.InboundContext) ruleMatchStateSet {
return matchRuleItemStatesWithBase(item, metadata, 0)
}
func matchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
if matcher, isStateMatcher := item.(ruleStateMatcherWithBase); isStateMatcher {
return matcher.matchStatesWithBase(metadata, base)
}
if matcher, isStateMatcher := item.(ruleStateMatcher); isStateMatcher {
return matcher.matchStates(metadata).withBase(base)
}
if item.Match(metadata) {
return emptyRuleMatchState().withBase(base)
}
return 0
}

View File

@@ -0,0 +1,76 @@
package rule
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
const (
routeRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules"
dnsRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules"
)
func ValidateNoNestedRuleActions(rule option.Rule) error {
return validateNoNestedRuleActions(rule, false)
}
func ValidateNoNestedDNSRuleActions(rule option.DNSRule) error {
return validateNoNestedDNSRuleActions(rule, false)
}
func validateNoNestedRuleActions(rule option.Rule, nested bool) error {
if nested && ruleHasConfiguredAction(rule) {
return E.New(routeRuleActionNestedUnsupportedMessage)
}
if rule.Type != C.RuleTypeLogical {
return nil
}
for i, subRule := range rule.LogicalOptions.Rules {
err := validateNoNestedRuleActions(subRule, true)
if err != nil {
return E.Cause(err, "sub rule[", i, "]")
}
}
return nil
}
func validateNoNestedDNSRuleActions(rule option.DNSRule, nested bool) error {
if nested && dnsRuleHasConfiguredAction(rule) {
return E.New(dnsRuleActionNestedUnsupportedMessage)
}
if rule.Type != C.RuleTypeLogical {
return nil
}
for i, subRule := range rule.LogicalOptions.Rules {
err := validateNoNestedDNSRuleActions(subRule, true)
if err != nil {
return E.Cause(err, "sub rule[", i, "]")
}
}
return nil
}
func ruleHasConfiguredAction(rule option.Rule) bool {
switch rule.Type {
case "", C.RuleTypeDefault:
return !reflect.DeepEqual(rule.DefaultOptions.RuleAction, option.RuleAction{})
case C.RuleTypeLogical:
return !reflect.DeepEqual(rule.LogicalOptions.RuleAction, option.RuleAction{})
default:
return false
}
}
func dnsRuleHasConfiguredAction(rule option.DNSRule) bool {
switch rule.Type {
case "", C.RuleTypeDefault:
return !reflect.DeepEqual(rule.DefaultOptions.DNSRuleAction, option.DNSRuleAction{})
case C.RuleTypeLogical:
return !reflect.DeepEqual(rule.LogicalOptions.DNSRuleAction, option.DNSRuleAction{})
default:
return false
}
}

View File

@@ -0,0 +1,157 @@
package rule
import (
"context"
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
"github.com/stretchr/testify/require"
)
func TestNewRulePreservesImplicitTopLevelDefaultAction(t *testing.T) {
t.Parallel()
var options option.Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"domain": "example.com"
}`), &options)
require.NoError(t, err)
rule, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), options, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
func TestNewRuleAllowsNestedRuleWithoutAction(t *testing.T) {
t.Parallel()
var options option.Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com"}
]
}`), &options)
require.NoError(t, err)
rule, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), options, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
func TestNewRuleRejectsNestedRuleAction(t *testing.T) {
t.Parallel()
_, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), option.Rule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalRule{
RawLogicalRule: option.RawLogicalRule{
Mode: C.LogicalTypeAnd,
Rules: []option.Rule{{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.RouteActionOptions{
Outbound: "direct",
},
},
},
}},
},
},
}, false)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestNewDNSRulePreservesImplicitTopLevelDefaultAction(t *testing.T) {
t.Parallel()
var options option.DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"domain": "example.com"
}`), &options)
require.NoError(t, err)
rule, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), options, false, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
func TestNewDNSRuleAllowsNestedRuleWithoutAction(t *testing.T) {
t.Parallel()
var options option.DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com"}
]
}`), &options)
require.NoError(t, err)
rule, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), options, false, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
func TestNewDNSRuleRejectsNestedRuleAction(t *testing.T) {
t.Parallel()
_, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalDNSRule{
RawLogicalDNSRule: option.RawLogicalDNSRule{
Mode: C.LogicalTypeAnd,
Rules: []option.DNSRule{{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{
Server: "default",
},
},
},
}},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{
Server: "default",
},
},
},
}, true, false)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestNewDNSRuleRejectsReplyRejectMethod(t *testing.T) {
t.Parallel()
_, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: []string{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeReject,
RejectOptions: option.RejectActionOptions{
Method: C.RuleActionRejectMethodReply,
},
},
},
}, false, false)
require.ErrorContains(t, err, "reject method `reply` is not supported for DNS rules")
}

View File

@@ -52,88 +52,120 @@ func (r *abstractDefaultRule) Close() error {
}
func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
}
func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool {
return metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
}
func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
return !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
}
func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata)
}
func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.destinationAddressItems) > 0 || r.destinationIPCIDRMatchesDestination(metadata)
}
func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesWithBase(metadata, 0)
}
func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) ruleMatchStateSet {
if len(r.allItems) == 0 {
return true
return emptyRuleMatchState().withBase(inheritedBase)
}
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
evaluationBase := inheritedBase
if r.invert {
evaluationBase = 0
}
baseState := evaluationBase
if len(r.sourceAddressItems) > 0 {
metadata.DidMatch = true
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
metadata.SourceAddressMatch = true
break
}
if matchAnyItem(r.sourceAddressItems, metadata) {
baseState |= ruleMatchSourceAddress
}
}
if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) {
metadata.DidMatch = true
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
metadata.SourcePortMatch = true
break
}
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
baseState |= ruleMatchSourceAddress
}
} else if r.destinationIPCIDRMatchesSource(metadata) {
metadata.DidMatch = true
}
if len(r.sourcePortItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.sourcePortItems, metadata) {
baseState |= ruleMatchSourcePort
}
}
if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
if len(r.destinationAddressItems) > 0 {
metadata.DidMatch = true
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
metadata.DestinationAddressMatch = true
break
}
if matchAnyItem(r.destinationAddressItems, metadata) {
baseState |= ruleMatchDestinationAddress
}
}
if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch {
if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) {
metadata.DidMatch = true
for _, item := range r.destinationIPCIDRItems {
if item.Match(metadata) {
metadata.DestinationAddressMatch = true
break
}
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
baseState |= ruleMatchDestinationAddress
}
} else if r.destinationIPCIDRMatchesDestination(metadata) {
metadata.DidMatch = true
}
if len(r.destinationPortItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.destinationPortItems, metadata) {
baseState |= ruleMatchDestinationPort
}
}
if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
metadata.DidMatch = true
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
metadata.DestinationPortMatch = true
break
}
}
}
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
return r.invert
return r.invertedFailure(inheritedBase)
}
}
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
return r.invert
var stateSet ruleMatchStateSet
if r.ruleSetItem != nil {
metadata.DidMatch = true
stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState)
} else {
stateSet = singleRuleMatchState(baseState)
}
if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
return r.invert
}
if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch {
return r.invert
}
if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
return r.invert
}
if !metadata.DidMatch {
stateSet = stateSet.filter(func(state ruleMatchState) bool {
if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) {
return false
}
if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) {
return false
}
if r.requiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) {
return false
}
if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) {
return false
}
return true
})
if stateSet.isEmpty() {
return r.invertedFailure(inheritedBase)
}
if r.invert {
return 0
}
return stateSet
}
return !r.invert
func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet {
if r.invert {
return emptyRuleMatchState().withBase(base)
}
return 0
}
func (r *abstractDefaultRule) Action() adapter.RuleAction {
@@ -191,17 +223,50 @@ func (r *abstractLogicalRule) Close() error {
}
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.Match(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.Match(metadata)
}) != r.invert
return !r.matchStates(metadata).isEmpty()
}
func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesWithBase(metadata, 0)
}
func (r *abstractLogicalRule) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
evaluationBase := base
if r.invert {
evaluationBase = 0
}
var stateSet ruleMatchStateSet
if r.mode == C.LogicalTypeAnd {
stateSet = emptyRuleMatchState().withBase(evaluationBase)
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)
if nestedStateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState().withBase(base)
}
return 0
}
stateSet = stateSet.combine(nestedStateSet)
}
} else {
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
}
if stateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState().withBase(base)
}
return 0
}
}
if r.invert {
return 0
}
return stateSet
}
func (r *abstractLogicalRule) Action() adapter.RuleAction {
@@ -222,3 +287,13 @@ func (r *abstractLogicalRule) String() string {
return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
}
}
func matchAnyItem(items []RuleItem, metadata *adapter.InboundContext) bool {
return common.Any(items, func(it RuleItem) bool {
return it.Match(metadata)
})
}
func (s ruleMatchState) has(target ruleMatchState) bool {
return s&target != 0
}

View File

@@ -78,9 +78,9 @@ func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule {
}
return &DefaultRule{
abstractDefaultRule: abstractDefaultRule{
items: []RuleItem{ruleSetItem},
allItems: []RuleItem{ruleSetItem},
invert: invert,
ruleSetItem: ruleSetItem,
allItems: []RuleItem{ruleSetItem},
invert: invert,
},
}
}

View File

@@ -132,6 +132,16 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
},
}
case C.RuleActionTypeEvaluate:
return &RuleActionEvaluate{
Server: action.RouteOptions.Server,
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
DisableCache: action.RouteOptions.DisableCache,
RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
},
}
case C.RuleActionTypeRouteOptions:
return &RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
@@ -266,18 +276,35 @@ func (r *RuleActionDNSRoute) Type() string {
}
func (r *RuleActionDNSRoute) String() string {
return formatDNSRouteAction("route", r.Server, r.RuleActionDNSRouteOptions)
}
type RuleActionEvaluate struct {
Server string
RuleActionDNSRouteOptions
}
func (r *RuleActionEvaluate) Type() string {
return C.RuleActionTypeEvaluate
}
func (r *RuleActionEvaluate) String() string {
return formatDNSRouteAction("evaluate", r.Server, r.RuleActionDNSRouteOptions)
}
func formatDNSRouteAction(action string, server string, options RuleActionDNSRouteOptions) string {
var descriptions []string
descriptions = append(descriptions, r.Server)
if r.DisableCache {
descriptions = append(descriptions, server)
if options.DisableCache {
descriptions = append(descriptions, "disable-cache")
}
if r.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL))
if options.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL))
}
if r.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
if options.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet))
}
return F.ToString("route(", strings.Join(descriptions, ","), ")")
return F.ToString(action, "(", strings.Join(descriptions, ","), ")")
}
type RuleActionDNSRouteOptions struct {

View File

@@ -47,6 +47,10 @@ type DefaultRule struct {
abstractDefaultRule
}
func (r *DefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractDefaultRule.matchStates(metadata)
}
type RuleItem interface {
Match(metadata *adapter.InboundContext) bool
String() string
@@ -285,7 +289,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
matchSource = true
}
item := NewRuleSetItem(router, options.RuleSet, matchSource, false)
rule.items = append(rule.items, item)
rule.ruleSetItem = item
rule.allItems = append(rule.allItems, item)
}
return rule, nil
@@ -297,6 +301,10 @@ type LogicalRule struct {
abstractLogicalRule
}
func (r *LogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractLogicalRule.matchStates(metadata)
}
func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
action, err := NewRuleAction(ctx, logger, options.RuleAction)
if err != nil {
@@ -318,6 +326,10 @@ func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options optio
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subOptions := range options.Rules {
err = validateNoNestedRuleActions(subOptions, true)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
subRule, err := NewRule(ctx, logger, subOptions, false)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")

View File

@@ -5,54 +5,84 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) {
func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool, legacyDNSMode bool) (adapter.DNSRule, error) {
switch options.Type {
case "", C.RuleTypeDefault:
if !options.DefaultOptions.IsValid() {
return nil, E.New("missing conditions")
}
if !checkServer && options.DefaultOptions.Action == C.RuleActionTypeEvaluate {
return nil, E.New(options.DefaultOptions.Action, " is only allowed on top-level DNS rules")
}
err := validateDNSRuleAction(options.DefaultOptions.DNSRuleAction)
if err != nil {
return nil, err
}
switch options.DefaultOptions.Action {
case "", C.RuleActionTypeRoute:
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions)
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions, legacyDNSMode)
case C.RuleTypeLogical:
if !options.LogicalOptions.IsValid() {
return nil, E.New("missing conditions")
}
if !checkServer && options.LogicalOptions.Action == C.RuleActionTypeEvaluate {
return nil, E.New(options.LogicalOptions.Action, " is only allowed on top-level DNS rules")
}
err := validateDNSRuleAction(options.LogicalOptions.DNSRuleAction)
if err != nil {
return nil, err
}
switch options.LogicalOptions.Action {
case "", C.RuleActionTypeRoute:
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions)
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions, legacyDNSMode)
default:
return nil, E.New("unknown rule type: ", options.Type)
}
}
func validateDNSRuleAction(action option.DNSRuleAction) error {
if action.Action == C.RuleActionTypeReject && action.RejectOptions.Method == C.RuleActionRejectMethodReply {
return E.New("reject method `reply` is not supported for DNS rules")
}
return nil
}
var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
type DefaultDNSRule struct {
abstractDefaultRule
matchResponse bool
}
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractDefaultRule.matchStates(metadata)
}
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule, legacyDNSMode bool) (*DefaultDNSRule, error) {
rule := &DefaultDNSRule{
abstractDefaultRule: abstractDefaultRule{
invert: options.Invert,
action: NewDNSRuleAction(logger, options.DNSRuleAction),
},
matchResponse: options.MatchResponse,
}
if len(options.Inbound) > 0 {
item := NewInboundRule(options.Inbound)
@@ -112,7 +142,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Geosite) > 0 {
if len(options.Geosite) > 0 { //nolint:staticcheck
return nil, E.New("geosite database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0")
}
if len(options.SourceGeoIP) > 0 {
@@ -147,11 +177,36 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPAcceptAny {
if options.IPAcceptAny { //nolint:staticcheck
if legacyDNSMode {
deprecated.Report(ctx, deprecated.OptionIPAcceptAny)
} else {
return nil, E.New("ip_accept_any is removed when legacyDNSMode is disabled, use ip_cidr with match_response")
}
item := NewIPAcceptAnyItem()
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.ResponseRcode != nil {
item := NewDNSResponseRCodeItem(int(*options.ResponseRcode))
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseAnswer) > 0 {
item := NewDNSResponseRecordItem("response_answer", options.ResponseAnswer, dnsResponseAnswers)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseNs) > 0 {
item := NewDNSResponseRecordItem("response_ns", options.ResponseNs, dnsResponseNS)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseExtra) > 0 {
item := NewDNSResponseRecordItem("response_extra", options.ResponseExtra, dnsResponseExtra)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
@@ -280,8 +335,15 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
if options.RuleSetIPCIDRMatchSource {
matchSource = true
}
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty)
rule.items = append(rule.items, item)
if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
if legacyDNSMode {
deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty)
} else {
return nil, E.New("rule_set_ip_cidr_accept_empty is removed when legacyDNSMode is disabled")
}
}
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck
rule.ruleSetItem = item
rule.allItems = append(rule.allItems, item)
}
return rule, nil
@@ -295,12 +357,9 @@ func (r *DefaultDNSRule) WithAddressLimit() bool {
if len(r.destinationIPCIDRItems) > 0 {
return true
}
for _, rawRule := range r.items {
ruleSet, isRuleSet := rawRule.(*RuleSetItem)
if !isRuleSet {
continue
}
if ruleSet.ContainsDestinationIPCIDRRule() {
if r.ruleSetItem != nil {
ruleSet, isRuleSet := r.ruleSetItem.(*RuleSetItem)
if isRuleSet && ruleSet.ContainsDestinationIPCIDRRule() {
return true
}
}
@@ -308,15 +367,44 @@ func (r *DefaultDNSRule) WithAddressLimit() bool {
}
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() {
metadata.IgnoreDestinationIPCIDRMatch = false
}()
return r.abstractDefaultRule.Match(metadata)
return !r.matchStatesForMatch(metadata).isEmpty()
}
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
return r.abstractDefaultRule.Match(metadata)
func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
if r.matchResponse {
return !r.legacyMatchStatesForMatch(metadata).isEmpty()
}
return !r.abstractDefaultRule.legacyMatchStates(metadata).isEmpty()
}
func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesForMatchWithMissingResponse(metadata, true)
}
func (r *DefaultDNSRule) legacyMatchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesForMatchWithMissingResponse(metadata, false)
}
func (r *DefaultDNSRule) matchStatesForMatchWithMissingResponse(metadata *adapter.InboundContext, ordinaryFailure bool) ruleMatchStateSet {
if r.matchResponse {
if metadata.DNSResponse == nil {
if ordinaryFailure {
return r.abstractDefaultRule.invertedFailure(0)
}
return 0
}
matchMetadata := *metadata
matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractDefaultRule.matchStates(&matchMetadata)
}
return r.abstractDefaultRule.matchStates(metadata)
}
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty()
}
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
@@ -325,7 +413,57 @@ type LogicalDNSRule struct {
abstractLogicalRule
}
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractLogicalRule.matchStates(metadata)
}
func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
switch typedRule := rule.(type) {
case *DefaultDNSRule:
return typedRule.matchStatesForMatch(metadata)
case *LogicalDNSRule:
return typedRule.matchStatesForMatch(metadata)
default:
return matchHeadlessRuleStates(typedRule, metadata)
}
}
func (r *LogicalDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
var stateSet ruleMatchStateSet
if r.mode == C.LogicalTypeAnd {
stateSet = emptyRuleMatchState()
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
nestedStateSet := matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata)
if nestedStateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState()
}
return 0
}
stateSet = stateSet.combine(nestedStateSet)
}
} else {
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata))
}
if stateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState()
}
return 0
}
}
if r.invert {
return 0
}
return stateSet
}
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) {
r := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{
rules: make([]adapter.HeadlessRule, len(options.Rules)),
@@ -342,7 +480,11 @@ func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options op
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subRule := range options.Rules {
rule, err := NewDNSRule(ctx, logger, subRule, false)
err := validateNoNestedDNSRuleActions(subRule, true)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
rule, err := NewDNSRule(ctx, logger, subRule, false, legacyDNSMode)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
@@ -372,29 +514,16 @@ func (r *LogicalDNSRule) WithAddressLimit() bool {
}
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).Match(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).Match(metadata)
}) != r.invert
}
return !r.matchStatesForMatch(metadata).isEmpty()
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).MatchAddressLimit(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).MatchAddressLimit(metadata)
}) != r.invert
}
func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
return !r.abstractLogicalRule.legacyMatchStates(metadata).isEmpty()
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty()
}

View File

@@ -0,0 +1,754 @@
package rule
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"go4.org/netipx"
)
type legacyResponseLiteralKind uint8
const (
legacyLiteralRequireEmpty legacyResponseLiteralKind = iota
legacyLiteralRequireNonEmpty
legacyLiteralRequireSet
legacyLiteralForbidSet
)
type legacyResponseLiteral struct {
kind legacyResponseLiteralKind
ipSet *netipx.IPSet
}
type legacyResponseFormulaKind uint8
const (
legacyFormulaFalse legacyResponseFormulaKind = iota
legacyFormulaTrue
legacyFormulaLiteral
legacyFormulaAnd
legacyFormulaOr
)
type legacyResponseFormula struct {
kind legacyResponseFormulaKind
literal legacyResponseLiteral
children []legacyResponseFormula
}
type legacyResponseConstraint struct {
requireEmpty bool
requireNonEmpty bool
requiredSets []*netipx.IPSet
forbiddenSet *netipx.IPSet
}
const (
legacyRuleMatchDeferredDestinationAddress ruleMatchState = 1 << 4
legacyRuleMatchStateCount = 32
)
type legacyRuleMatchStateSet [legacyRuleMatchStateCount]legacyResponseFormula
var (
legacyAllIPSet = func() *netipx.IPSet {
var builder netipx.IPSetBuilder
builder.Complement()
return common.Must1(builder.IPSet())
}()
legacyNonPublicIPSet = func() *netipx.IPSet {
var builder netipx.IPSetBuilder
for _, prefix := range []string{
"0.0.0.0/32",
"10.0.0.0/8",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.168.0.0/16",
"224.0.0.0/4",
"::/128",
"::1/128",
"fc00::/7",
"fe80::/10",
"ff00::/8",
} {
builder.AddPrefix(netip.MustParsePrefix(prefix))
}
return common.Must1(builder.IPSet())
}()
)
func legacyFalseFormula() legacyResponseFormula {
return legacyResponseFormula{}
}
func legacyTrueFormula() legacyResponseFormula {
return legacyResponseFormula{kind: legacyFormulaTrue}
}
func legacyLiteralFormula(literal legacyResponseLiteral) legacyResponseFormula {
return legacyResponseFormula{
kind: legacyFormulaLiteral,
literal: literal,
}
}
func (f legacyResponseFormula) isFalse() bool {
return f.kind == legacyFormulaFalse
}
func (f legacyResponseFormula) isTrue() bool {
return f.kind == legacyFormulaTrue
}
func (f legacyResponseFormula) or(other legacyResponseFormula) legacyResponseFormula {
return legacyOrFormulas(f, other)
}
func (f legacyResponseFormula) and(other legacyResponseFormula) legacyResponseFormula {
return legacyAndFormulas(f, other)
}
func (f legacyResponseFormula) not() legacyResponseFormula {
switch f.kind {
case legacyFormulaFalse:
return legacyTrueFormula()
case legacyFormulaTrue:
return legacyFalseFormula()
case legacyFormulaLiteral:
return legacyLiteralFormula(legacyNegateResponseLiteral(f.literal))
case legacyFormulaAnd:
negated := make([]legacyResponseFormula, 0, len(f.children))
for _, child := range f.children {
negated = append(negated, child.not())
}
return legacyOrFormulas(negated...)
case legacyFormulaOr:
negated := make([]legacyResponseFormula, 0, len(f.children))
for _, child := range f.children {
negated = append(negated, child.not())
}
return legacyAndFormulas(negated...)
default:
panic("unknown legacy response formula kind")
}
}
func legacyNegateResponseLiteral(literal legacyResponseLiteral) legacyResponseLiteral {
switch literal.kind {
case legacyLiteralRequireEmpty:
return legacyResponseLiteral{kind: legacyLiteralRequireNonEmpty}
case legacyLiteralRequireNonEmpty:
return legacyResponseLiteral{kind: legacyLiteralRequireEmpty}
case legacyLiteralRequireSet:
return legacyResponseLiteral{kind: legacyLiteralForbidSet, ipSet: literal.ipSet}
case legacyLiteralForbidSet:
return legacyResponseLiteral{kind: legacyLiteralRequireSet, ipSet: literal.ipSet}
default:
panic("unknown legacy response literal kind")
}
}
func legacyOrFormulas(formulas ...legacyResponseFormula) legacyResponseFormula {
children := make([]legacyResponseFormula, 0, len(formulas))
for _, formula := range formulas {
if formula.isFalse() {
continue
}
if formula.isTrue() {
return legacyTrueFormula()
}
if formula.kind == legacyFormulaOr {
children = append(children, formula.children...)
continue
}
children = append(children, formula)
}
switch len(children) {
case 0:
return legacyFalseFormula()
case 1:
return children[0]
default:
return legacyResponseFormula{
kind: legacyFormulaOr,
children: children,
}
}
}
func legacyAndFormulas(formulas ...legacyResponseFormula) legacyResponseFormula {
children := make([]legacyResponseFormula, 0, len(formulas))
for _, formula := range formulas {
if formula.isFalse() {
return legacyFalseFormula()
}
if formula.isTrue() {
continue
}
if formula.kind == legacyFormulaAnd {
children = append(children, formula.children...)
continue
}
children = append(children, formula)
}
switch len(children) {
case 0:
return legacyTrueFormula()
case 1:
return children[0]
}
result := legacyResponseFormula{
kind: legacyFormulaAnd,
children: children,
}
if !result.satisfiable() {
return legacyFalseFormula()
}
return result
}
func (f legacyResponseFormula) satisfiable() bool {
return legacyResponseFormulasSatisfiable(legacyResponseConstraint{}, []legacyResponseFormula{f})
}
func legacyResponseFormulasSatisfiable(constraint legacyResponseConstraint, formulas []legacyResponseFormula) bool {
stack := append(make([]legacyResponseFormula, 0, len(formulas)), formulas...)
var disjunctions []legacyResponseFormula
for len(stack) > 0 {
formula := stack[len(stack)-1]
stack = stack[:len(stack)-1]
switch formula.kind {
case legacyFormulaFalse:
return false
case legacyFormulaTrue:
continue
case legacyFormulaLiteral:
var ok bool
constraint, ok = constraint.withLiteral(formula.literal)
if !ok {
return false
}
case legacyFormulaAnd:
stack = append(stack, formula.children...)
case legacyFormulaOr:
if len(formula.children) == 0 {
return false
}
disjunctions = append(disjunctions, formula)
default:
panic("unknown legacy response formula kind")
}
}
if len(disjunctions) == 0 {
return true
}
bestIndex := 0
for i := 1; i < len(disjunctions); i++ {
if len(disjunctions[i].children) < len(disjunctions[bestIndex].children) {
bestIndex = i
}
}
selected := disjunctions[bestIndex]
remaining := make([]legacyResponseFormula, 0, len(disjunctions)-1)
remaining = append(remaining, disjunctions[:bestIndex]...)
remaining = append(remaining, disjunctions[bestIndex+1:]...)
for _, child := range selected.children {
nextFormulas := make([]legacyResponseFormula, 0, len(remaining)+1)
nextFormulas = append(nextFormulas, remaining...)
nextFormulas = append(nextFormulas, child)
if legacyResponseFormulasSatisfiable(constraint, nextFormulas) {
return true
}
}
return false
}
func (c legacyResponseConstraint) withLiteral(literal legacyResponseLiteral) (legacyResponseConstraint, bool) {
switch literal.kind {
case legacyLiteralRequireEmpty:
c.requireEmpty = true
case legacyLiteralRequireNonEmpty:
c.requireNonEmpty = true
case legacyLiteralRequireSet:
requiredSets := make([]*netipx.IPSet, len(c.requiredSets)+1)
copy(requiredSets, c.requiredSets)
requiredSets[len(c.requiredSets)] = literal.ipSet
c.requiredSets = requiredSets
case legacyLiteralForbidSet:
c.forbiddenSet = legacyUnionIPSets(c.forbiddenSet, literal.ipSet)
default:
panic("unknown legacy response literal kind")
}
return c, c.satisfiable()
}
func (c legacyResponseConstraint) satisfiable() bool {
if c.requireEmpty && (c.requireNonEmpty || len(c.requiredSets) > 0) {
return false
}
if c.requireEmpty {
return true
}
for _, required := range c.requiredSets {
if !legacyIPSetHasAllowedIP(required, c.forbiddenSet) {
return false
}
}
if c.requireNonEmpty && len(c.requiredSets) == 0 {
return legacyIPSetHasAllowedIP(legacyAllIPSet, c.forbiddenSet)
}
return true
}
func legacyUnionIPSets(left *netipx.IPSet, right *netipx.IPSet) *netipx.IPSet {
if left == nil {
return right
}
if right == nil {
return left
}
var builder netipx.IPSetBuilder
builder.AddSet(left)
builder.AddSet(right)
return common.Must1(builder.IPSet())
}
func legacyIPSetHasAllowedIP(required *netipx.IPSet, forbidden *netipx.IPSet) bool {
if required == nil {
required = legacyAllIPSet
}
if forbidden == nil {
return len(required.Ranges()) > 0
}
builder := netipx.IPSetBuilder{}
builder.AddSet(required)
builder.RemoveSet(forbidden)
remaining := common.Must1(builder.IPSet())
return len(remaining.Ranges()) > 0
}
func legacySingleRuleMatchState(state ruleMatchState) legacyRuleMatchStateSet {
return legacySingleRuleMatchStateWithFormula(state, legacyTrueFormula())
}
func legacySingleRuleMatchStateWithFormula(state ruleMatchState, formula legacyResponseFormula) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
if !formula.isFalse() {
stateSet[state] = formula
}
return stateSet
}
func (s legacyRuleMatchStateSet) isEmpty() bool {
for _, formula := range s {
if !formula.isFalse() {
return false
}
}
return true
}
func (s legacyRuleMatchStateSet) merge(other legacyRuleMatchStateSet) legacyRuleMatchStateSet {
var merged legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
merged[state] = s[state].or(other[state])
}
return merged
}
func (s legacyRuleMatchStateSet) combine(other legacyRuleMatchStateSet) legacyRuleMatchStateSet {
if s.isEmpty() || other.isEmpty() {
return legacyRuleMatchStateSet{}
}
var combined legacyRuleMatchStateSet
for left := ruleMatchState(0); left < legacyRuleMatchStateCount; left++ {
if s[left].isFalse() {
continue
}
for right := ruleMatchState(0); right < legacyRuleMatchStateCount; right++ {
if other[right].isFalse() {
continue
}
combined[left|right] = combined[left|right].or(s[left].and(other[right]))
}
}
return combined
}
func (s legacyRuleMatchStateSet) withBase(base ruleMatchState) legacyRuleMatchStateSet {
if s.isEmpty() {
return legacyRuleMatchStateSet{}
}
var withBase legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
withBase[state|base] = withBase[state|base].or(s[state])
}
return withBase
}
func (s legacyRuleMatchStateSet) filter(allowed func(ruleMatchState) bool) legacyRuleMatchStateSet {
var filtered legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
if allowed(state) {
filtered[state] = s[state]
}
}
return filtered
}
func (s legacyRuleMatchStateSet) addBit(bit ruleMatchState) legacyRuleMatchStateSet {
var withBit legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
withBit[state|bit] = withBit[state|bit].or(s[state])
}
return withBit
}
func (s legacyRuleMatchStateSet) branchOnBit(bit ruleMatchState, condition legacyResponseFormula) legacyRuleMatchStateSet {
if condition.isFalse() {
return s
}
if condition.isTrue() {
return s.addBit(bit)
}
var branched legacyRuleMatchStateSet
conditionFalse := condition.not()
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
if state.has(bit) {
branched[state] = branched[state].or(s[state])
continue
}
branched[state] = branched[state].or(s[state].and(conditionFalse))
branched[state|bit] = branched[state|bit].or(s[state].and(condition))
}
return branched
}
func (s legacyRuleMatchStateSet) andFormula(formula legacyResponseFormula) legacyRuleMatchStateSet {
if formula.isFalse() || s.isEmpty() {
return legacyRuleMatchStateSet{}
}
if formula.isTrue() {
return s
}
var result legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
result[state] = s[state].and(formula)
}
return result
}
func (s legacyRuleMatchStateSet) anyFormula() legacyResponseFormula {
var formula legacyResponseFormula
for _, stateFormula := range s {
formula = formula.or(stateFormula)
}
return formula
}
type legacyRuleStateMatcher interface {
legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet
}
type legacyRuleStateMatcherWithBase interface {
legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet
}
func legacyMatchHeadlessRuleStates(rule adapter.HeadlessRule, metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return legacyMatchHeadlessRuleStatesWithBase(rule, metadata, 0)
}
func legacyMatchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
if matcher, loaded := rule.(legacyRuleStateMatcherWithBase); loaded {
return matcher.legacyMatchStatesWithBase(metadata, base)
}
if matcher, loaded := rule.(legacyRuleStateMatcher); loaded {
return matcher.legacyMatchStates(metadata).withBase(base)
}
if rule.Match(metadata) {
return legacySingleRuleMatchState(base)
}
return legacyRuleMatchStateSet{}
}
func legacyMatchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
if matcher, loaded := item.(legacyRuleStateMatcherWithBase); loaded {
return matcher.legacyMatchStatesWithBase(metadata, base)
}
if matcher, loaded := item.(legacyRuleStateMatcher); loaded {
return matcher.legacyMatchStates(metadata).withBase(base)
}
if item.Match(metadata) {
return legacySingleRuleMatchState(base)
}
return legacyRuleMatchStateSet{}
}
func (r *DefaultHeadlessRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.abstractDefaultRule.legacyMatchStates(metadata)
}
func (r *LogicalHeadlessRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.abstractLogicalRule.legacyMatchStates(metadata)
}
func (r *RuleSetItem) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.legacyMatchStatesWithBase(metadata, 0)
}
func (r *RuleSetItem) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
for _, ruleSet := range r.setList {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base))
}
return stateSet
}
func (s *LocalRuleSet) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return s.legacyMatchStatesWithBase(metadata, 0)
}
func (s *LocalRuleSet) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return stateSet
}
func (s *RemoteRuleSet) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return s.legacyMatchStatesWithBase(metadata, 0)
}
func (s *RemoteRuleSet) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return stateSet
}
func (r *abstractDefaultRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.legacyMatchStatesWithBase(metadata, 0)
}
func (r *abstractDefaultRule) legacyMatchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) legacyRuleMatchStateSet {
if len(r.allItems) == 0 {
return legacySingleRuleMatchState(inheritedBase)
}
evaluationBase := inheritedBase
if r.invert {
evaluationBase = 0
}
stateSet := legacySingleRuleMatchState(evaluationBase)
if len(r.sourceAddressItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.sourceAddressItems, metadata) {
stateSet = stateSet.addBit(ruleMatchSourceAddress)
}
}
if r.destinationIPCIDRMatchesSource(metadata) {
metadata.DidMatch = true
stateSet = stateSet.branchOnBit(ruleMatchSourceAddress, legacyDestinationIPFormula(r.destinationIPCIDRItems, metadata))
}
if len(r.sourcePortItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.sourcePortItems, metadata) {
stateSet = stateSet.addBit(ruleMatchSourcePort)
}
}
if len(r.destinationAddressItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.destinationAddressItems, metadata) {
stateSet = stateSet.addBit(ruleMatchDestinationAddress)
}
}
if r.legacyDestinationIPCIDRMatchesDestination(metadata) {
metadata.DidMatch = true
stateSet = stateSet.branchOnBit(legacyRuleMatchDeferredDestinationAddress, legacyDestinationIPFormula(r.destinationIPCIDRItems, metadata))
}
if len(r.destinationPortItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.destinationPortItems, metadata) {
stateSet = stateSet.addBit(ruleMatchDestinationPort)
}
}
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
if r.invert {
return legacySingleRuleMatchState(inheritedBase)
}
return legacyRuleMatchStateSet{}
}
}
if r.ruleSetItem != nil {
metadata.DidMatch = true
var merged legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if stateSet[state].isFalse() {
continue
}
nestedStateSet := legacyMatchRuleItemStatesWithBase(r.ruleSetItem, metadata, state)
merged = merged.merge(nestedStateSet.andFormula(stateSet[state]))
}
stateSet = merged
}
stateSet = stateSet.filter(func(state ruleMatchState) bool {
if r.legacyRequiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) {
return false
}
if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) {
return false
}
if r.legacyRequiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) {
return false
}
if r.legacyRequiresDeferredDestinationAddressMatch(metadata) && !state.has(legacyRuleMatchDeferredDestinationAddress) {
return false
}
if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) {
return false
}
return true
})
if r.invert {
return legacySingleRuleMatchStateWithFormula(inheritedBase, stateSet.anyFormula().not())
}
return stateSet
}
func (r *abstractDefaultRule) legacyRequiresSourceAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata)
}
func (r *abstractDefaultRule) legacyDestinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
return !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
}
func (r *abstractDefaultRule) legacyRequiresDestinationAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.destinationAddressItems) > 0
}
func (r *abstractDefaultRule) legacyRequiresDeferredDestinationAddressMatch(metadata *adapter.InboundContext) bool {
return r.legacyDestinationIPCIDRMatchesDestination(metadata)
}
func (r *abstractLogicalRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.legacyMatchStatesWithBase(metadata, 0)
}
func (r *abstractLogicalRule) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
evaluationBase := base
if r.invert {
evaluationBase = 0
}
var stateSet legacyRuleMatchStateSet
if r.mode == C.LogicalTypeAnd {
stateSet = legacySingleRuleMatchState(evaluationBase)
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.combine(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
if stateSet.isEmpty() && !r.invert {
return legacyRuleMatchStateSet{}
}
}
} else {
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
}
}
if r.invert {
return legacySingleRuleMatchStateWithFormula(base, stateSet.anyFormula().not())
}
return stateSet
}
func legacyDestinationIPFormula(items []RuleItem, metadata *adapter.InboundContext) legacyResponseFormula {
if legacyDestinationIPResolved(metadata) {
if matchAnyItem(items, metadata) {
return legacyTrueFormula()
}
return legacyFalseFormula()
}
var formula legacyResponseFormula
for _, rawItem := range items {
switch item := rawItem.(type) {
case *IPCIDRItem:
if item.isSource || metadata.IPCIDRMatchSource {
if item.Match(metadata) {
return legacyTrueFormula()
}
continue
}
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireSet,
ipSet: item.ipSet,
}))
if metadata.IPCIDRAcceptEmpty {
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireEmpty,
}))
}
case *IPIsPrivateItem:
if item.isSource {
if item.Match(metadata) {
return legacyTrueFormula()
}
continue
}
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireSet,
ipSet: legacyNonPublicIPSet,
}))
case *IPAcceptAnyItem:
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireNonEmpty,
}))
default:
if rawItem.Match(metadata) {
return legacyTrueFormula()
}
}
}
return formula
}
func legacyDestinationIPResolved(metadata *adapter.InboundContext) bool {
return metadata.IPCIDRMatchSource ||
metadata.DestinationAddressMatchFromResponse ||
metadata.DNSResponse != nil ||
metadata.Destination.IsIP() ||
len(metadata.DestinationAddresses) > 0
}

View File

@@ -34,6 +34,10 @@ type DefaultHeadlessRule struct {
abstractDefaultRule
}
func (r *DefaultHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractDefaultRule.matchStates(metadata)
}
func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx)
rule := &DefaultHeadlessRule{
@@ -41,6 +45,11 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
invert: options.Invert,
},
}
if len(options.QueryType) > 0 {
item := NewQueryTypeItem(options.QueryType)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
@@ -199,6 +208,10 @@ type LogicalHeadlessRule struct {
abstractLogicalRule
}
func (r *LogicalHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractLogicalRule.matchStates(metadata)
}
func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) {
r := &LogicalHeadlessRule{
abstractLogicalRule{

View File

@@ -76,11 +76,26 @@ func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
if r.isSource || metadata.IPCIDRMatchSource {
return r.ipSet.Contains(metadata.Source.Addr)
}
if metadata.DestinationAddressMatchFromResponse {
addresses := metadata.DNSResponseAddressesForMatch()
if len(addresses) == 0 {
// Legacy rule_set_ip_cidr_accept_empty only applies when the DNS response
// does not expose any address answers for matching.
return metadata.IPCIDRAcceptEmpty
}
for _, address := range addresses {
if r.ipSet.Contains(address) {
return true
}
}
return false
}
if metadata.Destination.IsIP() {
return r.ipSet.Contains(metadata.Destination.Addr)
}
if len(metadata.DestinationAddresses) > 0 {
for _, address := range metadata.DestinationAddresses {
addresses := metadata.DestinationAddresses
if len(addresses) > 0 {
for _, address := range addresses {
if r.ipSet.Contains(address) {
return true
}

View File

@@ -13,6 +13,9 @@ func NewIPAcceptAnyItem() *IPAcceptAnyItem {
}
func (r *IPAcceptAnyItem) Match(metadata *adapter.InboundContext) bool {
if metadata.DestinationAddressMatchFromResponse {
return len(metadata.DNSResponseAddressesForMatch()) > 0
}
return len(metadata.DestinationAddresses) > 0
}

View File

@@ -1,8 +1,6 @@
package rule
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
)
@@ -18,21 +16,24 @@ func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem {
}
func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool {
var destination netip.Addr
if r.isSource {
destination = metadata.Source.Addr
} else {
destination = metadata.Destination.Addr
return !N.IsPublicAddr(metadata.Source.Addr)
}
if destination.IsValid() {
return !N.IsPublicAddr(destination)
}
if !r.isSource {
for _, destinationAddress := range metadata.DestinationAddresses {
if metadata.DestinationAddressMatchFromResponse {
for _, destinationAddress := range metadata.DNSResponseAddressesForMatch() {
if !N.IsPublicAddr(destinationAddress) {
return true
}
}
return false
}
if metadata.Destination.Addr.IsValid() {
return !N.IsPublicAddr(metadata.Destination.Addr)
}
for _, destinationAddress := range metadata.DestinationAddresses {
if !N.IsPublicAddr(destinationAddress) {
return true
}
}
return false
}

View File

@@ -0,0 +1,25 @@
package rule
import (
"github.com/miekg/dns"
"github.com/sagernet/sing-box/adapter"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*DNSResponseRCodeItem)(nil)
type DNSResponseRCodeItem struct {
rcode int
}
func NewDNSResponseRCodeItem(rcode int) *DNSResponseRCodeItem {
return &DNSResponseRCodeItem{rcode: rcode}
}
func (r *DNSResponseRCodeItem) Match(metadata *adapter.InboundContext) bool {
return metadata.DNSResponse != nil && metadata.DNSResponse.Rcode == r.rcode
}
func (r *DNSResponseRCodeItem) String() string {
return F.ToString("response_rcode=", dns.RcodeToString[r.rcode])
}

View File

@@ -0,0 +1,63 @@
package rule
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/miekg/dns"
)
var _ RuleItem = (*DNSResponseRecordItem)(nil)
type DNSResponseRecordItem struct {
field string
records []option.DNSRecordOptions
selector func(*dns.Msg) []dns.RR
}
func NewDNSResponseRecordItem(field string, records []option.DNSRecordOptions, selector func(*dns.Msg) []dns.RR) *DNSResponseRecordItem {
return &DNSResponseRecordItem{
field: field,
records: records,
selector: selector,
}
}
func (r *DNSResponseRecordItem) Match(metadata *adapter.InboundContext) bool {
if metadata.DNSResponse == nil {
return false
}
records := r.selector(metadata.DNSResponse)
for _, expected := range r.records {
for _, record := range records {
if expected.Match(record) {
return true
}
}
}
return false
}
func (r *DNSResponseRecordItem) String() string {
descriptions := make([]string, 0, len(r.records))
for _, record := range r.records {
if record.RR != nil {
descriptions = append(descriptions, record.RR.String())
}
}
return r.field + "=[" + strings.Join(descriptions, " ") + "]"
}
func dnsResponseAnswers(message *dns.Msg) []dns.RR {
return message.Answer
}
func dnsResponseNS(message *dns.Msg) []dns.RR {
return message.Ns
}
func dnsResponseExtra(message *dns.Msg) []dns.RR {
return message.Extra
}

View File

@@ -29,9 +29,11 @@ func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource b
}
func (r *RuleSetItem) Start() error {
_ = r.Close()
for _, tag := range r.tagList {
ruleSet, loaded := r.router.RuleSet(tag)
if !loaded {
_ = r.Close()
return E.New("rule-set not found: ", tag)
}
ruleSet.IncRef()
@@ -40,17 +42,33 @@ func (r *RuleSetItem) Start() error {
return nil
}
func (r *RuleSetItem) Close() error {
for _, ruleSet := range r.setList {
ruleSet.DecRef()
}
clear(r.setList)
r.setList = nil
return nil
}
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
}
func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesWithBase(metadata, 0)
}
func (r *RuleSetItem) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
var stateSet ruleMatchStateSet
for _, ruleSet := range r.setList {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
if ruleSet.Match(&nestedMetadata) {
return true
}
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base))
}
return false
return stateSet
}
func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {

View File

@@ -0,0 +1,139 @@
package rule
import (
"context"
"net"
"sync/atomic"
"testing"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/stretchr/testify/require"
"go4.org/netipx"
)
type ruleSetItemTestRouter struct {
ruleSets map[string]adapter.RuleSet
}
func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil }
func (r *ruleSetItemTestRouter) Close() error { return nil }
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) {
return nil, nil
}
func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error {
return nil
}
func (r *ruleSetItemTestRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error {
return nil
}
func (r *ruleSetItemTestRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) {
}
func (r *ruleSetItemTestRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) {
}
func (r *ruleSetItemTestRouter) RuleSet(tag string) (adapter.RuleSet, bool) {
ruleSet, loaded := r.ruleSets[tag]
return ruleSet, loaded
}
func (r *ruleSetItemTestRouter) Rules() []adapter.Rule { return nil }
func (r *ruleSetItemTestRouter) NeedFindProcess() bool { return false }
func (r *ruleSetItemTestRouter) NeedFindNeighbor() bool { return false }
func (r *ruleSetItemTestRouter) NeighborResolver() adapter.NeighborResolver { return nil }
func (r *ruleSetItemTestRouter) AppendTracker(adapter.ConnectionTracker) {}
func (r *ruleSetItemTestRouter) ResetNetwork() {}
type countingRuleSet struct {
name string
refs atomic.Int32
}
func (s *countingRuleSet) Name() string { return s.name }
func (s *countingRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
func (s *countingRuleSet) PostStart() error { return nil }
func (s *countingRuleSet) Metadata() adapter.RuleSetMetadata { return adapter.RuleSetMetadata{} }
func (s *countingRuleSet) ExtractIPSet() []*netipx.IPSet { return nil }
func (s *countingRuleSet) IncRef() { s.refs.Add(1) }
func (s *countingRuleSet) DecRef() {
if s.refs.Add(-1) < 0 {
panic("rule-set: negative refs")
}
}
func (s *countingRuleSet) Cleanup() {}
func (s *countingRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
return nil
}
func (s *countingRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {}
func (s *countingRuleSet) Close() error { return nil }
func (s *countingRuleSet) Match(*adapter.InboundContext) bool { return true }
func (s *countingRuleSet) String() string { return s.name }
func (s *countingRuleSet) RefCount() int32 { return s.refs.Load() }
func TestRuleSetItemCloseReleasesRefs(t *testing.T) {
t.Parallel()
firstSet := &countingRuleSet{name: "first"}
secondSet := &countingRuleSet{name: "second"}
item := NewRuleSetItem(&ruleSetItemTestRouter{
ruleSets: map[string]adapter.RuleSet{
"first": firstSet,
"second": secondSet,
},
}, []string{"first", "second"}, false, false)
require.NoError(t, item.Start())
require.EqualValues(t, 1, firstSet.RefCount())
require.EqualValues(t, 1, secondSet.RefCount())
require.NoError(t, item.Close())
require.Zero(t, firstSet.RefCount())
require.Zero(t, secondSet.RefCount())
require.NoError(t, item.Close())
require.Zero(t, firstSet.RefCount())
require.Zero(t, secondSet.RefCount())
}
func TestRuleSetItemStartRollbackOnFailure(t *testing.T) {
t.Parallel()
firstSet := &countingRuleSet{name: "first"}
item := NewRuleSetItem(&ruleSetItemTestRouter{
ruleSets: map[string]adapter.RuleSet{
"first": firstSet,
},
}, []string{"first", "missing"}, false, false)
err := item.Start()
require.ErrorContains(t, err, "rule-set not found: missing")
require.Zero(t, firstSet.RefCount())
require.Empty(t, item.setList)
}
func TestRuleSetItemRestartKeepsBalancedRefs(t *testing.T) {
t.Parallel()
firstSet := &countingRuleSet{name: "first"}
item := NewRuleSetItem(&ruleSetItemTestRouter{
ruleSets: map[string]adapter.RuleSet{
"first": firstSet,
},
}, []string{"first"}, false, false)
require.NoError(t, item.Start())
require.EqualValues(t, 1, firstSet.RefCount())
require.NoError(t, item.Start())
require.EqualValues(t, 1, firstSet.RefCount())
require.NoError(t, item.Close())
require.Zero(t, firstSet.RefCount())
}

View File

@@ -69,3 +69,7 @@ func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.IPCIDR) > 0 || rule.IPSet != nil
}
func isDNSQueryTypeHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.QueryType) > 0
}

View File

@@ -141,6 +141,7 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
metadata.ContainsDNSQueryTypeRule = HasHeadlessRule(headlessRules, isDNSQueryTypeHeadlessRule)
s.access.Lock()
s.rules = rules
s.metadata = metadata
@@ -202,12 +203,19 @@ func (s *LocalRuleSet) Close() error {
}
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
return !s.matchStates(metadata).isEmpty()
}
func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return s.matchStatesWithBase(metadata, 0)
}
func (s *LocalRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
var stateSet ruleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
if rule.Match(&nestedMetadata) {
return true
}
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return false
return stateSet
}

View File

@@ -193,6 +193,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.metadata.ContainsDNSQueryTypeRule = HasHeadlessRule(plainRuleSet.Rules, isDNSQueryTypeHeadlessRule)
s.rules = rules
callbacks := s.callbacks.Array()
s.access.Unlock()
@@ -322,12 +323,19 @@ func (s *RemoteRuleSet) Close() error {
}
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
return !s.matchStates(metadata).isEmpty()
}
func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return s.matchStatesWithBase(metadata, 0)
}
func (s *RemoteRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
var stateSet ruleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
if rule.Match(&nestedMetadata) {
return true
}
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return false
return stateSet
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,18 +7,18 @@ require github.com/sagernet/sing-box v0.0.0
replace github.com/sagernet/sing-box => ../
require (
github.com/docker/docker v28.0.0+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde
github.com/sagernet/sing-quic v0.6.0
github.com/sagernet/quic-go v0.59.0-sing-box-mod.2
github.com/sagernet/sing v0.8.0-beta.16
github.com/sagernet/sing-quic v0.6.0-beta.11
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/spyzhov/ajson v0.9.4
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.3.0
golang.org/x/net v0.50.0
golang.org/x/net v0.48.0
)
require (
@@ -28,17 +28,16 @@ require (
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
github.com/anthropics/anthropic-sdk-go v1.19.0 // indirect
github.com/anytls/sing-anytls v0.0.11 // indirect
github.com/caddyserver/certmagic v0.25.2 // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/caddyserver/certmagic v0.25.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/cretz/bine v0.2.0 // indirect
github.com/database64128/netx-go v0.1.1 // indirect
github.com/database64128/tfo-go/v2 v2.3.2 // indirect
github.com/database64128/tfo-go/v2 v2.3.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/distribution/reference v0.5.0 // indirect
@@ -49,7 +48,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -57,7 +56,7 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/godbus/dbus/v5 v5.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect
@@ -66,26 +65,26 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect
github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/keybase/go-keychain v0.0.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/acmedns v0.5.0 // indirect
github.com/libdns/alidns v1.0.6 // indirect
github.com/libdns/alidns v1.0.6-beta.3 // indirect
github.com/libdns/cloudflare v0.2.2 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mdlayher/netlink v1.9.0 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/metacubex/utls v1.8.4 // indirect
github.com/mholt/acmez/v3 v3.1.6 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mholt/acmez/v3 v3.1.4 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/openai/openai-go/v3 v3.26.0 // indirect
github.com/openai/openai-go/v3 v3.15.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
@@ -97,48 +96,41 @@ require (
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc // indirect
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287 // indirect
github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287 // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing-mux v0.3.4 // indirect
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d // indirect
github.com/sagernet/sing-tun v0.8.0-beta.17 // indirect
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 // indirect
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c // indirect
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 // indirect
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
@@ -157,29 +149,29 @@ require (
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect

View File

@@ -1,5 +1,3 @@
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -14,18 +12,16 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE=
github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
@@ -38,8 +34,8 @@ github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM=
github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc=
github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw=
github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724=
github.com/database64128/tfo-go/v2 v2.3.1 h1:EGE+ELd5/AQ0X6YBlQ9RgKs8+kciNhgN3d8lRvfEJQw=
github.com/database64128/tfo-go/v2 v2.3.1/go.mod h1:k9wcpg/8i5zenspBkc9jUEYehpZZccBnCElzOJB++bU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -47,10 +43,8 @@ github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbww
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -69,12 +63,10 @@ github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -88,8 +80,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/godbus/dbus/v5 v5.2.1 h1:I4wwMdWSkmI57ewd+elNGwLRf2/dtSaFz1DujfWYvOk=
github.com/godbus/dbus/v5 v5.2.1/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -112,8 +104,8 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY=
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 h1:MEufgJohwIjFi2n3eJv4c/8UdRLQVUwPwSWQPoER+eU=
github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
@@ -128,30 +120,26 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE=
github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ=
github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM=
github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
github.com/libdns/alidns v1.0.6-beta.3 h1:KAmb7FQ1tRzKsaAUGa7ZpGKAMRANwg7+1c7tUbSELq8=
github.com/libdns/alidns v1.0.6-beta.3/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -162,8 +150,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE=
github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo=
github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -189,68 +177,54 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc h1:YK7PwJT0irRAEui9ASdXSxcE2BOVQipWMF/A1Ogt+7c=
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc h1:EJPHOqk23IuBsTjXK9OXqkNxPbKOBWKRmviQoCcriAs=
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc/go.mod h1:8aty0RW96DrJSMWXO6bRPMBJEjuqq5JWiOIi4bCRzFA=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287 h1:0BYNmr0ptjsII948U0oBFmrbo4qEaCFcrE2JPRg3Zlk=
github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287 h1:ghxhYSBQpzkakqWqJDvXr/Zmxe0WjTjKuALEGbjGiGY=
github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287/go.mod h1:M+4ZjPhLJXIvoxcQsbDofmc19Wrig59hZ+hLvj6S3To=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f h1:8jZbZ4KBTdcXDFLwUBNQt5Xci6ZuAKh255S8TwuBCaM=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f h1:tG0hCx+0u5zca7qQ7AMkcv4DCrBG/DKW1ggs/P+BRRI=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f h1:ZXp5hKJIA7iJ52ZShJCKMQEPLpp/7dDIVZmPGV9Il40=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f h1:gL7H8HS8s38adz4/HZtRHh79qMwsbLTRRPz4GQ9LcWI=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f h1:Dchgc0pAY5Jwb5lzUlE+1nhHIzqLx+YOurXLHgvWd/0=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f h1:+MOLSQoduuKDxF410i1LcSPaQGaiP0eZb0INvMlmjM4=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f h1:lIZna05Vn6n8k21p8OpSUnhwGm+E57PrMjiI4ZUfMSg=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f h1:B2aFQ5CRHI20t8YsEizvtguS5W2QfK7D5XV/NzTIxPE=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f h1:qpSwJ1rFGYCfJDenNCZoWYjoG7N+xEa6ke+E7/JO1i4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f h1:cx7Ipg0tSvTDjS4maMEYz4vuzz93BMPAysmZ1YLrz80=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f h1:4jOHuUiBxD8pJEpBBVQfJqyLmxjpd3t4MLRzU7YLFyg=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f h1:OpXBa2WlRU+Mam9oRe9Nn4/zf7gQ+qiBTNK8A5RwbfQ=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f h1:nJpGFi+6hI85tl4zoyNFEnFEQ5+xEV5gyvsUoMvd8g0=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f h1:SEy2rpmgOJgrqcEryJI/RSnqUWIsEsp0cfYoA8y21jc=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f h1:EW2TuFMLm0iBGqRZtuGwIZdeYmDtDsDmRcRRJQOMxUo=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f h1:3U5woxrNCkzfv1+UX+mVoWh1228AE1qAiMG02F9oFbY=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f h1:YwFTfuWG3mmctroeDYtFZ6LHjGsedVO+5wInYbbUuUY=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f h1:r4V0ddPCRLgGu0VdgR3aUsO9NjpmyjAf+h+3oTD9D6E=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f h1:B8yf4gFvEYUnwWmtVK9sdwUsflYZ387MhYmlOP2ohFQ=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f h1:9YyaMg4rO1/jIgrxmNb0LKH+X7frSYWfX2pFgW5JUVM=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f h1:B0fnGu0sh9yT/9JDN5u/GqThGoOzNN/daOAuGWFLXEk=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f h1:lxPcIXKSSI5JDhc7rx/6yufISWM4vtBS2FY9PavWQTs=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPrSEgNII+HkfqC8hKMpYLodom4o4EU=
@@ -259,30 +233,30 @@ 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.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.2 h1:hJUL+HtxEOjxsa0CsucbBVqI/AMS4k52NwNU637zmdw=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.2/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.0-beta.16 h1:Fe+6E9VHYky9Mx4cf0ugbZPWDcXRflpAu7JQ5bWXvaA=
github.com/sagernet/sing v0.8.0-beta.16/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM=
github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-quic v0.6.0-beta.11 h1:eUusxITKKRedhWC2ScUYFUvD96h/QfbKLaS3N6/7in4=
github.com/sagernet/sing-quic v0.6.0-beta.11/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d h1:vi0j6301f6H8t2GYgAC2PA2AdnGdMwkP34B4+N03Qt4=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-tun v0.8.0-beta.17 h1:6DdbNXeTFYj8Tb4FCh8Mp2boA3rVY6VNqzTOObj7Xis=
github.com/sagernet/sing-tun v0.8.0-beta.17/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 h1:eYz/OpMqWCvO2++iw3dEuzrlfC2xv78GdlGvprIM6O8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 h1:E2tZFeg9mGYGQ7E7BbxMv1cU35HxwgRm6tPKI2Pp7DA=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -338,20 +312,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -370,26 +344,26 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -405,24 +379,24 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -433,19 +407,17 @@ golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -229,12 +229,13 @@ func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
}
func (e *Endpoint) Close() error {
if e.device != nil {
e.device.Close()
}
if e.pauseCallback != nil {
e.pause.UnregisterCallback(e.pauseCallback)
}
if e.device != nil {
e.device.Down()
e.device.Close()
}
return nil
}