Compare commits

...

75 Commits

Author SHA1 Message Date
世界
e6427e8244 Bump version 2026-04-10 16:24:31 +08:00
世界
c0d9551bcf documentation: Fix missing update for ip_version and query_type 2026-04-10 16:24:31 +08:00
世界
5cdf1aa000 Add cloudflared inbound 2026-04-10 16:24:31 +08:00
世界
6da0aa0c82 Fix lint errors 2026-04-10 16:24:30 +08:00
世界
97f4723467 platform: Wrap command RPC error returns with E.Cause 2026-04-10 16:24:30 +08:00
世界
6c7fb1dad1 Add package_name_regex route, DNS and headless rule item 2026-04-10 16:24:30 +08:00
世界
e0696f5e94 documentation: Fixes 2026-04-10 16:24:30 +08:00
世界
ddcaf040e2 Un-deprecate ip_accept_any DNS rule item 2026-04-10 16:24:29 +08:00
世界
57039ac11d tools: Tailscale status 2026-04-10 16:24:29 +08:00
世界
abd6baf3cb Fix darwin local DNS transport 2026-04-10 16:24:29 +08:00
世界
a48fd106c3 Fix rules lock 2026-04-10 16:24:29 +08:00
世界
6dfab9225f Revert "Also enable certificate store by default on Apple platforms"
This reverts commit 62cb06c02f.
2026-04-10 16:24:28 +08:00
世界
5e7e58f5e9 tools: Tailscale status 2026-04-10 16:24:28 +08:00
世界
cfcc766d74 platform: Fix darwin signal handler 2026-04-10 16:24:28 +08:00
世界
a24170638e tools: Network Quality & STUN 2026-04-10 16:24:27 +08:00
世界
ac9c0e7a81 oom-killer: Free memory on pressure notification and use gradual interval backoff 2026-04-10 16:24:27 +08:00
世界
51166f4601 Fix deprecated warning double-formatting on localized clients 2026-04-10 16:24:27 +08:00
世界
5d254d9015 platform: Fix set local 2026-04-10 16:24:26 +08:00
nekohasekai
d3fc58ceb8 Add evaluate DNS rule action and related rule items 2026-04-10 16:24:26 +08:00
世界
58d22df1be Also enable certificate store by default on Apple platforms
`SecTrustEvaluateWithError` is serial
2026-04-10 16:24:26 +08:00
世界
574852bdc1 platform: Add OOM Report & Crash Rerport 2026-04-10 16:24:25 +08:00
世界
ddc181f65a Add BBR profile and hop interval randomization for Hysteria2 2026-04-10 16:24:25 +08:00
nekohasekai
e2727d9556 Refactor ACME support to certificate provider 2026-04-10 16:24:25 +08:00
世界
f8b05790d1 cronet-go: Update chromium to 145.0.7632.159 2026-04-10 16:24:25 +08:00
世界
c1203821f9 documentation: Update descriptions for neighbor rules 2026-04-10 16:24:24 +08:00
世界
9805db343c Add macOS support for MAC and hostname rule items 2026-04-10 16:24:24 +08:00
世界
b28083b131 Add Android support for MAC and hostname rule items 2026-04-10 16:24:24 +08:00
世界
0d1ce7957d Add MAC and hostname rule items 2026-04-10 16:24:24 +08:00
世界
025b947a24 Bump version 2026-04-10 16:23:45 +08:00
世界
76fa3c2e5e tun: Fixes 2026-04-10 14:13:06 +08:00
世界
53db1f178c Fix tailscale crash 2026-04-10 14:09:03 +08:00
世界
55ec8abf17 Fix local DNS server for Android 2026-04-10 14:08:57 +08:00
Berkay Özdemirci
5a957fd750 Fix EDNS OPT record corruption in DNS cache
The TTL computation and assignment loops treat OPT record's Hdr.Ttl
as a regular TTL, but per RFC 6891 it encodes EDNS0 metadata
(ExtRCode|Version|Flags). This corrupts cached responses causing
systemd-resolved to reject them with EDNS version 255.

Also fix pointer aliasing: storeCache() stored raw *dns.Msg pointer
so subsequent mutations by Exchange() corrupted cached data.

- Skip OPT records in all TTL loops (Exchange + loadResponse)
- Use message.Copy() in storeCache() to isolate cache from mutations
2026-04-10 14:08:24 +08:00
TargetLocked
7c3d8cf8db Fix disable tcp keep alive 2026-04-10 13:29:15 +08:00
世界
813b634d08 Bump version 2026-04-06 23:09:11 +08:00
hdrover
d9b435fb62 Fix naive inbound padding bytes 2026-04-06 22:33:11 +08:00
世界
354b4b040e sing: Fix vectorised readv iovec length calculation
This does not seem to affect any actual paths in the sing-box.
2026-04-01 16:16:58 +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
世界
a3623eb41a tun: Fix system stack rewriting TUN subnet destinations to loopback 2026-03-23 19:38:55 +08:00
世界
72bc4c1f87 Fix DNS transport returning error for empty AAAA response
Closes #3925
2026-03-23 19:21:55 +08:00
世界
9ac1e2ff32 Match package_name in process_path rule on Android 2026-03-23 18:57:35 +08:00
世界
0045103d14 Fix package_name shared uid matching 2026-03-23 18:57:35 +08:00
世界
d2a933784c Optimize Darwin process finder 2026-03-23 18:57:35 +08:00
世界
3f05a37f65 Optimize Linux process finder 2026-03-23 18:57:35 +08:00
世界
b8e5a71450 Add process information cache to avoid duplicate lookups
PreMatch and full match phases each created a fresh InboundContext,
causing process search (expensive OS syscalls) to run twice per
connection. Use a freelru ShardedLRU cache with 200ms TTL to serve
the second lookup from cache.
2026-03-23 14:26:45 +08:00
世界
c13faa8e3c tailscale: Only set ProcessLocalIPs/ProcessSubnets for fake TUN 2026-03-23 14:16:40 +08:00
世界
7623bcd19e Fix DialerForICMPDestination 2026-03-23 13:58:55 +08:00
世界
795d1c2892 Fix nested rule-set match cache isolation 2026-03-23 12:26:19 +08:00
世界
6913b11e0a Reject removed legacy inbound fields instead of silently ignoring 2026-03-21 17:16:10 +08:00
世界
1e57c06295 daemon: Allow StartOrReloadService to recover from FATAL state 2026-03-21 13:37:14 +08:00
世界
ea464cef8d daemon: Fix CloseService leaving instance non-nil on close error 2026-03-21 13:23:57 +08:00
Andrew Novikov
a8e3cd3256 tun: Fix nfqueue not working in prerouting 2026-03-17 11:05:40 +08:00
世界
686cf1f304 documentation: Fix Chinese link anchors 2026-03-16 12:24:10 +08:00
世界
9fbfb87723 documentation: Fix unicode heading anchors 2026-03-16 12:10:32 +08:00
世界
d2fa21d07b Deprecate Socksaddr.IsFqdn: do not reject potentially valid domain names 2026-03-16 09:37:59 +08:00
世界
d3768cca36 Bump version 2026-03-15 17:56:37 +08:00
世界
0889ddd001 Fix connector canceled dial cleanup 2026-03-15 17:56:37 +08:00
深鸣
f46fbf188a documentation: Minor fixes 2026-03-15 17:56:37 +08:00
世界
f2d15139f5 tun: Fix nftables single include_uid not working 2026-03-15 16:58:34 +08:00
世界
041646b728 Fix kTLS crash 2026-03-14 21:38:38 +08:00
世界
b990de2e12 tun: Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" 2026-03-14 21:38:38 +08:00
世界
fe585157d2 Bump version 2026-03-14 21:38:38 +08:00
世界
eed6a36e5d tun:Fix auto_redirect dropping SO_BINDTODEVICE traffic 2026-03-14 21:38:38 +08:00
世界
eb0f38544c tailscale: Fix system interface rules 2026-03-14 21:38:38 +08:00
316 changed files with 22933 additions and 2418 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

View File

@@ -1 +1 @@
2fef65f9dba90ddb89a87d00a6eb6165487c10c1
ea7cd33752aed62603775af3df946c1b83f4b0b3

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

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

View File

@@ -0,0 +1,158 @@
package certificate
import (
"context"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ adapter.CertificateProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.CertificateProviderRegistry
access sync.Mutex
started bool
stage adapter.StartStage
providers []adapter.CertificateProviderService
providerByTag map[string]adapter.CertificateProviderService
}
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.CertificateProviderService),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
providers := m.providers
m.providers = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, provider := range providers {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, provider.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return err
}
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
m.access.Lock()
defer m.access.Unlock()
return m.providers
}
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
m.access.Lock()
provider, found := m.providerByTag[tag]
m.access.Unlock()
return provider, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.providerByTag, tag)
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
return it == provider
})
if index == -1 {
panic("invalid certificate provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return provider.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = existsProvider.Close()
if err != nil {
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid certificate provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
return nil
}

View File

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

View File

@@ -0,0 +1,38 @@
package adapter
import (
"context"
"crypto/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type CertificateProvider interface {
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
}
type ACMECertificateProvider interface {
CertificateProvider
GetACMENextProtos() []string
}
type CertificateProviderService interface {
Lifecycle
Type() string
Tag() string
CertificateProvider
}
type CertificateProviderRegistry interface {
option.CertificateProviderOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
}
type CertificateProviderManager interface {
Lifecycle
CertificateProviders() []CertificateProviderService
Get(tag string) (CertificateProviderService, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
}

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

@@ -2,6 +2,7 @@ package adapter
import (
"context"
"net"
"net/netip"
"time"
@@ -9,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 {
@@ -78,12 +81,16 @@ type InboundContext struct {
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
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
@@ -101,6 +108,10 @@ type InboundContext struct {
func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.ResetRuleMatchCache()
}
func (c *InboundContext) ResetRuleMatchCache() {
c.SourceAddressMatch = false
c.SourcePortMatch = false
c.DestinationAddressMatch = false
@@ -108,6 +119,51 @@ func (c *InboundContext) ResetRuleCache() {
c.DidMatch = false
}
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
return DNSResponseAddresses(c.DNSResponse)
}
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawRecord := range response.Answer {
switch record := rawRecord.(type) {
case *dns.A:
addr := M.AddrFromIP(record.A)
if addr.IsValid() {
addresses = append(addresses, addr)
}
case *dns.AAAA:
addr := M.AddrFromIP(record.AAAA)
if addr.IsValid() {
addresses = append(addresses, addr)
}
case *dns.HTTPS:
for _, value := range record.SVCB.Value {
switch hint := value.(type) {
case *dns.SVCBIPv4Hint:
for _, ip := range hint.Hint {
addr := M.AddrFromIP(ip).Unmap()
if addr.IsValid() {
addresses = append(addresses, addr)
}
}
case *dns.SVCBIPv6Hint:
for _, ip := range hint.Hint {
addr := M.AddrFromIP(ip)
if addr.IsValid() {
addresses = append(addresses, addr)
}
}
}
}
}
}
return addresses
}
type inboundContextKey struct{}
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {

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())
}

23
adapter/neighbor.go Normal file
View File

@@ -0,0 +1,23 @@
package adapter
import (
"net"
"net/netip"
)
type NeighborEntry struct {
Address netip.Addr
MACAddress net.HardwareAddr
Hostname string
}
type NeighborResolver interface {
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
LookupHostname(address netip.Addr) (string, bool)
Start() error
Close() error
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries []NeighborEntry)
}

View File

@@ -36,6 +36,10 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type FindConnectionOwnerRequest struct {
@@ -47,11 +51,11 @@ type FindConnectionOwnerRequest struct {
}
type ConnectionOwner struct {
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageName string
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageNames []string
}
type Notification struct {

View File

@@ -26,6 +26,8 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}
@@ -64,10 +66,16 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
type DNSRuleSetUpdateValidator interface {
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
}
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
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 {
@@ -29,7 +32,7 @@ type RuleAction interface {
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
return false
default:
return true

49
adapter/tailscale.go Normal file
View File

@@ -0,0 +1,49 @@
package adapter
import "context"
type TailscaleEndpoint interface {
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
}
type TailscalePingResult struct {
LatencyMs float64
IsDirect bool
Endpoint string
DERPRegionID int32
DERPRegionCode string
Error string
}
type TailscaleEndpointStatus struct {
BackendState string
AuthURL string
NetworkName string
MagicDNSSuffix string
Self *TailscalePeer
UserGroups []*TailscaleUserGroup
}
type TailscaleUserGroup struct {
UserID int64
LoginName string
DisplayName string
ProfilePicURL string
Peers []*TailscalePeer
}
type TailscalePeer struct {
HostName string
DNSName string
OS string
TailscaleIPs []string
Online bool
ExitNode bool
ExitNodeOption bool
Active bool
RxBytes int64
TxBytes int64
UserID int64
KeyExpiry int64
}

136
box.go
View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
@@ -19,7 +20,6 @@ import (
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/log"
@@ -37,20 +37,21 @@ import (
var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct {
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
certificateProvider *boxCertificate.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
}
type Options struct {
@@ -66,6 +67,7 @@ func Context(
endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
certificateProviderRegistry adapter.CertificateProviderRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -90,6 +92,10 @@ func Context(
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
}
return ctx
}
@@ -106,6 +112,7 @@ func New(options Options) (*Box, error) {
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
@@ -122,6 +129,9 @@ func New(options Options) (*Box, error) {
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
if certificateProviderRegistry == nil {
return nil, E.New("missing certificate provider registry in context")
}
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -179,13 +189,16 @@ func New(options Options) (*Box, error) {
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
@@ -272,6 +285,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize inbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
for i, outboundOptions := range options.Outbounds {
var tag string
if outboundOptions.Tag != "" {
@@ -298,22 +329,22 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
for i, certificateProviderOptions := range options.CertificateProviders {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
if certificateProviderOptions.Tag != "" {
tag = certificateProviderOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
err = certificateProviderManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
certificateProviderOptions.Type,
certificateProviderOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
}
}
outboundManager.Initialize(func() (adapter.Outbound, error) {
@@ -326,11 +357,12 @@ func New(options Options) (*Box, error) {
)
})
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
return local.NewTransport(
return dnsTransportRegistry.CreateDNSTransport(
ctx,
logFactory.NewLogger("dns/local"),
"local",
option.LocalDNSServerOptions{},
C.DNSTypeLocal,
&option.LocalDNSServerOptions{},
)
})
if platformInterface != nil {
@@ -383,20 +415,21 @@ func New(options Options) (*Box, error) {
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
return &Box{
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
certificateProvider: certificateProviderManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
}, nil
}
@@ -450,11 +483,11 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.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
}
@@ -470,11 +503,19 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
@@ -482,7 +523,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
@@ -506,8 +547,9 @@ func (s *Box) Close() error {
service adapter.Lifecycle
}{
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"certificate-provider", s.certificateProvider},
{"endpoint", s.endpoint},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
@@ -555,6 +597,10 @@ func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}
func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}

View File

@@ -82,6 +82,11 @@ func compileRuleSet(sourcePath string) error {
}
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.PackageNameRegex) > 0
}) {
version = C.RuleSetVersion4
}
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
len(rule.DefaultInterfaceAddress) > 0

View File

@@ -0,0 +1,121 @@
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var (
commandNetworkQualityFlagConfigURL string
commandNetworkQualityFlagSerial bool
commandNetworkQualityFlagMaxRuntime int
commandNetworkQualityFlagHTTP3 bool
)
var commandNetworkQuality = &cobra.Command{
Use: "networkquality",
Short: "Run a network quality test",
Run: func(cmd *cobra.Command, args []string) {
err := runNetworkQuality()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandNetworkQuality.Flags().StringVar(
&commandNetworkQualityFlagConfigURL,
"config-url", "",
"Network quality test config URL (default: Apple mensura)",
)
commandNetworkQuality.Flags().BoolVar(
&commandNetworkQualityFlagSerial,
"serial", false,
"Run download and upload tests sequentially instead of in parallel",
)
commandNetworkQuality.Flags().IntVar(
&commandNetworkQualityFlagMaxRuntime,
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
"Network quality maximum runtime in seconds",
)
commandNetworkQuality.Flags().BoolVar(
&commandNetworkQualityFlagHTTP3,
"http3", false,
"Use HTTP/3 (QUIC) for measurement traffic",
)
commandTools.AddCommand(commandNetworkQuality)
}
func runNetworkQuality() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
httpClient := networkquality.NewHTTPClient(dialer)
defer httpClient.CloseIdleConnections()
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
result, err := networkquality.Run(networkquality.Options{
ConfigURL: commandNetworkQualityFlagConfigURL,
HTTPClient: httpClient,
NewMeasurementClient: measurementClientFactory,
Serial: commandNetworkQualityFlagSerial,
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
Context: globalCtx,
OnProgress: func(p networkquality.Progress) {
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM,
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
return
}
switch networkquality.Phase(p.Phase) {
case networkquality.PhaseIdle:
if p.IdleLatencyMs > 0 {
fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs)
} else {
fmt.Fprint(os.Stderr, "\rMeasuring idle latency...")
}
case networkquality.PhaseDownload:
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM)
case networkquality.PhaseUpload:
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
}
},
})
if err != nil {
return err
}
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
return nil
}

View File

@@ -0,0 +1,79 @@
package main
import (
"fmt"
"os"
"github.com/sagernet/sing-box/common/stun"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var commandSTUNFlagServer string
var commandSTUN = &cobra.Command{
Use: "stun",
Short: "Run a STUN test",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := runSTUN()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address")
commandTools.AddCommand(commandSTUN)
}
func runSTUN() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "==== STUN TEST ====")
result, err := stun.Run(stun.Options{
Server: commandSTUNFlagServer,
Dialer: dialer,
Context: globalCtx,
OnProgress: func(p stun.Progress) {
switch p.Phase {
case stun.PhaseBinding:
if p.ExternalAddr != "" {
fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs)
} else {
fmt.Fprint(os.Stderr, "\rSending binding request...")
}
case stun.PhaseNATMapping:
fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...")
case stun.PhaseNATFiltering:
fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...")
}
},
})
if err != nil {
return err
}
fmt.Fprintln(os.Stderr)
fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr)
fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs)
if result.NATTypeSupported {
fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping)
fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering)
} else {
fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server")
}
return nil
}

View File

@@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else {
dialer.Timeout = C.TCPConnectTimeout
}
if !options.DisableTCPKeepAlive {
if options.DisableTCPKeepAlive {
dialer.KeepAlive = -1
dialer.KeepAliveConfig.Enable = false
} else {
keepIdle := time.Duration(options.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial
@@ -239,7 +242,7 @@ func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefaul
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() {
return nil, E.New("invalid address")
} else if address.IsFqdn() {
} else if address.IsDomain() {
return nil, E.New("domain not resolved")
}
if d.networkStrategy == nil {
@@ -329,9 +332,9 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
if !destination.Is6() {
return d.dialer6.Dialer
} else {
return d.dialer4.Dialer
} else {
return d.dialer6.Dialer
}
}

View File

@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
if !destination.IsDomain() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"net"
"unsafe"
)
func (c *Conn) Read(b []byte) (int, error) {
@@ -229,7 +230,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
data, typ, err = c.rawConn.In.Decrypt(record)
if err != nil {
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
return
}
return

View File

@@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
if !l.listenOptions.DisableTCPKeepAlive {
if l.listenOptions.DisableTCPKeepAlive {
listenConfig.KeepAlive = -1
listenConfig.KeepAliveConfig.Enable = false
} else {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial

View File

@@ -0,0 +1,142 @@
package networkquality
import (
"context"
"fmt"
"net"
"net/http"
"strings"
C "github.com/sagernet/sing-box/constant"
sBufio "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func FormatBitrate(bps int64) string {
switch {
case bps >= 1_000_000_000:
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
case bps >= 1_000_000:
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
case bps >= 1_000:
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
default:
return fmt.Sprintf("%d bps", bps)
}
}
func NewHTTPClient(dialer N.Dialer) *http.Client {
transport := &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
}
if dialer != nil {
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}
}
return &http.Client{Transport: transport}
}
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
if client == nil {
return nil, E.New("http client is nil")
}
if client.Transport == nil {
return http.DefaultTransport.(*http.Transport).Clone(), nil
}
transport, ok := client.Transport.(*http.Transport)
if !ok {
return nil, E.New("http client transport must be *http.Transport")
}
return transport.Clone(), nil
}
func newMeasurementClient(
baseClient *http.Client,
connectEndpoint string,
singleConnection bool,
disableKeepAlives bool,
readCounters []N.CountFunc,
writeCounters []N.CountFunc,
) (*http.Client, error) {
transport, err := baseTransportFromClient(baseClient)
if err != nil {
return nil, err
}
transport.DisableCompression = true
transport.DisableKeepAlives = disableKeepAlives
if singleConnection {
transport.MaxConnsPerHost = 1
transport.MaxIdleConnsPerHost = 1
transport.MaxIdleConns = 1
}
baseDialContext := transport.DialContext
if baseDialContext == nil {
dialer := &net.Dialer{}
baseDialContext = dialer.DialContext
}
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
dialAddr := addr
if connectEndpoint != "" {
dialAddr = rewriteDialAddress(addr, connectEndpoint)
}
conn, dialErr := baseDialContext(ctx, network, dialAddr)
if dialErr != nil {
return nil, dialErr
}
if len(readCounters) > 0 || len(writeCounters) > 0 {
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
}
return conn, nil
}
return &http.Client{
Transport: transport,
CheckRedirect: baseClient.CheckRedirect,
Jar: baseClient.Jar,
Timeout: baseClient.Timeout,
}, nil
}
type MeasurementClientFactory func(
connectEndpoint string,
singleConnection bool,
disableKeepAlives bool,
readCounters []N.CountFunc,
writeCounters []N.CountFunc,
) (*http.Client, error)
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
}
}
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
if !useHTTP3 {
return nil, nil
}
return NewHTTP3MeasurementClientFactory(dialer)
}
func rewriteDialAddress(addr string, connectEndpoint string) string {
connectEndpoint = strings.TrimSpace(connectEndpoint)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
if err == nil {
host = endpointHost
if endpointPort != "" {
port = endpointPort
}
} else if connectEndpoint != "" {
host = connectEndpoint
}
return net.JoinHostPort(host, port)
}

View File

@@ -0,0 +1,55 @@
//go:build with_quic
package networkquality
import (
"context"
"crypto/tls"
"net"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
sBufio "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
// singleConnection and disableKeepAlives are not applied:
// HTTP/3 multiplexes streams over a single QUIC connection by default.
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
transport := &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
dialAddr := addr
if connectEndpoint != "" {
dialAddr = rewriteDialAddress(addr, connectEndpoint)
}
destination := M.ParseSocksaddr(dialAddr)
var udpConn net.Conn
var dialErr error
if dialer != nil {
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
} else {
var netDialer net.Dialer
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
}
if dialErr != nil {
return nil, dialErr
}
wrappedConn := udpConn
if len(readCounters) > 0 || len(writeCounters) > 0 {
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
}
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
if dialErr != nil {
udpConn.Close()
return nil, dialErr
}
return quicConn, nil
},
}
return &http.Client{Transport: transport}, nil
}, nil
}

View File

@@ -0,0 +1,12 @@
//go:build !with_quic
package networkquality
import (
C "github.com/sagernet/sing-box/constant"
N "github.com/sagernet/sing/common/network"
)
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
return nil, C.ErrQUICNotIncluded
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ import (
type Searcher interface {
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
Close() error
}
var ErrNotFound = E.New("process not found")
@@ -28,7 +29,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
if err != nil {
return nil, err
}
if info.UserId != -1 {
if info.UserId != -1 && info.UserName == "" {
osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil {
info.UserName = osUser.Username

View File

@@ -6,6 +6,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
)
var _ Searcher = (*androidSearcher)(nil)
@@ -18,22 +19,30 @@ func NewSearcher(config Config) (Searcher, error) {
return &androidSearcher{config.PackageManager}, nil
}
func (s *androidSearcher) Close() error {
return nil
}
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
_, uid, err := resolveSocketByNetlink(network, source, destination)
family, protocol, err := socketDiagSettings(network, source)
if err != nil {
return nil, err
}
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageName: sharedPackage,
}, nil
_, uid, err := querySocketDiagOnce(family, protocol, source)
if err != nil {
return nil, err
}
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageName: packageName,
}, nil
appID := uid % 100000
var packageNames []string
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
packageNames = append(packageNames, sharedPackage)
}
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
packageNames = append(packageNames, packages...)
}
packageNames = common.Uniq(packageNames)
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageNames: packageNames,
}, nil
}

View File

@@ -1,19 +1,15 @@
//go:build darwin
package process
import (
"context"
"encoding/binary"
"net/netip"
"os"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
var _ Searcher = (*darwinSearcher)(nil)
@@ -24,12 +20,12 @@ func NewSearcher(_ Config) (Searcher, error) {
return &darwinSearcher{}, nil
}
func (d *darwinSearcher) Close() error {
return nil
}
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
if err != nil {
return nil, err
}
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
return FindDarwinConnectionOwner(network, source, destination)
}
var structSize = func() int {
@@ -47,107 +43,3 @@ var structSize = func() int {
return 384
}
}()
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
case N.NetworkTCP:
spath = "net.inet.tcp.pcblist_n"
case N.NetworkUDP:
spath = "net.inet.udp.pcblist_n"
default:
return "", os.ErrInvalid
}
isIPv4 := ip.Is4()
value, err := unix.SysctlRaw(spath)
if err != nil {
return "", err
}
buf := value
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := structSize
if network == N.NetworkTCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
inp, so := i, i+104
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
if uint16(port) != srcPort {
continue
}
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP netip.Addr
srcIsIPv4 := false
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80]))
srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80]))
default:
continue
}
if ip == srcIP {
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
// udp packet connection may be not equal with srcIP
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
pid := readNativeUint32(buf[so+68 : so+72])
fallbackUDPProcess, _ = getExecPathFromPID(pid)
}
}
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
return fallbackUDPProcess, nil
}
return "", ErrNotFound
}
func getExecPathFromPID(pid uint32) (string, error) {
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return unix.ByteSliceToString(buf), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}

View File

@@ -0,0 +1,269 @@
//go:build darwin
package process
import (
"encoding/binary"
"net/netip"
"os"
"sync"
"syscall"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
const (
darwinSnapshotTTL = 200 * time.Millisecond
darwinXinpgenSize = 24
darwinXsocketOffset = 104
darwinXinpcbForeignPort = 16
darwinXinpcbLocalPort = 18
darwinXinpcbVFlag = 44
darwinXinpcbForeignAddr = 48
darwinXinpcbLocalAddr = 64
darwinXinpcbIPv4Addr = 12
darwinXsocketUID = 64
darwinXsocketLastPID = 68
darwinTCPExtraStructSize = 208
)
type darwinConnectionEntry struct {
localAddr netip.Addr
remoteAddr netip.Addr
localPort uint16
remotePort uint16
pid uint32
uid int32
}
type darwinConnectionMatchKind uint8
const (
darwinConnectionMatchExact darwinConnectionMatchKind = iota
darwinConnectionMatchLocalFallback
darwinConnectionMatchWildcardFallback
)
type darwinSnapshot struct {
createdAt time.Time
entries []darwinConnectionEntry
}
type darwinConnectionFinder struct {
access sync.Mutex
ttl time.Duration
snapshots map[string]darwinSnapshot
builder func(string) (darwinSnapshot, error)
}
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
return &darwinConnectionFinder{
ttl: ttl,
snapshots: make(map[string]darwinSnapshot),
builder: buildDarwinSnapshot,
}
}
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
return sharedDarwinConnectionFinder.find(network, source, destination)
}
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
networkName := N.NetworkName(network)
source = normalizeDarwinAddrPort(source)
destination = normalizeDarwinAddrPort(destination)
var lastOwner *adapter.ConnectionOwner
for attempt := 0; attempt < 2; attempt++ {
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
if err != nil {
return nil, err
}
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
if err != nil {
if err == ErrNotFound && fromCache {
continue
}
return nil, err
}
if fromCache && matchKind != darwinConnectionMatchExact {
continue
}
owner := &adapter.ConnectionOwner{
UserId: entry.uid,
}
lastOwner = owner
if entry.pid == 0 {
return owner, nil
}
processPath, err := getExecPathFromPID(entry.pid)
if err == nil {
owner.ProcessPath = processPath
return owner, nil
}
if fromCache {
continue
}
return owner, nil
}
if lastOwner != nil {
return lastOwner, nil
}
return nil, ErrNotFound
}
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
f.access.Lock()
defer f.access.Unlock()
if !forceRefresh {
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
return snapshot, true, nil
}
}
snapshot, err := f.builder(network)
if err != nil {
return darwinSnapshot{}, false, err
}
f.snapshots[network] = snapshot
return snapshot, false, nil
}
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
spath, itemSize, err := darwinSnapshotSettings(network)
if err != nil {
return darwinSnapshot{}, err
}
value, err := unix.SysctlRaw(spath)
if err != nil {
return darwinSnapshot{}, err
}
return darwinSnapshot{
createdAt: time.Now(),
entries: parseDarwinSnapshot(value, itemSize),
}, nil
}
func darwinSnapshotSettings(network string) (string, int, error) {
itemSize := structSize
switch network {
case N.NetworkTCP:
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
case N.NetworkUDP:
return "net.inet.udp.pcblist_n", itemSize, nil
default:
return "", 0, os.ErrInvalid
}
}
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
inp := i
so := i + darwinXsocketOffset
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
if ok {
entries = append(entries, entry)
}
}
return entries
}
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
return darwinConnectionEntry{}, false
}
entry := darwinConnectionEntry{
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
}
flag := inp[darwinXinpcbVFlag]
switch {
case flag&0x1 != 0:
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
return entry, true
case flag&0x2 != 0:
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
return entry, true
default:
return darwinConnectionEntry{}, false
}
}
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
sourceAddr := source.Addr()
if !sourceAddr.IsValid() {
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
}
var localFallback darwinConnectionEntry
var hasLocalFallback bool
var wildcardFallback darwinConnectionEntry
var hasWildcardFallback bool
for _, entry := range entries {
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
continue
}
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
return entry, darwinConnectionMatchExact, nil
}
if !destination.IsValid() && entry.localAddr == sourceAddr {
return entry, darwinConnectionMatchExact, nil
}
if network != N.NetworkUDP {
continue
}
if !hasLocalFallback && entry.localAddr == sourceAddr {
hasLocalFallback = true
localFallback = entry
}
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
hasWildcardFallback = true
wildcardFallback = entry
}
}
if hasLocalFallback {
return localFallback, darwinConnectionMatchLocalFallback, nil
}
if hasWildcardFallback {
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
}
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
}
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
if !addrPort.IsValid() {
return addrPort
}
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
}
func getExecPathFromPID(pid uint32) (string, error) {
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return unix.ByteSliceToString(buf), nil
}

View File

@@ -4,33 +4,82 @@ package process
import (
"context"
"errors"
"net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
)
var _ Searcher = (*linuxSearcher)(nil)
type linuxSearcher struct {
logger log.ContextLogger
logger log.ContextLogger
diagConns [4]*socketDiagConn
processPathCache *uidProcessPathCache
}
func NewSearcher(config Config) (Searcher, error) {
return &linuxSearcher{config.Logger}, nil
searcher := &linuxSearcher{
logger: config.Logger,
processPathCache: newUIDProcessPathCache(time.Second),
}
for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} {
for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} {
searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol)
}
}
return searcher, nil
}
func (s *linuxSearcher) Close() error {
var errs []error
for _, conn := range s.diagConns {
if conn == nil {
continue
}
errs = append(errs, conn.Close())
}
return E.Errors(errs...)
}
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
inode, uid, err := resolveSocketByNetlink(network, source, destination)
inode, uid, err := s.resolveSocketByNetlink(network, source, destination)
if err != nil {
return nil, err
}
processPath, err := resolveProcessNameByProcSearch(inode, uid)
processInfo := &adapter.ConnectionOwner{
UserId: int32(uid),
}
processPath, err := s.processPathCache.findProcessPath(inode, uid)
if err != nil {
s.logger.DebugContext(ctx, "find process path: ", err)
} else {
processInfo.ProcessPath = processPath
}
return &adapter.ConnectionOwner{
UserId: int32(uid),
ProcessPath: processPath,
}, nil
return processInfo, nil
}
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
family, protocol, err := socketDiagSettings(network, source)
if err != nil {
return 0, 0, err
}
conn := s.diagConns[socketDiagConnIndex(family, protocol)]
if conn == nil {
return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol)
}
if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() {
inode, uid, err = conn.query(source, destination)
if err == nil {
return inode, uid, nil
}
if !errors.Is(err, ErrNotFound) {
return 0, 0, err
}
}
return querySocketDiagOnce(family, protocol, source)
}

View File

@@ -3,43 +3,67 @@
package process
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"errors"
"net/netip"
"os"
"path"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"unicode"
"unsafe"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
)
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
var nativeEndian = func() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
return binary.BigEndian
}
return binary.LittleEndian
}()
const (
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
socketDiagByFamily = 20
pathProc = "/proc"
sizeOfSocketDiagRequestData = 56
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
socketDiagResponseMinSize = 72
socketDiagByFamily = 20
pathProc = "/proc"
)
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
var family uint8
var protocol uint8
type socketDiagConn struct {
access sync.Mutex
family uint8
protocol uint8
fd int
}
type uidProcessPathCache struct {
cache freelru.Cache[uint32, *uidProcessPaths]
}
type uidProcessPaths struct {
entries map[uint32]string
}
func newSocketDiagConn(family, protocol uint8) *socketDiagConn {
return &socketDiagConn{
family: family,
protocol: protocol,
fd: -1,
}
}
func socketDiagConnIndex(family, protocol uint8) int {
index := 0
if protocol == syscall.IPPROTO_UDP {
index += 2
}
if family == syscall.AF_INET6 {
index++
}
return index
}
func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) {
switch network {
case N.NetworkTCP:
protocol = syscall.IPPROTO_TCP
@@ -48,151 +72,308 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
default:
return 0, 0, os.ErrInvalid
}
if source.Addr().Is4() {
switch {
case source.Addr().Is4():
family = syscall.AF_INET
} else {
case source.Addr().Is6():
family = syscall.AF_INET6
default:
return 0, 0, os.ErrInvalid
}
req := packSocketDiagRequest(family, protocol, source)
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
defer syscall.Close(socket)
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pad: 0,
Pid: 0,
Groups: 0,
})
if err != nil {
return
}
_, err = syscall.Write(socket, req)
if err != nil {
return 0, 0, E.Cause(err, "write netlink request")
}
buffer := buf.New()
defer buffer.Release()
n, err := syscall.Read(socket, buffer.FreeBytes())
if err != nil {
return 0, 0, E.Cause(err, "read netlink response")
}
buffer.Truncate(n)
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
if err != nil {
return 0, 0, E.Cause(err, "parse netlink message")
} else if len(messages) == 0 {
return 0, 0, E.New("unexcepted netlink response")
}
message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, E.New("netlink message: NLMSG_ERROR")
}
inode, uid = unpackSocketDiagResponse(&messages[0])
return
return family, protocol, nil
}
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
s := make([]byte, 16)
copy(s, source.Addr().AsSlice())
buf := make([]byte, sizeOfSocketDiagRequest)
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
nativeEndian.PutUint32(buf[8:12], 0)
nativeEndian.PutUint32(buf[12:16], 0)
buf[16] = family
buf[17] = protocol
buf[18] = 0
buf[19] = 0
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
binary.BigEndian.PutUint16(buf[24:26], source.Port())
binary.BigEndian.PutUint16(buf[26:28], 0)
copy(buf[28:44], s)
copy(buf[44:60], net.IPv6zero)
nativeEndian.PutUint32(buf[60:64], 0)
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
return buf
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
cache.SetLifetime(ttl)
return &uidProcessPathCache{cache: cache}
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < 72 {
return 0, 0
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
if cached, ok := c.cache.Get(uid); ok {
if processPath, found := cached.entries[targetInode]; found {
return processPath, nil
}
}
data := msg.Data
uid = nativeEndian.Uint32(data[64:68])
inode = nativeEndian.Uint32(data[68:72])
return
}
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
files, err := os.ReadDir(pathProc)
processPaths, err := buildProcessPathByUIDCache(uid)
if err != nil {
return "", err
}
c.cache.Add(uid, &uidProcessPaths{entries: processPaths})
processPath, found := processPaths[targetInode]
if !found {
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
}
return processPath, nil
}
func (c *socketDiagConn) Close() error {
c.access.Lock()
defer c.access.Unlock()
return c.closeLocked()
}
func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
c.access.Lock()
defer c.access.Unlock()
request := packSocketDiagRequest(c.family, c.protocol, source, destination, false)
for attempt := 0; attempt < 2; attempt++ {
err = c.ensureOpenLocked()
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
inode, uid, err = querySocketDiag(c.fd, request)
if err == nil || errors.Is(err, ErrNotFound) {
return inode, uid, err
}
if !shouldRetrySocketDiag(err) {
return 0, 0, err
}
_ = c.closeLocked()
}
return 0, 0, err
}
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
fd, err := openSocketDiag()
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
defer syscall.Close(fd)
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
}
func (c *socketDiagConn) ensureOpenLocked() error {
if c.fd != -1 {
return nil
}
fd, err := openSocketDiag()
if err != nil {
return err
}
c.fd = fd
return nil
}
func openSocketDiag() (int, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG)
if err != nil {
return -1, err
}
timeout := &syscall.Timeval{Usec: 100}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.Connect(fd, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pid: 0,
Groups: 0,
}); err != nil {
syscall.Close(fd)
return -1, err
}
return fd, nil
}
func (c *socketDiagConn) closeLocked() error {
if c.fd == -1 {
return nil
}
err := syscall.Close(c.fd)
c.fd = -1
return err
}
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte {
request := make([]byte, sizeOfSocketDiagRequest)
binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest)
binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily)
flags := uint16(syscall.NLM_F_REQUEST)
if dump {
flags |= syscall.NLM_F_DUMP
}
binary.NativeEndian.PutUint16(request[6:8], flags)
binary.NativeEndian.PutUint32(request[8:12], 0)
binary.NativeEndian.PutUint32(request[12:16], 0)
request[16] = family
request[17] = protocol
request[18] = 0
request[19] = 0
if dump {
binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF)
}
requestSource := source
requestDestination := destination
if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() {
// udp_dump_one expects the exact-match endpoints reversed for historical reasons.
requestSource, requestDestination = destination, source
}
binary.BigEndian.PutUint16(request[24:26], requestSource.Port())
binary.BigEndian.PutUint16(request[26:28], requestDestination.Port())
if family == syscall.AF_INET6 {
copy(request[28:44], requestSource.Addr().AsSlice())
if requestDestination.IsValid() {
copy(request[44:60], requestDestination.Addr().AsSlice())
}
} else {
copy(request[28:32], requestSource.Addr().AsSlice())
if requestDestination.IsValid() {
copy(request[44:48], requestDestination.Addr().AsSlice())
}
}
binary.NativeEndian.PutUint32(request[60:64], 0)
binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF)
return request
}
func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
_, err = syscall.Write(fd, request)
if err != nil {
return 0, 0, E.Cause(err, "write netlink request")
}
buffer := make([]byte, 64<<10)
n, err := syscall.Read(fd, buffer)
if err != nil {
return 0, 0, E.Cause(err, "read netlink response")
}
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
if err != nil {
return 0, 0, E.Cause(err, "parse netlink message")
}
return unpackSocketDiagMessages(messages)
}
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
for _, message := range messages {
switch message.Header.Type {
case syscall.NLMSG_DONE:
continue
case syscall.NLMSG_ERROR:
err = unpackSocketDiagError(&message)
if err != nil {
return 0, 0, err
}
case socketDiagByFamily:
inode, uid = unpackSocketDiagResponse(&message)
if inode != 0 || uid != 0 {
return inode, uid, nil
}
}
}
return 0, 0, ErrNotFound
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < socketDiagResponseMinSize {
return 0, 0
}
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
return inode, uid
}
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
if len(msg.Data) < 4 {
return E.New("netlink message: NLMSG_ERROR")
}
errno := int32(binary.NativeEndian.Uint32(msg.Data[:4]))
if errno == 0 {
return nil
}
if errno < 0 {
errno = -errno
}
sysErr := syscall.Errno(errno)
switch sysErr {
case syscall.ENOENT, syscall.ESRCH:
return ErrNotFound
default:
return E.New("netlink message: ", sysErr)
}
}
func shouldRetrySocketDiag(err error) bool {
return err != nil && !errors.Is(err, ErrNotFound)
}
func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) {
files, err := os.ReadDir(pathProc)
if err != nil {
return nil, err
}
buffer := make([]byte, syscall.PathMax)
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
processPaths := make(map[uint32]string)
for _, file := range files {
if !file.IsDir() || !isPid(file.Name()) {
continue
}
info, err := f.Info()
info, err := file.Info()
if err != nil {
return "", err
if isIgnorableProcError(err) {
continue
}
return nil, err
}
if info.Sys().(*syscall.Stat_t).Uid != uid {
continue
}
processPath := path.Join(pathProc, f.Name())
fdPath := path.Join(processPath, "fd")
processPath := filepath.Join(pathProc, file.Name())
fdPath := filepath.Join(processPath, "fd")
exePath, err := os.Readlink(filepath.Join(processPath, "exe"))
if err != nil {
if isIgnorableProcError(err) {
continue
}
return nil, err
}
fds, err := os.ReadDir(fdPath)
if err != nil {
continue
}
for _, fd := range fds {
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer)
if err != nil {
continue
}
if bytes.Equal(buffer[:n], socket) {
return os.Readlink(path.Join(processPath, "exe"))
inode, ok := parseSocketInode(buffer[:n])
if !ok {
continue
}
if _, loaded := processPaths[inode]; !loaded {
processPaths[inode] = exePath
}
}
}
return processPaths, nil
}
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
func isIgnorableProcError(err error) bool {
return os.IsNotExist(err) || os.IsPermission(err)
}
func parseSocketInode(link []byte) (uint32, bool) {
const socketPrefix = "socket:["
if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' {
return 0, false
}
var inode uint64
for _, char := range link[len(socketPrefix) : len(link)-1] {
if char < '0' || char > '9' {
return 0, false
}
inode = inode*10 + uint64(char-'0')
if inode > uint64(^uint32(0)) {
return 0, false
}
}
return uint32(inode), true
}
func isPid(s string) bool {

View File

@@ -0,0 +1,60 @@
//go:build linux
package process
import (
"net"
"net/netip"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestQuerySocketDiagUDPExact(t *testing.T) {
t.Parallel()
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer server.Close()
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
require.NoError(t, err)
defer client.Close()
err = client.SetDeadline(time.Now().Add(time.Second))
require.NoError(t, err)
_, err = client.Write([]byte{0})
require.NoError(t, err)
err = server.SetReadDeadline(time.Now().Add(time.Second))
require.NoError(t, err)
buffer := make([]byte, 1)
_, _, err = server.ReadFromUDP(buffer)
require.NoError(t, err)
source := addrPortFromUDPAddr(t, client.LocalAddr())
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
fd, err := openSocketDiag()
require.NoError(t, err)
defer syscall.Close(fd)
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
require.NoError(t, err)
require.NotZero(t, inode)
require.EqualValues(t, os.Getuid(), uid)
}
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
t.Helper()
udpAddr, ok := addr.(*net.UDPAddr)
require.True(t, ok)
ip, ok := netip.AddrFromSlice(udpAddr.IP)
require.True(t, ok)
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
}

View File

@@ -28,6 +28,10 @@ func initWin32API() error {
return winiphlpapi.LoadExtendedTable()
}
func (s *windowsSearcher) Close() error {
return nil
}
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
pid, err := winiphlpapi.FindPid(network, source)
if err != nil {

View File

@@ -46,6 +46,7 @@ const (
ruleItemNetworkIsConstrained
ruleItemNetworkInterfaceAddress
ruleItemDefaultInterfaceAddress
ruleItemPackageNameRegex
ruleItemFinal uint8 = 0xFF
)
@@ -215,6 +216,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
rule.ProcessPathRegex, err = readRuleItemString(reader)
case ruleItemPackageName:
rule.PackageName, err = readRuleItemString(reader)
case ruleItemPackageNameRegex:
rule.PackageNameRegex, err = readRuleItemString(reader)
case ruleItemWIFISSID:
rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID:
@@ -394,6 +397,15 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
return err
}
}
if len(rule.PackageNameRegex) > 0 {
if generateVersion < C.RuleSetVersion5 {
return E.New("`package_name_regex` rule item is only supported in version 5 or later")
}
err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex)
if err != nil {
return err
}
}
if len(rule.NetworkType) > 0 {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_type` rule item is only supported in version 3 or later")

607
common/stun/stun.go Normal file
View File

@@ -0,0 +1,607 @@
package stun
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net"
"net/netip"
"time"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
const (
DefaultServer = "stun.voipgate.com:3478"
magicCookie = 0x2112A442
headerSize = 20
bindingRequest = 0x0001
bindingSuccessResponse = 0x0101
bindingErrorResponse = 0x0111
attrMappedAddress = 0x0001
attrChangeRequest = 0x0003
attrErrorCode = 0x0009
attrXORMappedAddress = 0x0020
attrOtherAddress = 0x802c
familyIPv4 = 0x01
familyIPv6 = 0x02
changeIP = 0x04
changePort = 0x02
defaultRTO = 500 * time.Millisecond
minRTO = 250 * time.Millisecond
maxRetransmit = 2
)
type Phase int32
const (
PhaseBinding Phase = iota
PhaseNATMapping
PhaseNATFiltering
PhaseDone
)
type NATMapping int32
const (
NATMappingUnknown NATMapping = iota
_ // reserved
NATMappingEndpointIndependent
NATMappingAddressDependent
NATMappingAddressAndPortDependent
)
func (m NATMapping) String() string {
switch m {
case NATMappingEndpointIndependent:
return "Endpoint Independent"
case NATMappingAddressDependent:
return "Address Dependent"
case NATMappingAddressAndPortDependent:
return "Address and Port Dependent"
default:
return "Unknown"
}
}
type NATFiltering int32
const (
NATFilteringUnknown NATFiltering = iota
NATFilteringEndpointIndependent
NATFilteringAddressDependent
NATFilteringAddressAndPortDependent
)
func (f NATFiltering) String() string {
switch f {
case NATFilteringEndpointIndependent:
return "Endpoint Independent"
case NATFilteringAddressDependent:
return "Address Dependent"
case NATFilteringAddressAndPortDependent:
return "Address and Port Dependent"
default:
return "Unknown"
}
}
type TransactionID [12]byte
type Options struct {
Server string
Dialer N.Dialer
Context context.Context
OnProgress func(Progress)
}
type Progress struct {
Phase Phase
ExternalAddr string
LatencyMs int32
NATMapping NATMapping
NATFiltering NATFiltering
}
type Result struct {
ExternalAddr string
LatencyMs int32
NATMapping NATMapping
NATFiltering NATFiltering
NATTypeSupported bool
}
type parsedResponse struct {
xorMappedAddr netip.AddrPort
mappedAddr netip.AddrPort
otherAddr netip.AddrPort
}
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
if r.xorMappedAddr.IsValid() {
return r.xorMappedAddr, true
}
if r.mappedAddr.IsValid() {
return r.mappedAddr, true
}
return netip.AddrPort{}, false
}
type stunAttribute struct {
typ uint16
value []byte
}
func newTransactionID() TransactionID {
var id TransactionID
_, _ = rand.Read(id[:])
return id
}
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
attrLen := 0
for _, attr := range attrs {
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
}
buf := make([]byte, headerSize+attrLen)
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
copy(buf[8:20], txID[:])
offset := headerSize
for _, attr := range attrs {
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
}
return buf
}
func changeRequestAttr(flags byte) stunAttribute {
return stunAttribute{
typ: attrChangeRequest,
value: []byte{0, 0, 0, flags},
}
}
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
if len(data) < headerSize {
return nil, E.New("response too short")
}
msgType := binary.BigEndian.Uint16(data[0:2])
if msgType&0xC000 != 0 {
return nil, E.New("invalid STUN message: top 2 bits not zero")
}
cookie := binary.BigEndian.Uint32(data[4:8])
if cookie != magicCookie {
return nil, E.New("invalid magic cookie")
}
var txID TransactionID
copy(txID[:], data[8:20])
if txID != expectedTxID {
return nil, E.New("transaction ID mismatch")
}
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
if msgLen > len(data)-headerSize {
return nil, E.New("message length exceeds data")
}
attrData := data[headerSize : headerSize+msgLen]
if msgType == bindingErrorResponse {
return nil, parseErrorResponse(attrData)
}
if msgType != bindingSuccessResponse {
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
}
resp := &parsedResponse{}
offset := 0
for offset+4 <= len(attrData) {
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
if offset+4+attrLen > len(attrData) {
break
}
attrValue := attrData[offset+4 : offset+4+attrLen]
switch attrType {
case attrXORMappedAddress:
addr, err := parseXORMappedAddress(attrValue, txID)
if err == nil {
resp.xorMappedAddr = addr
}
case attrMappedAddress:
addr, err := parseMappedAddress(attrValue)
if err == nil {
resp.mappedAddr = addr
}
case attrOtherAddress:
addr, err := parseMappedAddress(attrValue)
if err == nil {
resp.otherAddr = addr
}
}
offset += 4 + attrLen + paddingLen(attrLen)
}
return resp, nil
}
func parseErrorResponse(data []byte) error {
offset := 0
for offset+4 <= len(data) {
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
if offset+4+attrLen > len(data) {
break
}
if attrType == attrErrorCode && attrLen >= 4 {
attrValue := data[offset+4 : offset+4+attrLen]
class := int(attrValue[2] & 0x07)
number := int(attrValue[3])
code := class*100 + number
if attrLen > 4 {
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
}
return E.New("STUN error ", code)
}
offset += 4 + attrLen + paddingLen(attrLen)
}
return E.New("STUN error response")
}
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
if len(data) < 4 {
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
}
family := data[1]
xPort := binary.BigEndian.Uint16(data[2:4])
port := xPort ^ uint16(magicCookie>>16)
switch family {
case familyIPv4:
if len(data) < 8 {
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
}
var ip [4]byte
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
case familyIPv6:
if len(data) < 20 {
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
}
var ip [16]byte
var xorKey [16]byte
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
copy(xorKey[4:16], txID[:])
for i := range 16 {
ip[i] = data[4+i] ^ xorKey[i]
}
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
default:
return netip.AddrPort{}, E.New("unknown address family: ", family)
}
}
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
if len(data) < 4 {
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
}
family := data[1]
port := binary.BigEndian.Uint16(data[2:4])
switch family {
case familyIPv4:
if len(data) < 8 {
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
}
return netip.AddrPortFrom(
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
), nil
case familyIPv6:
if len(data) < 20 {
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
}
var ip [16]byte
copy(ip[:], data[4:20])
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
default:
return netip.AddrPort{}, E.New("unknown address family: ", family)
}
}
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
request := buildBindingRequest(txID, attrs...)
currentRTO := rto
retransmitCount := 0
sendTime := time.Now()
_, err := conn.WriteTo(request, addr)
if err != nil {
return nil, 0, E.Cause(err, "send STUN request")
}
buf := make([]byte, 1024)
for {
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
if err != nil {
return nil, 0, E.Cause(err, "set read deadline")
}
n, _, readErr := conn.ReadFrom(buf)
if readErr != nil {
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
retransmitCount++
currentRTO *= 2
sendTime = time.Now()
_, err = conn.WriteTo(request, addr)
if err != nil {
return nil, 0, E.Cause(err, "retransmit STUN request")
}
continue
}
return nil, 0, E.Cause(readErr, "read STUN response")
}
if n < headerSize || buf[0]&0xC0 != 0 ||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
continue
}
var receivedTxID TransactionID
copy(receivedTxID[:], buf[8:20])
if receivedTxID != txID {
continue
}
latency := time.Since(sendTime)
resp, parseErr := parseResponse(buf[:n], txID)
if parseErr != nil {
return nil, 0, parseErr
}
return resp, latency, nil
}
}
func Run(options Options) (*Result, error) {
ctx := options.Context
if ctx == nil {
ctx = context.Background()
}
server := options.Server
if server == "" {
server = DefaultServer
}
serverSocksaddr := M.ParseSocksaddr(server)
if serverSocksaddr.Port == 0 {
serverSocksaddr.Port = 3478
}
reportProgress := options.OnProgress
if reportProgress == nil {
reportProgress = func(Progress) {}
}
var (
packetConn net.PacketConn
serverAddr net.Addr
err error
)
if options.Dialer != nil {
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
if err != nil {
return nil, E.Cause(err, "create UDP socket")
}
serverAddr = serverSocksaddr
} else {
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
if resolveErr != nil {
return nil, E.Cause(resolveErr, "resolve STUN server")
}
packetConn, err = net.ListenPacket("udp", "")
if err != nil {
return nil, E.Cause(err, "create UDP socket")
}
serverAddr = serverUDPAddr
}
defer func() {
_ = packetConn.Close()
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
rto := defaultRTO
// Phase 1: Binding
reportProgress(Progress{Phase: PhaseBinding})
txID := newTransactionID()
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
if err != nil {
return nil, E.Cause(err, "binding request")
}
rto = max(minRTO, 3*latency)
externalAddr, ok := resp.externalAddr()
if !ok {
return nil, E.New("no mapped address in response")
}
result := &Result{
ExternalAddr: externalAddr.String(),
LatencyMs: int32(latency.Milliseconds()),
}
reportProgress(Progress{
Phase: PhaseBinding,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
})
otherAddr := resp.otherAddr
if !otherAddr.IsValid() {
result.NATTypeSupported = false
reportProgress(Progress{
Phase: PhaseDone,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
})
return result, nil
}
result.NATTypeSupported = true
select {
case <-ctx.Done():
return result, nil
default:
}
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
reportProgress(Progress{
Phase: PhaseNATMapping,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
})
result.NATMapping = detectNATMapping(
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
)
reportProgress(Progress{
Phase: PhaseNATMapping,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NATMapping: result.NATMapping,
})
select {
case <-ctx.Done():
return result, nil
default:
}
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
reportProgress(Progress{
Phase: PhaseNATFiltering,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NATMapping: result.NATMapping,
})
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
reportProgress(Progress{
Phase: PhaseDone,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NATMapping: result.NATMapping,
NATFiltering: result.NATFiltering,
})
return result, nil
}
func detectNATMapping(
conn net.PacketConn,
serverPort uint16,
externalAddr netip.AddrPort,
otherAddr netip.AddrPort,
rto time.Duration,
) NATMapping {
// Mapping Test II: Send to other_ip:server_port
testIIAddr := net.UDPAddrFromAddrPort(
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
)
txID2 := newTransactionID()
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
if err != nil {
return NATMappingUnknown
}
externalAddr2, ok := resp2.externalAddr()
if !ok {
return NATMappingUnknown
}
if externalAddr == externalAddr2 {
return NATMappingEndpointIndependent
}
// Mapping Test III: Send to other_ip:other_port
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
txID3 := newTransactionID()
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
if err != nil {
return NATMappingUnknown
}
externalAddr3, ok := resp3.externalAddr()
if !ok {
return NATMappingUnknown
}
if externalAddr2 == externalAddr3 {
return NATMappingAddressDependent
}
return NATMappingAddressAndPortDependent
}
func detectNATFiltering(
conn net.PacketConn,
serverAddr net.Addr,
rto time.Duration,
) NATFiltering {
// Filtering Test II: Request response from different IP and port
txID := newTransactionID()
_, _, err := roundTrip(conn, serverAddr, txID,
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
if err == nil {
return NATFilteringEndpointIndependent
}
// Filtering Test III: Request response from different port only
txID = newTransactionID()
_, _, err = roundTrip(conn, serverAddr, txID,
[]stunAttribute{changeRequestAttr(changePort)}, rto)
if err == nil {
return NATFilteringAddressDependent
}
return NATFilteringAddressAndPortDependent
}
func paddingLen(n int) int {
if n%4 == 0 {
return 0
}
return 4 - n%4
}

View File

@@ -38,37 +38,6 @@ func (w *acmeWrapper) Close() error {
return nil
}
type acmeLogWriter struct {
logger logger.Logger
}
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.logger.Debug(logLine[7:])
default:
w.logger.Debug(logLine)
}
return len(p), nil
}
func (w *acmeLogWriter) Sync() error {
return nil
}
func encoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string
switch options.Provider {
@@ -91,8 +60,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
storage = certmagic.Default.Storage
}
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
&ACMELogWriter{Logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{
@@ -158,7 +127,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
} else {
tlsConfig = &tls.Config{
GetCertificate: config.GetCertificate,
NextProtos: []string{ACMETLS1Protocol},
NextProtos: []string{C.ACMETLS1Protocol},
}
}
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil

41
common/tls/acme_logger.go Normal file
View File

@@ -0,0 +1,41 @@
package tls
import (
"strings"
"github.com/sagernet/sing/common/logger"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type ACMELogWriter struct {
Logger logger.Logger
}
func (w *ACMELogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.Logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.Logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.Logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.Logger.Debug(logLine[7:])
default:
w.Logger.Debug(logLine)
}
return len(p), nil
}
func (w *ACMELogWriter) Sync() error {
return nil
}
func ACMEEncoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}

View File

@@ -32,6 +32,10 @@ type RealityServerConfig struct {
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
var tlsConfig utls.RealityConfig
if options.CertificateProvider != nil {
return nil, E.New("certificate_provider is unavailable in reality")
}
//nolint:staticcheck
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality")
}

View File

@@ -13,19 +13,87 @@ import (
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
)
var errInsecureUnused = E.New("tls: insecure unused")
type managedCertificateProvider interface {
adapter.CertificateProvider
adapter.SimpleLifecycle
}
type sharedCertificateProvider struct {
tag string
manager adapter.CertificateProviderManager
provider adapter.CertificateProviderService
}
func (p *sharedCertificateProvider) Start() error {
provider, found := p.manager.Get(p.tag)
if !found {
return E.New("certificate provider not found: ", p.tag)
}
p.provider = provider
return nil
}
func (p *sharedCertificateProvider) Close() error {
return nil
}
func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.provider.GetCertificate(hello)
}
func (p *sharedCertificateProvider) GetACMENextProtos() []string {
return getACMENextProtos(p.provider)
}
type inlineCertificateProvider struct {
provider adapter.CertificateProviderService
}
func (p *inlineCertificateProvider) Start() error {
for _, stage := range adapter.ListStartStages {
err := adapter.LegacyStart(p.provider, stage)
if err != nil {
return err
}
}
return nil
}
func (p *inlineCertificateProvider) Close() error {
return p.provider.Close()
}
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.provider.GetCertificate(hello)
}
func (p *inlineCertificateProvider) GetACMENextProtos() []string {
return getACMENextProtos(p.provider)
}
func getACMENextProtos(provider adapter.CertificateProvider) []string {
if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME {
return acmeProvider.GetACMENextProtos()
}
return nil
}
type STDServerConfig struct {
access sync.RWMutex
config *tls.Config
logger log.Logger
certificateProvider managedCertificateProvider
acmeService adapter.SimpleLifecycle
certificate []byte
key []byte
@@ -53,18 +121,17 @@ func (c *STDServerConfig) SetServerName(serverName string) {
func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
return c.config.NextProtos[1:]
} else {
return c.config.NextProtos
}
return c.config.NextProtos
}
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else {
config.NextProtos = nextProto
@@ -72,6 +139,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.config = config
}
func (c *STDServerConfig) hasACMEALPN() bool {
if c.acmeService != nil {
return true
}
if c.certificateProvider != nil {
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
return len(acmeProvider.GetACMENextProtos()) > 0
}
}
return false
}
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
return c.config, nil
}
@@ -91,15 +170,39 @@ func (c *STDServerConfig) Clone() Config {
}
func (c *STDServerConfig) Start() error {
if c.acmeService != nil {
return c.acmeService.Start()
} else {
err := c.startWatcher()
if c.certificateProvider != nil {
err := c.certificateProvider.Start()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
return err
}
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
nextProtos := acmeProvider.GetACMENextProtos()
if len(nextProtos) > 0 {
c.access.Lock()
config := c.config.Clone()
mergedNextProtos := append([]string{}, nextProtos...)
for _, nextProto := range config.NextProtos {
if !common.Contains(mergedNextProtos, nextProto) {
mergedNextProtos = append(mergedNextProtos, nextProto)
}
}
config.NextProtos = mergedNextProtos
c.config = config
c.access.Unlock()
}
}
return nil
}
if c.acmeService != nil {
err := c.acmeService.Start()
if err != nil {
return err
}
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
return nil
}
func (c *STDServerConfig) startWatcher() error {
@@ -203,23 +306,34 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
}
func (c *STDServerConfig) Close() error {
if c.acmeService != nil {
return c.acmeService.Close()
}
if c.watcher != nil {
return c.watcher.Close()
}
return nil
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
}
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
//nolint:staticcheck
if options.CertificateProvider != nil && options.ACME != nil {
return nil, E.New("certificate_provider and acme are mutually exclusive")
}
var tlsConfig *tls.Config
var certificateProvider managedCertificateProvider
var acmeService adapter.SimpleLifecycle
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
if options.CertificateProvider != nil {
certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider)
if err != nil {
return nil, err
}
tlsConfig = &tls.Config{
GetCertificate: certificateProvider.GetCertificate,
}
if options.Insecure {
return nil, errInsecureUnused
}
} else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck
deprecated.Report(ctx, deprecated.OptionInlineACME)
//nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil {
@@ -272,7 +386,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
certificate []byte
key []byte
)
if acmeService == nil {
if certificateProvider == nil && acmeService == nil {
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
@@ -360,6 +474,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
serverConfig := &STDServerConfig{
config: tlsConfig,
logger: logger,
certificateProvider: certificateProvider,
acmeService: acmeService,
certificate: certificate,
key: key,
@@ -369,8 +484,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
echKeyPath: echKeyPath,
}
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
serverConfig.access.RLock()
defer serverConfig.access.RUnlock()
return serverConfig.config, nil
}
var config ServerConfig = serverConfig
@@ -387,3 +502,27 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
}
return config, nil
}
func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
if options.IsShared() {
manager := service.FromContext[adapter.CertificateProviderManager](ctx)
if manager == nil {
return nil, E.New("missing certificate provider manager in context")
}
return &sharedCertificateProvider{
tag: options.Tag,
manager: manager,
}, nil
}
registry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if registry == nil {
return nil, E.New("missing certificate provider registry in context")
}
provider, err := registry.Create(ctx, logger, "", options.Type, options.Options)
if err != nil {
return nil, E.Cause(err, "create inline certificate provider")
}
return &inlineCertificateProvider{
provider: provider,
}, nil
}

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

@@ -1,36 +1,39 @@
package constant
const (
TypeTun = "tun"
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeTun = "tun"
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
TypeCloudflared = "cloudflared"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeACME = "acme"
TypeCloudflareOriginCA = "cloudflare-origin-ca"
)
const (
@@ -88,6 +91,8 @@ func ProxyDisplayName(proxyType string) string {
return "AnyTLS"
case TypeTailscale:
return "Tailscale"
case TypeCloudflared:
return "Cloudflared"
case TypeSelector:
return "Selector"
case TypeURLTest:

View File

@@ -23,12 +23,15 @@ const (
RuleSetVersion2
RuleSetVersion3
RuleSetVersion4
RuleSetVersionCurrent = RuleSetVersion4
RuleSetVersion5
RuleSetVersionCurrent = RuleSetVersion5
)
const (
RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options"
RuleActionTypeEvaluate = "evaluate"
RuleActionTypeRespond = "respond"
RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject"

View File

@@ -1,3 +1,3 @@
package tls
package constant
const ACMETLS1Protocol = "acme-tls/1"

View File

@@ -87,12 +87,17 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
}
}
}
if s.oomKiller && C.IsIos {
if s.oomKillerEnabled {
if !common.Any(options.Services, func(it option.Service) bool {
return it.Type == C.TypeOOMKiller
}) {
oomOptions := &option.OOMKillerServiceOptions{
KillerDisabled: s.oomKillerDisabled,
MemoryLimitOverride: s.oomMemoryLimit,
}
options.Services = append(options.Services, option.Service{
Type: C.TypeOOMKiller,
Type: C.TypeOOMKiller,
Options: oomOptions,
})
}
}

View File

@@ -5,5 +5,6 @@ type PlatformHandler interface {
ServiceReload() error
SystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
TriggerNativeCrash() error
WriteDebugMessage(message string)
}

View File

@@ -6,14 +6,20 @@ import (
"runtime"
"sync"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/common/stun"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
@@ -24,6 +30,8 @@ import (
"github.com/gofrs/uuid/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -32,10 +40,12 @@ var _ StartedServiceServer = (*StartedService)(nil)
type StartedService struct {
ctx context.Context
// platform adapter.PlatformInterface
handler PlatformHandler
debug bool
logMaxLines int
oomKiller bool
handler PlatformHandler
debug bool
logMaxLines int
oomKillerEnabled bool
oomKillerDisabled bool
oomMemoryLimit uint64
// workingDirectory string
// tempDirectory string
// userID int
@@ -64,10 +74,12 @@ type StartedService struct {
type ServiceOptions struct {
Context context.Context
// Platform adapter.PlatformInterface
Handler PlatformHandler
Debug bool
LogMaxLines int
OOMKiller bool
Handler PlatformHandler
Debug bool
LogMaxLines int
OOMKillerEnabled bool
OOMKillerDisabled bool
OOMMemoryLimit uint64
// WorkingDirectory string
// TempDirectory string
// UserID int
@@ -79,10 +91,12 @@ func NewStartedService(options ServiceOptions) *StartedService {
s := &StartedService{
ctx: options.Context,
// platform: options.Platform,
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
oomKiller: options.OOMKiller,
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
oomKillerEnabled: options.OOMKillerEnabled,
oomKillerDisabled: options.OOMKillerDisabled,
oomMemoryLimit: options.OOMMemoryLimit,
// workingDirectory: options.WorkingDirectory,
// tempDirectory: options.TempDirectory,
// userID: options.UserID,
@@ -168,7 +182,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
s.serviceAccess.Lock()
switch s.serviceStatus.Status {
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
default:
s.serviceAccess.Unlock()
return os.ErrInvalid
@@ -226,13 +240,14 @@ func (s *StartedService) CloseService() error {
return os.ErrInvalid
}
s.updateStatus(ServiceStatus_STOPPING)
if s.instance != nil {
err := s.instance.Close()
instance := s.instance
s.instance = nil
if instance != nil {
err := instance.Close()
if err != nil {
return s.updateStatusError(err)
}
}
s.instance = nil
s.startedAt = time.Time{}
s.updateStatus(ServiceStatus_IDLE)
s.serviceAccess.Unlock()
@@ -681,7 +696,42 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
if err != nil {
return nil, err
}
return nil, err
return &emptypb.Empty{}, nil
}
func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
if !s.debug {
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
}
if request == nil {
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
}
switch request.Type {
case DebugCrashRequest_GO:
time.AfterFunc(200*time.Millisecond, func() {
*(*int)(unsafe.Pointer(uintptr(0))) = 0
})
case DebugCrashRequest_NATIVE:
err := s.handler.TriggerNativeCrash()
if err != nil {
return nil, err
}
default:
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
instance := s.Instance()
if instance == nil {
return nil, status.Error(codes.FailedPrecondition, "service not started")
}
reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx)
if reporter == nil {
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
}
return &emptypb.Empty{}, reporter.WriteReport(memory.Total())
}
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
@@ -949,11 +999,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
var processInfo *ProcessInfo
if metadata.Metadata.ProcessInfo != nil {
processInfo = &ProcessInfo{
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
UserId: metadata.Metadata.ProcessInfo.UserId,
UserName: metadata.Metadata.ProcessInfo.UserName,
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
UserId: metadata.Metadata.ProcessInfo.UserId,
UserName: metadata.Metadata.ProcessInfo.UserName,
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
}
}
return &Connection{
@@ -1018,9 +1068,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
return &DeprecatedWarnings{
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
return &DeprecatedWarning{
Message: it.Message(),
Impending: it.Impending(),
MigrationLink: it.MigrationLink,
Message: it.Message(),
Impending: it.Impending(),
MigrationLink: it.MigrationLink,
Description: it.Description,
DeprecatedVersion: it.DeprecatedVersion,
ScheduledVersion: it.ScheduledVersion,
}
}),
}, nil
@@ -1032,6 +1085,386 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
}
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.urlTestObserver.Subscribe()
if err != nil {
return err
}
defer s.urlTestObserver.UnSubscribe(subscription)
for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
boxService := s.instance
s.serviceAccess.RUnlock()
historyStorage := boxService.urlTestHistoryStorage
var list OutboundList
for _, ob := range boxService.instance.Outbound().Outbounds() {
item := &GroupItem{
Tag: ob.Tag(),
Type: ob.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
for _, ep := range boxService.instance.Endpoint().Endpoints() {
item := &GroupItem{
Tag: ep.Tag(),
Type: ep.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
err = server.Send(&list)
if err != nil {
return err
}
select {
case <-subscription:
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
}
}
}
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
if tag == "" {
return instance.instance.Outbound().Default(), nil
}
outbound, loaded := instance.instance.Outbound().Outbound(tag)
if !loaded {
return nil, E.New("outbound not found: ", tag)
}
return outbound, nil
}
func (s *StartedService) StartNetworkQualityTest(
request *NetworkQualityTestRequest,
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
outbound, err := resolveOutbound(boxService, request.OutboundTag)
if err != nil {
return err
}
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
httpClient := networkquality.NewHTTPClient(resolvedDialer)
defer httpClient.CloseIdleConnections()
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3)
if err != nil {
return err
}
result, nqErr := networkquality.Run(networkquality.Options{
ConfigURL: request.ConfigURL,
HTTPClient: httpClient,
NewMeasurementClient: measurementClientFactory,
Serial: request.Serial,
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
Context: server.Context(),
OnProgress: func(p networkquality.Progress) {
_ = server.Send(&NetworkQualityTestProgress{
Phase: int32(p.Phase),
DownloadCapacity: p.DownloadCapacity,
UploadCapacity: p.UploadCapacity,
DownloadRPM: p.DownloadRPM,
UploadRPM: p.UploadRPM,
IdleLatencyMs: p.IdleLatencyMs,
ElapsedMs: p.ElapsedMs,
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
})
},
})
if nqErr != nil {
return server.Send(&NetworkQualityTestProgress{
IsFinal: true,
Error: nqErr.Error(),
})
}
return server.Send(&NetworkQualityTestProgress{
Phase: int32(networkquality.PhaseDone),
DownloadCapacity: result.DownloadCapacity,
UploadCapacity: result.UploadCapacity,
DownloadRPM: result.DownloadRPM,
UploadRPM: result.UploadRPM,
IdleLatencyMs: result.IdleLatencyMs,
IsFinal: true,
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
})
}
func (s *StartedService) StartSTUNTest(
request *STUNTestRequest,
server grpc.ServerStreamingServer[STUNTestProgress],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
outbound, err := resolveOutbound(boxService, request.OutboundTag)
if err != nil {
return err
}
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
result, stunErr := stun.Run(stun.Options{
Server: request.Server,
Dialer: resolvedDialer,
Context: server.Context(),
OnProgress: func(p stun.Progress) {
_ = server.Send(&STUNTestProgress{
Phase: int32(p.Phase),
ExternalAddr: p.ExternalAddr,
LatencyMs: p.LatencyMs,
NatMapping: int32(p.NATMapping),
NatFiltering: int32(p.NATFiltering),
})
},
})
if stunErr != nil {
return server.Send(&STUNTestProgress{
IsFinal: true,
Error: stunErr.Error(),
})
}
return server.Send(&STUNTestProgress{
Phase: int32(stun.PhaseDone),
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NatMapping: int32(result.NATMapping),
NatFiltering: int32(result.NATFiltering),
IsFinal: true,
NatTypeSupported: result.NATTypeSupported,
})
}
func (s *StartedService) SubscribeTailscaleStatus(
_ *emptypb.Empty,
server grpc.ServerStreamingServer[TailscaleStatusUpdate],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
if endpointManager == nil {
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
type tailscaleEndpoint struct {
tag string
provider adapter.TailscaleEndpoint
}
var endpoints []tailscaleEndpoint
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
continue
}
endpoints = append(endpoints, tailscaleEndpoint{
tag: endpoint.Tag(),
provider: provider,
})
}
if len(endpoints) == 0 {
return status.Error(codes.NotFound, "no Tailscale endpoint found")
}
type taggedStatus struct {
tag string
status *adapter.TailscaleEndpointStatus
}
updates := make(chan taggedStatus, len(endpoints))
ctx, cancel := context.WithCancel(server.Context())
defer cancel()
var waitGroup sync.WaitGroup
for _, endpoint := range endpoints {
waitGroup.Add(1)
go func(tag string, provider adapter.TailscaleEndpoint) {
defer waitGroup.Done()
_ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) {
select {
case updates <- taggedStatus{tag: tag, status: endpointStatus}:
case <-ctx.Done():
}
})
}(endpoint.tag, endpoint.provider)
}
go func() {
waitGroup.Wait()
close(updates)
}()
var tags []string
statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints))
for update := range updates {
if _, exists := statuses[update.tag]; !exists {
tags = append(tags, update.tag)
}
statuses[update.tag] = update.status
protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses))
for _, tag := range tags {
protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag]))
}
sendErr := server.Send(&TailscaleStatusUpdate{
Endpoints: protoEndpoints,
})
if sendErr != nil {
return sendErr
}
}
return nil
}
func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus {
userGroups := make([]*TailscaleUserGroup, len(s.UserGroups))
for i, group := range s.UserGroups {
peers := make([]*TailscalePeer, len(group.Peers))
for j, peer := range group.Peers {
peers[j] = tailscalePeerToProto(peer)
}
userGroups[i] = &TailscaleUserGroup{
UserID: group.UserID,
LoginName: group.LoginName,
DisplayName: group.DisplayName,
ProfilePicURL: group.ProfilePicURL,
Peers: peers,
}
}
result := &TailscaleEndpointStatus{
EndpointTag: tag,
BackendState: s.BackendState,
AuthURL: s.AuthURL,
NetworkName: s.NetworkName,
MagicDNSSuffix: s.MagicDNSSuffix,
UserGroups: userGroups,
}
if s.Self != nil {
result.Self = tailscalePeerToProto(s.Self)
}
return result
}
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
return &TailscalePeer{
HostName: peer.HostName,
DnsName: peer.DNSName,
Os: peer.OS,
TailscaleIPs: peer.TailscaleIPs,
Online: peer.Online,
ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption,
Active: peer.Active,
RxBytes: peer.RxBytes,
TxBytes: peer.TxBytes,
KeyExpiry: peer.KeyExpiry,
}
}
func (s *StartedService) StartTailscalePing(
request *TailscalePingRequest,
server grpc.ServerStreamingServer[TailscalePingResponse],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
if endpointManager == nil {
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
var provider adapter.TailscaleEndpoint
if request.EndpointTag != "" {
endpoint, loaded := endpointManager.Get(request.EndpointTag)
if !loaded {
return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag)
}
if endpoint.Type() != C.TypeTailscale {
return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag)
}
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
}
provider = pingProvider
} else {
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
if loaded {
provider = pingProvider
break
}
}
if provider == nil {
return status.Error(codes.NotFound, "no Tailscale endpoint found")
}
}
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
_ = server.Send(&TailscalePingResponse{
LatencyMs: result.LatencyMs,
IsDirect: result.IsDirect,
Endpoint: result.Endpoint,
DerpRegionID: result.DERPRegionID,
DerpRegionCode: result.DERPRegionCode,
Error: result.Error,
})
})
}
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,12 +26,20 @@ service StartedService {
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {}
rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {}
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
}
message ServiceStatus {
@@ -141,6 +149,15 @@ message SetSystemProxyEnabledRequest {
bool enabled = 1;
}
message DebugCrashRequest {
enum Type {
GO = 0;
NATIVE = 1;
}
Type type = 1;
}
message SubscribeConnectionsRequest {
int64 interval = 1;
}
@@ -195,7 +212,7 @@ message ProcessInfo {
int32 userId = 2;
string userName = 3;
string processPath = 4;
string packageName = 5;
repeated string packageNames = 5;
}
message CloseConnectionRequest {
@@ -210,8 +227,105 @@ message DeprecatedWarning {
string message = 1;
bool impending = 2;
string migrationLink = 3;
string description = 4;
string deprecatedVersion = 5;
string scheduledVersion = 6;
}
message StartedAt {
int64 startedAt = 1;
}
}
message OutboundList {
repeated GroupItem outbounds = 1;
}
message NetworkQualityTestRequest {
string configURL = 1;
string outboundTag = 2;
bool serial = 3;
int32 maxRuntimeSeconds = 4;
bool http3 = 5;
}
message NetworkQualityTestProgress {
int32 phase = 1;
int64 downloadCapacity = 2;
int64 uploadCapacity = 3;
int32 downloadRPM = 4;
int32 uploadRPM = 5;
int32 idleLatencyMs = 6;
int64 elapsedMs = 7;
bool isFinal = 8;
string error = 9;
int32 downloadCapacityAccuracy = 10;
int32 uploadCapacityAccuracy = 11;
int32 downloadRPMAccuracy = 12;
int32 uploadRPMAccuracy = 13;
}
message STUNTestRequest {
string server = 1;
string outboundTag = 2;
}
message STUNTestProgress {
int32 phase = 1;
string externalAddr = 2;
int32 latencyMs = 3;
int32 natMapping = 4;
int32 natFiltering = 5;
bool isFinal = 6;
string error = 7;
bool natTypeSupported = 8;
}
message TailscaleStatusUpdate {
repeated TailscaleEndpointStatus endpoints = 1;
}
message TailscaleEndpointStatus {
string endpointTag = 1;
string backendState = 2;
string authURL = 3;
string networkName = 4;
string magicDNSSuffix = 5;
TailscalePeer self = 6;
repeated TailscaleUserGroup userGroups = 7;
}
message TailscaleUserGroup {
int64 userID = 1;
string loginName = 2;
string displayName = 3;
string profilePicURL = 4;
repeated TailscalePeer peers = 5;
}
message TailscalePeer {
string hostName = 1;
string dnsName = 2;
string os = 3;
repeated string tailscaleIPs = 4;
bool online = 5;
bool exitNode = 6;
bool exitNodeOption = 7;
bool active = 8;
int64 rxBytes = 9;
int64 txBytes = 10;
int64 keyExpiry = 11;
}
message TailscalePingRequest {
string endpointTag = 1;
string peerIP = 2;
}
message TailscalePingResponse {
double latencyMs = 1;
bool isDirect = 2;
string endpoint = 3;
int32 derpRegionID = 4;
string derpRegionCode = 5;
string error = 6;
}

View File

@@ -15,27 +15,34 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
)
// StartedServiceClient is the client API for StartedService service.
@@ -58,11 +65,18 @@ type StartedServiceClient interface {
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error)
StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error)
StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error)
SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error)
StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error)
}
type startedServiceClient struct {
@@ -278,6 +292,26 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
return out, nil
}
func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
@@ -337,6 +371,101 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp
return out, nil
}
func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList]
func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress]
func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress]
func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate]
func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse]
// StartedServiceServer is the server API for StartedService service.
// All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility.
@@ -357,11 +486,18 @@ type StartedServiceServer interface {
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error
StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error
StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error
SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error
StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error
mustEmbedUnimplementedStartedServiceServer()
}
@@ -436,6 +572,14 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context,
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
}
func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
}
@@ -455,6 +599,26 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context,
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error {
return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented")
}
func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error {
return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented")
}
func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error {
return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error {
return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented")
}
func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error {
return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented")
}
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -729,6 +893,42 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DebugCrashRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_TriggerDebugCrash_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).TriggerOOMReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_TriggerOOMReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeConnectionsRequest)
if err := stream.RecvMsg(m); err != nil {
@@ -812,6 +1012,61 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList]
func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(NetworkQualityTestRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress]
func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(STUNTestRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress]
func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate]
func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(TailscalePingRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse]
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -863,6 +1118,14 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetSystemProxyEnabled",
Handler: _StartedService_SetSystemProxyEnabled_Handler,
},
{
MethodName: "TriggerDebugCrash",
Handler: _StartedService_TriggerDebugCrash_Handler,
},
{
MethodName: "TriggerOOMReport",
Handler: _StartedService_TriggerOOMReport_Handler,
},
{
MethodName: "CloseConnection",
Handler: _StartedService_CloseConnection_Handler,
@@ -911,6 +1174,31 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeConnections_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeOutbounds",
Handler: _StartedService_SubscribeOutbounds_Handler,
ServerStreams: true,
},
{
StreamName: "StartNetworkQualityTest",
Handler: _StartedService_StartNetworkQualityTest_Handler,
ServerStreams: true,
},
{
StreamName: "StartSTUNTest",
Handler: _StartedService_StartSTUNTest_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeTailscaleStatus",
Handler: _StartedService_SubscribeTailscaleStatus_Handler,
ServerStreams: true,
},
{
StreamName: "StartTailscalePing",
Handler: _StartedService_StartTailscalePing_Handler,
ServerStreams: true,
},
},
Metadata: "daemon/started_service.proto",
}

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 {
@@ -283,6 +278,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
@@ -294,6 +292,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
@@ -315,7 +316,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
@@ -381,26 +382,26 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
}
if c.disableExpire {
if !c.independentCache {
c.cache.Add(question, message)
c.cache.Add(question, message.Copy())
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message)
}, message.Copy())
}
} else {
if !c.independentCache {
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
} else {
c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message, time.Second*time.Duration(timeToLive))
}, message.Copy(), time.Second*time.Duration(timeToLive))
}
}
}
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(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,
@@ -486,6 +487,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl)
}
@@ -500,12 +504,18 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = record.Header().Ttl - duration
}
}
} else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = uint32(nowTTL)
}
}
@@ -515,25 +525,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 {

View File

@@ -5,10 +5,11 @@ import (
)
const (
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeServerFailure RcodeError = mDNS.RcodeServerFailure
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused
)
type RcodeError int

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))
}

View File

@@ -5,11 +5,13 @@ import (
"errors"
"net/netip"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule"
@@ -19,6 +21,7 @@ import (
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
"github.com/sagernet/sing/service"
@@ -26,7 +29,10 @@ import (
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSRouter = (*Router)(nil)
var (
_ adapter.DNSRouter = (*Router)(nil)
_ adapter.DNSRuleSetUpdateValidator = (*Router)(nil)
)
type Router struct {
ctx context.Context
@@ -34,10 +40,15 @@ type Router struct {
transport adapter.DNSTransportManager
outbound adapter.OutboundManager
client adapter.DNSClient
rawRules []option.DNSRule
rules []adapter.DNSRule
defaultDomainStrategy C.DomainStrategy
dnsReverseMapping freelru.Cache[netip.Addr, string]
platformInterface adapter.PlatformInterface
legacyDNSMode bool
rulesAccess sync.RWMutex
started bool
closing bool
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
@@ -46,6 +57,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
logger: logFactory.NewLogger("dns"),
transport: service.FromContext[adapter.DNSTransportManager](ctx),
outbound: service.FromContext[adapter.OutboundManager](ctx),
rawRules: make([]option.DNSRule, 0, len(options.Rules)),
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
}
@@ -74,13 +86,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
}
func (r *Router) Initialize(rules []option.DNSRule) error {
for i, ruleOptions := range rules {
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
if err != nil {
return E.Cause(err, "parse dns rule[", i, "]")
}
r.rules = append(r.rules, dnsRule)
r.rawRules = append(r.rawRules[:0], rules...)
newRules, _, _, err := r.buildRules(false)
if err != nil {
return err
}
closeRules(newRules)
return nil
}
@@ -92,32 +103,146 @@ func (r *Router) Start(stage adapter.StartStage) error {
r.client.Start()
monitor.Finish()
for i, rule := range r.rules {
monitor.Start("initialize DNS rule[", i, "]")
err := rule.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "initialize DNS rule[", i, "]")
}
monitor.Start("initialize DNS rules")
newRules, legacyDNSMode, modeFlags, err := r.buildRules(true)
monitor.Finish()
if err != nil {
return err
}
r.rulesAccess.Lock()
if r.closing {
r.rulesAccess.Unlock()
closeRules(newRules)
return nil
}
r.rules = newRules
r.legacyDNSMode = legacyDNSMode
r.started = true
r.rulesAccess.Unlock()
if legacyDNSMode && common.Any(newRules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() }) {
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter)
}
if legacyDNSMode && modeFlags.neededFromStrategy {
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSRuleStrategy)
}
}
return nil
}
func (r *Router) Close() error {
monitor := taskmonitor.New(r.logger, C.StopTimeout)
var err error
for i, rule := range r.rules {
monitor.Start("close dns rule[", i, "]")
err = E.Append(err, rule.Close(), func(err error) error {
return E.Cause(err, "close dns rule[", i, "]")
})
monitor.Finish()
r.rulesAccess.Lock()
if r.closing {
r.rulesAccess.Unlock()
return nil
}
return err
r.closing = true
runtimeRules := r.rules
r.rules = nil
r.rulesAccess.Unlock()
closeRules(runtimeRules)
return nil
}
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) {
for i, ruleOptions := range r.rawRules {
err := R.ValidateNoNestedDNSRuleActions(ruleOptions)
if err != nil {
return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]")
}
}
router := service.FromContext[adapter.Router](r.ctx)
legacyDNSMode, modeFlags, err := resolveLegacyDNSMode(router, r.rawRules, nil)
if err != nil {
return nil, false, dnsRuleModeFlags{}, err
}
if !legacyDNSMode {
err = validateLegacyDNSModeDisabledRules(r.rawRules)
if err != nil {
return nil, false, dnsRuleModeFlags{}, err
}
}
err = validateEvaluateFakeIPRules(r.rawRules, r.transport)
if err != nil {
return nil, false, dnsRuleModeFlags{}, err
}
newRules := make([]adapter.DNSRule, 0, len(r.rawRules))
for i, ruleOptions := range r.rawRules {
var dnsRule adapter.DNSRule
dnsRule, err = R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode)
if err != nil {
closeRules(newRules)
return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]")
}
newRules = append(newRules, dnsRule)
}
if startRules {
for i, rule := range newRules {
err = rule.Start()
if err != nil {
closeRules(newRules)
return nil, false, dnsRuleModeFlags{}, E.Cause(err, "initialize DNS rule[", i, "]")
}
}
}
return newRules, legacyDNSMode, modeFlags, nil
}
func closeRules(rules []adapter.DNSRule) {
for _, rule := range rules {
_ = rule.Close()
}
}
func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error {
if len(r.rawRules) == 0 {
return nil
}
router := service.FromContext[adapter.Router](r.ctx)
if router == nil {
return E.New("router service not found")
}
overrides := map[string]adapter.RuleSetMetadata{
tag: metadata,
}
r.rulesAccess.RLock()
started := r.started
legacyDNSMode := r.legacyDNSMode
closing := r.closing
r.rulesAccess.RUnlock()
if closing {
return nil
}
if !started {
candidateLegacyDNSMode, _, err := resolveLegacyDNSMode(router, r.rawRules, overrides)
if err != nil {
return err
}
if !candidateLegacyDNSMode {
return validateLegacyDNSModeDisabledRules(r.rawRules)
}
return nil
}
candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides)
if err != nil {
return err
}
if legacyDNSMode {
if !candidateLegacyDNSMode && flags.disabled {
err := validateLegacyDNSModeDisabledRules(r.rawRules)
if err != nil {
return err
}
return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
}
return nil
}
if candidateLegacyDNSMode {
return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
}
return nil
}
func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
panic("no context")
@@ -126,22 +251,18 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if ruleIndex != -1 {
currentRuleIndex = ruleIndex + 1
}
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
currentRule := r.rules[currentRuleIndex]
for ; currentRuleIndex < len(rules); currentRuleIndex++ {
currentRule := rules[currentRuleIndex]
if currentRule.WithAddressLimit() && !isAddressQuery {
continue
}
metadata.ResetRuleCache()
if currentRule.Match(metadata) {
displayRuleIndex := currentRuleIndex
if displayRuleIndex != -1 {
displayRuleIndex += displayRuleIndex + 1
}
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
metadata.DestinationAddressMatchFromResponse = false
if currentRule.LegacyPreMatch(metadata) {
if ruleDescription := currentRule.String(); ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
}
switch action := currentRule.Action().(type) {
case *R.RuleActionDNSRoute:
@@ -166,14 +287,6 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if action.ClientSubnet.IsValid() {
options.ClientSubnet = action.ClientSubnet
}
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
return transport, currentRule, currentRuleIndex
case *R.RuleActionDNSRouteOptions:
if action.Strategy != C.DomainStrategyAsIS {
@@ -196,15 +309,270 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
}
}
transport := r.transport.Default()
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
return transport, nil, -1
}
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) {
// Strategy is intentionally skipped here. A non-default DNS rule action strategy
// forces legacy mode via resolveLegacyDNSMode, so this path is only reachable
// when strategy remains at its default value.
if routeOptions.DisableCache {
options.DisableCache = true
}
if routeOptions.RewriteTTL != nil {
options.RewriteTTL = routeOptions.RewriteTTL
}
if routeOptions.ClientSubnet.IsValid() {
options.ClientSubnet = routeOptions.ClientSubnet
}
}
type dnsRouteStatus uint8
const (
dnsRouteStatusMissing dnsRouteStatus = iota
dnsRouteStatusSkipped
dnsRouteStatusResolved
)
func (r *Router) resolveDNSRoute(server string, routeOptions R.RuleActionDNSRouteOptions, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus) {
transport, loaded := r.transport.Transport(server)
if !loaded {
return nil, dnsRouteStatusMissing
}
isFakeIP := transport.Type() == C.DNSTypeFakeIP
if isFakeIP && !allowFakeIP {
return transport, dnsRouteStatusSkipped
}
r.applyDNSRouteOptions(options, routeOptions)
if isFakeIP {
options.DisableCache = true
}
return transport, dnsRouteStatusResolved
}
func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) {
if ruleDescription := currentRule.String(); ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", ruleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "match[", ruleIndex, "] => ", currentRule.Action())
}
}
type exchangeWithRulesResult struct {
response *mDNS.Msg
transport adapter.DNSTransport
rejectAction *R.RuleActionReject
err error
}
const dnsRespondMissingResponseMessage = "respond action requires an evaluated response from a preceding evaluate action"
func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
panic("no context")
}
effectiveOptions := options
var evaluatedResponse *mDNS.Msg
var evaluatedTransport adapter.DNSTransport
for currentRuleIndex, currentRule := range rules {
metadata.ResetRuleCache()
metadata.DNSResponse = evaluatedResponse
metadata.DestinationAddressMatchFromResponse = false
if !currentRule.Match(metadata) {
continue
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
r.logRuleMatch(ctx, currentRuleIndex, currentRule)
switch action := currentRule.Action().(type) {
case *R.RuleActionDNSRouteOptions:
r.applyDNSRouteOptions(&effectiveOptions, *action)
case *R.RuleActionEvaluate:
queryOptions := effectiveOptions
transport, loaded := r.transport.Transport(action.Server)
if !loaded {
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
evaluatedResponse = nil
evaluatedTransport = nil
continue
}
r.applyDNSRouteOptions(&queryOptions, action.RuleActionDNSRouteOptions)
exchangeOptions := queryOptions
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
exchangeOptions.Strategy = r.defaultDomainStrategy
}
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
if err != nil {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
evaluatedResponse = nil
evaluatedTransport = nil
continue
}
evaluatedResponse = response
evaluatedTransport = transport
case *R.RuleActionRespond:
if evaluatedResponse == nil {
return exchangeWithRulesResult{
err: E.New(dnsRespondMissingResponseMessage),
}
}
return exchangeWithRulesResult{
response: evaluatedResponse,
transport: evaluatedTransport,
}
case *R.RuleActionDNSRoute:
queryOptions := effectiveOptions
transport, status := r.resolveDNSRoute(action.Server, action.RuleActionDNSRouteOptions, allowFakeIP, &queryOptions)
switch status {
case dnsRouteStatusMissing:
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
continue
case dnsRouteStatusSkipped:
continue
}
exchangeOptions := queryOptions
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
exchangeOptions.Strategy = r.defaultDomainStrategy
}
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
return exchangeWithRulesResult{
response: response,
transport: transport,
err: err,
}
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return exchangeWithRulesResult{
response: &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
},
rejectAction: action,
}
case C.RuleActionRejectMethodDrop:
return exchangeWithRulesResult{
rejectAction: action,
err: tun.ErrDrop,
}
}
case *R.RuleActionPredefined:
return exchangeWithRulesResult{
response: action.Response(message),
}
}
}
return transport, nil, -1
transport := r.transport.Default()
exchangeOptions := effectiveOptions
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
exchangeOptions.Strategy = r.defaultDomainStrategy
}
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
return exchangeWithRulesResult{
response: response,
transport: transport,
err: err,
}
}
func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.DomainStrategy {
if options.LookupStrategy != C.DomainStrategyAsIS {
return options.LookupStrategy
}
if options.Strategy != C.DomainStrategyAsIS {
return options.Strategy
}
return r.defaultDomainStrategy
}
func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.QueryType = qType
metadata.IPVersion = 0
switch qType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
return ctx
}
func filterAddressesByQueryType(addresses []netip.Addr, qType uint16) []netip.Addr {
switch qType {
case mDNS.TypeA:
return common.Filter(addresses, func(address netip.Addr) bool {
return address.Is4()
})
case mDNS.TypeAAAA:
return common.Filter(addresses, func(address netip.Addr) bool {
return address.Is6()
})
default:
return addresses
}
}
func (r *Router) lookupWithRules(ctx context.Context, rules []adapter.DNSRule, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
strategy := r.resolveLookupStrategy(options)
lookupOptions := options
if strategy != C.DomainStrategyAsIS {
lookupOptions.Strategy = strategy
}
if strategy == C.DomainStrategyIPv4Only {
return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions)
}
if strategy == C.DomainStrategyIPv6Only {
return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions)
}
var (
response4 []netip.Addr
response6 []netip.Addr
)
var group task.Group
group.Append("exchange4", func(ctx context.Context) error {
result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions)
response4 = result
return err
})
group.Append("exchange6", func(ctx context.Context) error {
result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions)
response6 = result
return err
})
err := group.Run(ctx)
if len(response4) == 0 && len(response6) == 0 {
return nil, err
}
return sortAddresses(response4, response6, strategy), nil
}
func (r *Router) lookupWithRulesType(ctx context.Context, rules []adapter.DNSRule, domain string, qType uint16, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{{
Name: mDNS.Fqdn(domain),
Qtype: qType,
Qclass: mDNS.ClassINET,
}},
}
exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), rules, request, options, false)
if exchangeResult.rejectAction != nil {
return nil, exchangeResult.rejectAction.Error(ctx)
}
if exchangeResult.err != nil {
return nil, exchangeResult.err
}
if exchangeResult.response.Rcode != mDNS.RcodeSuccess {
return nil, RcodeError(exchangeResult.response.Rcode)
}
return filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType), nil
}
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
@@ -220,6 +588,14 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
return &responseMessage, nil
}
r.rulesAccess.RLock()
if r.closing {
r.rulesAccess.RUnlock()
return nil, E.New("dns router closed")
}
rules := r.rules
legacyDNSMode := r.legacyDNSMode
r.rulesAccess.RUnlock()
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var (
response *mDNS.Msg
@@ -230,6 +606,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
metadata.DNSResponse = nil
metadata.DestinationAddressMatchFromResponse = false
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
@@ -239,18 +617,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else if !legacyDNSMode {
exchangeResult := r.exchangeWithRules(ctx, rules, message, options, true)
response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err
} else {
var (
rule adapter.DNSRule
@@ -260,7 +633,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
@@ -278,7 +651,9 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
err = nil
response = action.Response(message)
goto done
}
}
responseCheck := addressLimitResponseCheck(rule, metadata)
@@ -306,6 +681,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
break
}
}
done:
if err != nil {
return nil, err
}
@@ -325,6 +701,14 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
r.rulesAccess.RLock()
if r.closing {
r.rulesAccess.RUnlock()
return nil, E.New("dns router closed")
}
rules := r.rules
legacyDNSMode := r.legacyDNSMode
r.rulesAccess.RUnlock()
var (
responseAddrs []netip.Addr
err error
@@ -338,6 +722,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
r.logger.DebugContext(ctx, "response rejected for ", domain)
} else if R.IsRejected(err) {
r.logger.DebugContext(ctx, "lookup rejected for ", domain)
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
}
@@ -350,20 +736,16 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.Domain = FqdnToDomain(domain)
metadata.DNSResponse = nil
metadata.DestinationAddressMatchFromResponse = false
if options.Transport != nil {
transport := options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
} else if !legacyDNSMode {
responseAddrs, err = r.lookupWithRules(ctx, rules, domain, options)
} else {
var (
transport adapter.DNSTransport
@@ -374,7 +756,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
@@ -425,15 +807,14 @@ func isAddressQuery(message *mDNS.Msg) bool {
return false
}
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool {
if rule == nil || !rule.WithAddressLimit() {
return nil
}
responseMetadata := *metadata
return func(responseAddrs []netip.Addr) bool {
return func(response *mDNS.Msg) bool {
checkMetadata := responseMetadata
checkMetadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(&checkMetadata)
return rule.MatchAddressLimit(&checkMetadata, response)
}
}
@@ -458,3 +839,265 @@ func (r *Router) ResetNetwork() {
transport.Reset()
}
}
func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool {
if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
return true
}
return !rule.MatchResponse && (rule.IPAcceptAny || len(rule.IPCIDR) > 0 || rule.IPIsPrivate)
}
func hasResponseMatchFields(rule option.DefaultDNSRule) bool {
return rule.ResponseRcode != nil ||
len(rule.ResponseAnswer) > 0 ||
len(rule.ResponseNs) > 0 ||
len(rule.ResponseExtra) > 0
}
func defaultRuleDisablesLegacyDNSMode(rule option.DefaultDNSRule) bool {
return rule.MatchResponse ||
hasResponseMatchFields(rule) ||
rule.Action == C.RuleActionTypeEvaluate ||
rule.Action == C.RuleActionTypeRespond ||
rule.IPVersion > 0 ||
len(rule.QueryType) > 0
}
type dnsRuleModeFlags struct {
disabled bool
needed bool
neededFromStrategy bool
}
func (f *dnsRuleModeFlags) merge(other dnsRuleModeFlags) {
f.disabled = f.disabled || other.disabled
f.needed = f.needed || other.needed
f.neededFromStrategy = f.neededFromStrategy || other.neededFromStrategy
}
func resolveLegacyDNSMode(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, dnsRuleModeFlags, error) {
flags, err := dnsRuleModeRequirements(router, rules, metadataOverrides)
if err != nil {
return false, flags, err
}
if flags.disabled && flags.neededFromStrategy {
return false, flags, E.New(deprecated.OptionLegacyDNSRuleStrategy.MessageWithLink())
}
if flags.disabled {
return false, flags, nil
}
return flags.needed, flags, nil
}
func dnsRuleModeRequirements(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
var flags dnsRuleModeFlags
for i, rule := range rules {
ruleFlags, err := dnsRuleModeRequirementsInRule(router, rule, metadataOverrides)
if err != nil {
return dnsRuleModeFlags{}, E.Cause(err, "dns rule[", i, "]")
}
flags.merge(ruleFlags)
}
return flags, nil
}
func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
switch rule.Type {
case "", C.RuleTypeDefault:
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides)
case C.RuleTypeLogical:
flags := dnsRuleModeFlags{
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond,
neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
}
flags.needed = flags.neededFromStrategy
for i, subRule := range rule.LogicalOptions.Rules {
subFlags, err := dnsRuleModeRequirementsInRule(router, subRule, metadataOverrides)
if err != nil {
return dnsRuleModeFlags{}, E.Cause(err, "sub rule[", i, "]")
}
flags.merge(subFlags)
}
return flags, nil
default:
return dnsRuleModeFlags{}, nil
}
}
func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
flags := dnsRuleModeFlags{
disabled: defaultRuleDisablesLegacyDNSMode(rule),
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
}
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
if len(rule.RuleSet) == 0 {
return flags, nil
}
if router == nil {
return dnsRuleModeFlags{}, E.New("router service not found")
}
for _, tag := range rule.RuleSet {
metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides)
if err != nil {
return dnsRuleModeFlags{}, err
}
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
flags.disabled = flags.disabled || metadata.ContainsDNSQueryTypeRule
if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule {
flags.needed = true
}
}
return flags, nil
}
func lookupDNSRuleSetMetadata(router adapter.Router, tag string, metadataOverrides map[string]adapter.RuleSetMetadata) (adapter.RuleSetMetadata, error) {
if metadataOverrides != nil {
if metadata, loaded := metadataOverrides[tag]; loaded {
return metadata, nil
}
}
ruleSet, loaded := router.RuleSet(tag)
if !loaded {
return adapter.RuleSetMetadata{}, E.New("rule-set not found: ", tag)
}
return ruleSet.Metadata(), nil
}
func referencedDNSRuleSetTags(rules []option.DNSRule) []string {
tagMap := make(map[string]bool)
var walkRule func(rule option.DNSRule)
walkRule = func(rule option.DNSRule) {
switch rule.Type {
case "", C.RuleTypeDefault:
for _, tag := range rule.DefaultOptions.RuleSet {
tagMap[tag] = true
}
case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules {
walkRule(subRule)
}
}
}
for _, rule := range rules {
walkRule(rule)
}
tags := make([]string, 0, len(tagMap))
for tag := range tagMap {
if tag != "" {
tags = append(tags, tag)
}
}
return tags
}
func validateLegacyDNSModeDisabledRules(rules []option.DNSRule) error {
var seenEvaluate bool
for i, rule := range rules {
requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(rule)
if err != nil {
return E.Cause(err, "validate dns rule[", i, "]")
}
if requiresPriorEvaluate && !seenEvaluate {
return E.New("dns rule[", i, "]: response-based matching requires a preceding evaluate action")
}
if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate {
seenEvaluate = true
}
}
return nil
}
func validateEvaluateFakeIPRules(rules []option.DNSRule, transportManager adapter.DNSTransportManager) error {
if transportManager == nil {
return nil
}
for i, rule := range rules {
if dnsRuleActionType(rule) != C.RuleActionTypeEvaluate {
continue
}
server := dnsRuleActionServer(rule)
if server == "" {
continue
}
transport, loaded := transportManager.Transport(server)
if !loaded || transport.Type() != C.DNSTypeFakeIP {
continue
}
return E.New("dns rule[", i, "]: evaluate action cannot use fakeip server: ", server)
}
return nil
}
func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) {
switch rule.Type {
case "", C.RuleTypeDefault:
return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions)
case C.RuleTypeLogical:
requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond
for i, subRule := range rule.LogicalOptions.Rules {
subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(subRule)
if err != nil {
return false, E.Cause(err, "sub rule[", i, "]")
}
requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate
}
return requiresPriorEvaluate, nil
default:
return false, nil
}
}
func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) {
hasResponseRecords := hasResponseMatchFields(rule)
if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate || rule.IPAcceptAny) && !rule.MatchResponse {
return false, E.New("Response Match Fields (ip_cidr, ip_is_private, ip_accept_any, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled")
}
// Intentionally do not reject rule_set here. A referenced rule set may mix
// destination-IP predicates with pre-response predicates such as domain items.
// When match_response is false, those destination-IP branches fail closed during
// pre-response evaluation instead of consuming DNS response state, while sibling
// non-response branches remain matchable.
if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink())
}
return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil
}
func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
return C.DomainStrategy(action.RouteOptions.Strategy) != C.DomainStrategyAsIS
case C.RuleActionTypeRouteOptions:
return C.DomainStrategy(action.RouteOptionsOptions.Strategy) != C.DomainStrategyAsIS
default:
return false
}
}
func dnsRuleActionType(rule option.DNSRule) string {
switch rule.Type {
case "", C.RuleTypeDefault:
if rule.DefaultOptions.Action == "" {
return C.RuleActionTypeRoute
}
return rule.DefaultOptions.Action
case C.RuleTypeLogical:
if rule.LogicalOptions.Action == "" {
return C.RuleActionTypeRoute
}
return rule.LogicalOptions.Action
default:
return ""
}
}
func dnsRuleActionServer(rule option.DNSRule) string {
switch rule.Type {
case "", C.RuleTypeDefault:
return rule.DefaultOptions.RouteOptions.Server
case C.RuleTypeLogical:
return rule.LogicalOptions.RouteOptions.Server
default:
return ""
}
}

2547
dns/router_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,12 @@ type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
type connectorDialResult[T any] struct {
connection T
cancel context.CancelFunc
err error
}
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
@@ -100,41 +106,37 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
return zero, err
}
c.connecting = make(chan struct{})
connecting := make(chan struct{})
c.connecting = connecting
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
dialResult := make(chan connectorDialResult[T], 1)
c.access.Unlock()
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
connection, cancel, err := c.dialWithCancellation(dialContext)
go func() {
connection, cancel, err := c.dialWithCancellation(dialContext)
dialResult <- connectorDialResult[T]{
connection: connection,
cancel: cancel,
err: err,
}
}()
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
select {
case result := <-dialResult:
return c.completeDial(ctx, connecting, result)
case <-ctx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ctx.Err()
case <-c.closeCtx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ErrTransportClosed
}
if err = ctx.Err(); err != nil {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, err
}
c.connection = connection
c.hasConnection = true
c.connectionCancel = cancel
result := c.connection
c.access.Unlock()
return result, nil
}
}
@@ -143,6 +145,38 @@ func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T
return loaded && dialConnector == connector
}
func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) {
var zero T
c.access.Lock()
defer c.access.Unlock()
defer func() {
if c.connecting == connecting {
c.connecting = nil
}
close(connecting)
}()
if result.err != nil {
return zero, result.err
}
if c.closed || c.closeCtx.Err() != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, ErrTransportClosed
}
if err := ctx.Err(); err != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, err
}
c.connection = result.connection
c.hasConnection = true
c.connectionCancel = result.cancel
return c.connection, nil
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {

View File

@@ -188,13 +188,157 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
err := <-result
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 1, closeCount.Load())
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err = connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
dialStarted := make(chan struct{}, 1)
releaseDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
select {
case dialStarted <- struct{}{}:
default:
}
<-releaseDial
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
result <- err
}()
<-dialStarted
cancel()
select {
case err := <-result:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("Get did not return after request cancel")
}
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 0, closeCount.Load())
close(releaseDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err := connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
firstDialStarted := make(chan struct{}, 1)
secondDialStarted := make(chan struct{}, 1)
releaseFirstDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
attempt := dialCount.Add(1)
switch attempt {
case 1:
select {
case firstDialStarted <- struct{}{}:
default:
}
<-releaseFirstDial
case 2:
select {
case secondDialStarted <- struct{}{}:
default:
}
}
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
firstResult := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
firstResult <- err
}()
<-firstDialStarted
cancel()
secondResult := make(chan error, 1)
go func() {
_, err := connector.Get(context.Background())
secondResult <- err
}()
select {
case <-secondDialStarted:
t.Fatal("second dial started before first dial completed")
case <-time.After(100 * time.Millisecond):
}
select {
case err := <-firstResult:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("first Get did not return after request cancel")
}
close(releaseFirstDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
select {
case <-secondDialStarted:
case <-time.After(time.Second):
t.Fatal("second dial did not start after first dial completed")
}
err := <-secondResult
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
t.Parallel()

View File

@@ -7,7 +7,6 @@ import (
"strings"
"syscall"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
@@ -40,13 +39,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, servers, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = dns.RcodeSuccess
}
}
select {
case results <- queryResult{response, err}:
case <-returned:

View File

@@ -4,8 +4,6 @@ package local
import (
"context"
"errors"
"net"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
@@ -14,7 +12,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -35,10 +32,8 @@ type Transport struct {
logger logger.ContextLogger
hosts *hosts.File
dialer N.Dialer
preferGo bool
fallback bool
dhcpTransport dhcpTransport
resolver net.Resolver
}
type dhcpTransport interface {
@@ -52,14 +47,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
if err != nil {
return nil, err
}
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
return &Transport{
TransportAdapter: transportAdapter,
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
ctx: ctx,
logger: logger,
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
preferGo: options.PreferGo,
}, nil
}
@@ -97,44 +90,3 @@ func (t *Transport) Reset() {
t.dhcpTransport.Reset()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
if !t.fallback {
return t.exchange(ctx, message, question.Name)
}
if t.dhcpTransport != nil {
dhcpTransports := t.dhcpTransport.Fetch()
if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
}
if t.preferGo {
// Assuming the user knows what they are doing, we still execute the query which will fail.
return t.exchange(ctx, message, question.Name)
}
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string
if question.Qtype == mDNS.TypeA {
network = "ip4"
} else {
network = "ip6"
}
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
}

View File

@@ -0,0 +1,162 @@
//go:build darwin
package local
/*
#include <stdlib.h>
#include <resolv.h>
#include <netdb.h>
static void *cgo_res_init() {
res_state state = calloc(1, sizeof(struct __res_state));
if (state == NULL) return NULL;
if (res_ninit(state) != 0) {
free(state);
return NULL;
}
return state;
}
static void cgo_res_destroy(void *opaque) {
res_state state = (res_state)opaque;
res_ndestroy(state);
free(state);
}
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
unsigned char *answer, int anslen,
int timeout_seconds,
int *out_h_errno) {
res_state state = (res_state)opaque;
state->retrans = timeout_seconds;
state->retry = 1;
int n = res_nsearch(state, dname, class, type, answer, anslen);
if (n < 0) {
*out_h_errno = state->res_h_errno;
}
return n;
}
*/
import "C"
import (
"context"
"errors"
"time"
"unsafe"
boxC "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
state := C.cgo_res_init()
if state == nil {
return nil, E.New("res_ninit failed")
}
defer C.cgo_res_destroy(state)
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
var hErrno C.int
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
C.int(timeoutSeconds),
&hErrno)
if n >= 0 {
if int(n) > bufSize {
bufSize = int(n)
continue
}
var response mDNS.Msg
err := response.Unpack(answer[:int(n)])
if err != nil {
return nil, E.Cause(err, "unpack res_nsearch response")
}
return &response, nil
}
var response mDNS.Msg
_ = response.Unpack(answer[:bufSize])
if response.Response {
if response.Truncated && bufSize < 65535 {
bufSize *= 2
if bufSize > 65535 {
bufSize = 65535
}
continue
}
return &response, nil
}
switch hErrno {
case C.HOST_NOT_FOUND:
return nil, dns.RcodeNameError
case C.TRY_AGAIN:
return nil, dns.RcodeNameError
case C.NO_RECOVERY:
return nil, dns.RcodeServerFailure
case C.NO_DATA:
return nil, dns.RcodeSuccess
default:
return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name)
}
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
}
}
if t.fallback && t.dhcpTransport != nil {
dhcpServers := t.dhcpTransport.Fetch()
if len(dhcpServers) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
}
}
name := question.Name
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
remaining := time.Until(deadline)
if remaining <= 0 {
return nil, context.DeadlineExceeded
}
seconds := int(remaining.Seconds())
if seconds < 1 {
seconds = 1
}
timeoutSeconds = seconds
}
type resolvResult struct {
response *mDNS.Msg
err error
}
resultCh := make(chan resolvResult, 1)
go func() {
response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
resultCh <- resolvResult{response, err}
}()
var result resolvResult
select {
case <-ctx.Done():
return nil, ctx.Err()
case result = <-resultCh:
}
if result.err != nil {
var rcodeError dns.RcodeError
if errors.As(result.err, &rcodeError) {
return dns.FixedResponseStatus(message, int(rcodeError)), nil
}
return nil, result.err
}
result.response.Id = message.Id
return result.response, nil
}

View File

@@ -1,3 +1,5 @@
//go:build !darwin
package local
import (
@@ -7,7 +9,6 @@ import (
"syscall"
"time"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
@@ -49,13 +50,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result")
}
}
select {
case results <- queryResult{response, err}:
case <-returned:

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,6 +2,186 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.10
* Add `evaluate` DNS rule action and Response Match Fields **1**
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
* Add `package_name_regex` route, DNS and headless rule item **3**
* Add cloudflared inbound **4**
* Fixes and improvements
**1**:
Response Match Fields
([`response_rcode`](/configuration/dns/rule/#response_rcode),
[`response_answer`](/configuration/dns/rule/#response_answer),
[`response_ns`](/configuration/dns/rule/#response_ns),
and [`response_extra`](/configuration/dns/rule/#response_extra))
match the evaluated DNS response. They are gated by the new
[`match_response`](/configuration/dns/rule/#match_response) field and
populated by a preceding
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
the evaluated response can also be returned directly by a
[`respond`](/configuration/dns/rule_action/#respond) action.
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
`ip_is_private` without `match_response`) in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
in sing-box 1.16.0.
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
**2**:
`ip_version` and `query_type` in DNS rules, together with `query_type` in
referenced rule-sets, now take effect on every DNS rule evaluation,
including matches from internal domain resolutions that do not target a
specific DNS server (for example a `resolve` route rule action without
`server` set). In earlier versions they were silently ignored in that
path. Combining these fields with any of the legacy DNS fields deprecated
in **1** in the same DNS configuration is no longer supported and is
rejected at startup.
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
**3**:
See [Route Rule](/configuration/route/rule/#package_name_regex),
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
**4**:
See [Cloudflared](/configuration/inbound/cloudflared/).
#### 1.13.7
* Fixes and improvement
#### 1.13.6
* Fixes and improvements
#### 1.14.0-alpha.8
* Add BBR profile and hop interval randomization for Hysteria2 **1**
* Fixes and improvements
**1**:
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
#### 1.14.0-alpha.8
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.14.0-alpha.7
* Fixes and improvements
#### 1.13.4
* Fixes and improvements
#### 1.14.0-alpha.4
* Refactor ACME support to certificate provider system **1**
* Add Cloudflare Origin CA certificate provider **2**
* Add Tailscale certificate provider **3**
* Fixes and improvements
**1**:
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
**2**:
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
**3**:
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.12.24
* Fixes and improvements
#### 1.14.0-alpha.2
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.14.0-alpha.1
* Add `source_mac_address` and `source_hostname` rule items **1**
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
* Update NaiveProxy to 145.0.7632.159 **3**
* Fixes and improvements
**1**:
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
Supported on Linux, macOS, or in graphical clients on Android and macOS.
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
**2**:
Limit or exclude devices from TUN routing by MAC address.
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
See [TUN](/configuration/inbound/tun/#include_mac_address).
**3**:
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
#### 1.13.2
* Fixes and improvements
@@ -623,7 +803,7 @@ DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
Compatibility for old formats will be removed in sing-box 1.14.0.
@@ -1093,7 +1273,7 @@ DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
Compatibility for old formats will be removed in sing-box 1.14.0.
@@ -1929,7 +2109,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
@@ -1943,7 +2123,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users
**5**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
**6**:
@@ -2124,7 +2304,7 @@ See [TUN](/configuration/inbound/tun) inbound.
**1**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
#### 1.9.0-alpha.7
@@ -2171,7 +2351,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.

View File

@@ -42,6 +42,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
| `process_path` | :material-close: | No permission |
| `process_path_regex` | :material-close: | No permission |
| `package_name` | :material-check: | / |
| `package_name_regex` | :material-check: | / |
| `user` | :material-close: | Use `package_name` instead |
| `user_id` | :material-close: | Use `package_name` instead |
| `wifi_ssid` | :material-check: | Fine location permission required |

View File

@@ -44,6 +44,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
| `process_path` | :material-close: | No permission |
| `process_path_regex` | :material-close: | No permission |
| `package_name` | :material-close: | / |
| `package_name_regex` | :material-close: | / |
| `user` | :material-close: | No permission |
| `user_id` | :material-close: | No permission |
| `wifi_ssid` | :material-alert: | Only supported on iOS |

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-server-formats).
### Structure
@@ -26,6 +26,6 @@ Enable FakeIP service.
IPv4 address range for FakeIP.
#### inet6_address
#### inet6_range
IPv6 address range for FakeIP.

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 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
旧的 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

@@ -2,6 +2,20 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [match_response](#match_response)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -89,12 +103,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
],
@@ -124,6 +132,9 @@ icon: material/alert-decagram
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"user": [
"sekai"
],
@@ -149,6 +160,12 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -160,7 +177,17 @@ 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,
"ip_accept_any": false,
"response_rcode": "",
"response_answer": [],
"response_ns": [],
"response_extra": [],
"invert": false,
"outbound": [
"direct"
@@ -169,7 +196,8 @@ icon: material/alert-decagram
"server": "local",
// Deprecated
"rule_set_ip_cidr_accept_empty": false,
"rule_set_ipcidr_match_source": false,
"geosite": [
"cn"
@@ -209,7 +237,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
@@ -217,12 +245,46 @@ Tags of [Inbound](/configuration/inbound/).
#### ip_version
!!! quote "Changes in sing-box 1.14.0"
This field now also applies when a DNS rule is matched from an internal
domain resolution that does not target a specific DNS server, such as a
[`resolve`](../../route/rule_action/#resolve) route rule action without a
`server` set. In earlier versions, only DNS queries received from a
client evaluated this field. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
for the full list.
Setting this field makes the DNS rule incompatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
action and [`match_response`](#match_response).
4 (A DNS query) or 6 (AAAA DNS query).
Not limited if empty.
#### query_type
!!! quote "Changes in sing-box 1.14.0"
This field now also applies when a DNS rule is matched from an internal
domain resolution that does not target a specific DNS server, such as a
[`resolve`](../../route/rule_action/#resolve) route rule action without a
`server` set. In earlier versions, only DNS queries received from a
client evaluated this field. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
for the full list.
Setting this field makes the DNS rule incompatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
action and [`match_response`](#match_response).
DNS query type. Values can be integers or type name strings.
#### network
@@ -325,6 +387,12 @@ Match process path using regular expression.
Match android package name.
#### package_name_regex
!!! question "Since sing-box 1.14.0"
Match android package name using regular expression.
#### user
!!! quote ""
@@ -408,6 +476,26 @@ Matches network interface (same values as `network_type`) address.
Match default interface address.
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### wifi_ssid
!!! quote ""
@@ -446,6 +534,25 @@ 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 the evaluated response
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
instead of only matching the original query.
The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`).
Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields.
#### ip_accept_any
!!! question "Since sing-box 1.12.0"
Match when the DNS query response contains at least one address.
#### invert
Invert match result.
@@ -490,7 +597,12 @@ See [DNS Rule Actions](../rule_action/) for details.
Moved to [DNS Rule Action](../rule_action#route).
### Address Filter Fields
### Legacy Address Filter Fields
!!! failure "Deprecated in sing-box 1.14.0"
Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
@@ -516,23 +628,61 @@ Match GeoIP with query response.
Match IP CIDR with query response.
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
#### ip_is_private
!!! question "Since sing-box 1.9.0"
Match private IP with query response.
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
#### rule_set_ip_cidr_accept_empty
!!! question "Since sing-box 1.10.0"
!!! failure "Deprecated in sing-box 1.14.0"
`rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
Make `ip_cidr` rules in rule-sets accept empty query response.
#### ip_accept_any
### Response Match Fields
!!! question "Since sing-box 1.12.0"
!!! question "Since sing-box 1.14.0"
Match any IP with query response.
Match fields for the evaluated response. Require `match_response` to be set to `true`
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
#### response_rcode
Match DNS response code.
Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode).
#### response_answer
Match DNS answer records.
Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer).
#### response_ns
Match DNS name server records.
Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns).
#### response_extra
Match DNS extra records.
Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra).
### Logical Fields
@@ -546,4 +696,4 @@ Match any IP with query response.
#### rules
Included rules.
Included rules.

View File

@@ -2,6 +2,20 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [match_response](#match_response)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -89,12 +103,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
],
@@ -124,6 +132,9 @@ icon: material/alert-decagram
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"user": [
"sekai"
],
@@ -149,6 +160,12 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -160,7 +177,17 @@ 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,
"ip_accept_any": false,
"response_rcode": "",
"response_answer": [],
"response_ns": [],
"response_extra": [],
"invert": false,
"outbound": [
"direct"
@@ -169,6 +196,8 @@ icon: material/alert-decagram
"server": "local",
// 已弃用
"rule_set_ip_cidr_accept_empty": false,
"rule_set_ipcidr_match_source": false,
"geosite": [
"cn"
@@ -208,7 +237,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
另外,引用规则集可视为被合并,而不是作为一个单独的规则子项
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
#### inbound
@@ -216,12 +245,38 @@ icon: material/alert-decagram
#### ip_version
!!! quote "sing-box 1.14.0 中的更改"
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
[`match_response`](#match_response)。
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
默认不限制。
#### query_type
!!! quote "sing-box 1.14.0 中的更改"
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
[`match_response`](#match_response)。
DNS 查询类型。值可以为整数或者类型名称字符串。
#### network
@@ -256,7 +311,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
匹配 Geosite。
@@ -264,7 +319,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
匹配源 GeoIP。
@@ -324,6 +379,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配 Android 应用包名。
#### package_name_regex
!!! question "自 sing-box 1.14.0 起"
使用正则表达式匹配 Android 应用包名。
#### user
!!! quote ""
@@ -407,6 +468,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配默认接口地址。
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### wifi_ssid
!!! quote ""
@@ -445,6 +526,23 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### match_response
!!! question "自 sing-box 1.14.0 起"
启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
响应匹配字段(`response_rcode``response_answer``response_ns``response_extra`)需要此选项。
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr``ip_is_private``ip_accept_any` 也需要此选项。
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
当 DNS 查询响应包含至少一个地址时匹配。
#### invert
反选匹配结果。
@@ -453,7 +551,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 废弃"
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。
匹配出站。
@@ -489,7 +587,12 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
已移动到 [DNS 规则动作](../rule_action#route).
### 地址筛选字段
### 旧版地址筛选字段
!!! failure "已在 sing-box 1.14.0 废弃"
旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
@@ -505,7 +608,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
与查询响应匹配 GeoIP。
@@ -516,24 +619,62 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
与查询响应匹配 IP CIDR。
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
#### ip_is_private
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配非公开 IP。
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
匹配任意 IP。
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
#### rule_set_ip_cidr_accept_empty
!!! question "自 sing-box 1.10.0 起"
!!! failure "已在 sing-box 1.14.0 废弃"
`rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
使规则集中的 `ip_cidr` 规则接受空查询响应。
### 响应匹配字段
!!! question "自 sing-box 1.14.0 起"
已评估的响应的匹配字段。需要将 `match_response` 设为 `true`
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
#### response_rcode
匹配 DNS 响应码。
接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。
#### response_answer
匹配 DNS 应答记录。
记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。
#### response_ns
匹配 DNS 名称服务器记录。
记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。
#### response_extra
匹配 DNS 额外记录。
记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。
### 逻辑字段
#### type
@@ -550,4 +691,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
==必填==
包括的规则。
包括的规则。

View File

@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [strategy](#strategy)
@@ -34,6 +40,10 @@ Tag of target server.
!!! question "Since sing-box 1.12.0"
!!! failure "Deprecated in sing-box 1.14.0"
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
Set domain strategy for this query.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
@@ -52,7 +62,68 @@ 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 evaluated response for subsequent rules
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
Unlike `route`, it does **not** terminate rule evaluation.
Only allowed on top-level DNS rules (not inside logical sub-rules).
Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields
require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action
does not satisfy this requirement, because matching happens before the action runs.
#### server
==Required==
Tag of target server.
#### disable_cache
Disable cache and save cache in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
#### client_subnet
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will override `dns.client_subnet`.
### respond
!!! question "Since sing-box 1.14.0"
```json
{
"action": "respond"
}
```
`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action.
This action does not send a new DNS query and has no extra options.
Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules.
### route-options

View File

@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [strategy](#strategy)
@@ -34,6 +40,10 @@ 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 中被移除。
为此查询设置域名策略。
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
@@ -54,6 +64,65 @@ 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 规则中使用(不可在逻辑子规则内部使用)。
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
因为匹配发生在动作执行之前。
#### server
==必填==
目标 DNS 服务器的标签。
#### disable_cache
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
#### client_subnet
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet`.
### respond
!!! question "自 sing-box 1.14.0 起"
```json
{
"action": "respond"
}
```
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
此动作不会发起新的 DNS 查询,也没有额外选项。
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
### route-options
```json
@@ -84,7 +153,7 @@ icon: material/new-box
- `default`: 返回 REFUSED。
- `drop`: 丢弃请求。
默认使用 `defualt`
默认使用 `default`
#### no_drop

View File

@@ -73,24 +73,55 @@ Example:
=== "Use hosts if available"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"action": "evaluate",
"server": "hosts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
]
}
]
}
}
```
}
```

View File

@@ -73,24 +73,55 @@ hosts 文件路径列表。
=== "如果可用则使用 hosts"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"action": "evaluate",
"server": "hosts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
]
}
]
}
}
```
}
```

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

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-server-formats).
!!! 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 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
!!! quote "sing-box 1.9.0 中的更改"

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -43,29 +43,62 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc
=== "Split DNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"action": "evaluate",
"server": "resolved"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
]
}
]
}
}
```
}
```
=== "Use as global DNS"

View File

@@ -42,29 +42,62 @@ icon: material/new-box
=== "仅分割 DNS"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"action": "evaluate",
"server": "resolved"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
]
}
]
}
}
```
}
```
=== "用作全局 DNS"

View File

@@ -42,29 +42,62 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
=== "MagicDNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"action": "evaluate",
"server": "ts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
]
}
]
}
}
```
}
```
=== "Use as global DNS"

View File

@@ -42,29 +42,62 @@ icon: material/new-box
=== "仅 MagicDNS"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"action": "evaluate",
"server": "ts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
]
}
]
}
}
```
}
```
=== "用作全局 DNS"

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### 拨号字段

View File

@@ -44,7 +44,7 @@ Store fakeip in the cache file
Store rejected DNS response cache in the cache file
The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
will be cached until expiration.
#### rdrc_timeout

View File

@@ -42,7 +42,7 @@
将拒绝的 DNS 响应缓存存储在缓存文件中。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
#### rdrc_timeout

View File

@@ -1,6 +1,6 @@
!!! quote ""
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#_5)。
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#构建标记)。
### 结构

View File

@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -0,0 +1,89 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
`cloudflared` inbound runs an embedded Cloudflare Tunnel client and routes all
incoming tunnel traffic (TCP, UDP, ICMP) through sing-box's routing engine.
### Structure
```json
{
"type": "cloudflared",
"tag": "",
"token": "",
"ha_connections": 0,
"protocol": "",
"post_quantum": false,
"edge_ip_version": 0,
"datagram_version": "",
"grace_period": "",
"region": "",
"control_dialer": {
... // Dial Fields
},
"tunnel_dialer": {
... // Dial Fields
}
}
```
### Fields
#### token
==Required==
Base64-encoded tunnel token from the Cloudflare Zero Trust dashboard
(`Networks → Tunnels → Install connector`).
#### ha_connections
Number of high-availability connections to the Cloudflare edge.
Capped by the number of discovered edge addresses.
#### protocol
Transport protocol for edge connections.
One of `quic` `http2`.
#### post_quantum
Enable post-quantum key exchange on the control connection.
#### edge_ip_version
IP version used when connecting to the Cloudflare edge.
One of `0` (automatic) `4` `6`.
#### datagram_version
Datagram protocol version used for UDP proxying over QUIC.
One of `v2` `v3`. Only meaningful when `protocol` is `quic`.
#### grace_period
Graceful shutdown window for in-flight edge connections.
#### region
Cloudflare edge region selector.
Conflict with endpoints embedded in `token`.
#### control_dialer
[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the
Cloudflare control plane.
#### tunnel_dialer
[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the
Cloudflare edge data plane.

View File

@@ -0,0 +1,89 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
`cloudflared` 入站运行一个内嵌的 Cloudflare Tunnel 客户端,并将所有传入的隧道流量
TCP、UDP、ICMP通过 sing-box 的路由引擎转发。
### 结构
```json
{
"type": "cloudflared",
"tag": "",
"token": "",
"ha_connections": 0,
"protocol": "",
"post_quantum": false,
"edge_ip_version": 0,
"datagram_version": "",
"grace_period": "",
"region": "",
"control_dialer": {
... // 拨号字段
},
"tunnel_dialer": {
... // 拨号字段
}
}
```
### 字段
#### token
==必填==
来自 Cloudflare Zero Trust 仪表板的 Base64 编码隧道令牌
`Networks → Tunnels → Install connector`)。
#### ha_connections
到 Cloudflare edge 的高可用连接数。
上限为已发现的 edge 地址数量。
#### protocol
edge 连接使用的传输协议。
`quic` `http2` 之一。
#### post_quantum
在控制连接上启用后量子密钥交换。
#### edge_ip_version
连接 Cloudflare edge 时使用的 IP 版本。
`0`(自动)`4` `6` 之一。
#### datagram_version
通过 QUIC 进行 UDP 代理时使用的数据报协议版本。
`v2` `v3` 之一。仅在 `protocol``quic` 时有效。
#### grace_period
正在处理的 edge 连接的优雅关闭窗口。
#### region
Cloudflare edge 区域选择器。
`token` 中嵌入的 endpoint 冲突。
#### control_dialer
隧道客户端拨向 Cloudflare 控制面时使用的
[拨号字段](/zh/configuration/shared/dial/)。
#### tunnel_dialer
隧道客户端拨向 Cloudflare edge 数据面时使用的
[拨号字段](/zh/configuration/shared/dial/)。

View File

@@ -26,7 +26,7 @@
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### users

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