mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-15 05:08:33 +10:00
Compare commits
84 Commits
renovate/m
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3c93c232 | ||
|
|
1efda8a87e | ||
|
|
1b653c6462 | ||
|
|
9708c5dd0b | ||
|
|
ce41cc48fa | ||
|
|
fea82a1e17 | ||
|
|
723cee38f2 | ||
|
|
ef12123c97 | ||
|
|
1e6fd30bdb | ||
|
|
ad7593a92f | ||
|
|
4da332723d | ||
|
|
8c1dc9c90d | ||
|
|
52831ca113 | ||
|
|
ffc30a26ce | ||
|
|
b1f6c6bfae | ||
|
|
a5e141f561 | ||
|
|
d6422fc95a | ||
|
|
9cbbc9ab30 | ||
|
|
3588996ca9 | ||
|
|
00c73ef944 | ||
|
|
7405cadc0b | ||
|
|
6ff8ce7823 | ||
|
|
d4eccc17f4 | ||
|
|
ef1df15ebb | ||
|
|
85082250cb | ||
|
|
0d56394096 | ||
|
|
bc0c3edeaf | ||
|
|
cedada34ff | ||
|
|
ae6964fc8f | ||
|
|
582e714f04 | ||
|
|
ce04cecd46 | ||
|
|
d04f3f5adb | ||
|
|
527182b996 | ||
|
|
d5adb54bc6 | ||
|
|
1cfcea769f | ||
|
|
f43fc797d4 | ||
|
|
8e3176b789 | ||
|
|
025b947a24 | ||
|
|
76fa3c2e5e | ||
|
|
53db1f178c | ||
|
|
55ec8abf17 | ||
|
|
5a957fd750 | ||
|
|
7c3d8cf8db | ||
|
|
813b634d08 | ||
|
|
d9b435fb62 | ||
|
|
354b4b040e | ||
|
|
7ffdc48b49 | ||
|
|
e15bdf11eb | ||
|
|
e3bcb06c3e | ||
|
|
84d2280960 | ||
|
|
4fd2532b0a | ||
|
|
02ccde6c71 | ||
|
|
e98b4ad449 | ||
|
|
d09182614c | ||
|
|
6381de7bab | ||
|
|
b0c6762bc1 | ||
|
|
7425100bac | ||
|
|
d454aa0fdf | ||
|
|
a3623eb41a | ||
|
|
72bc4c1f87 | ||
|
|
9ac1e2ff32 | ||
|
|
0045103d14 | ||
|
|
d2a933784c | ||
|
|
3f05a37f65 | ||
|
|
b8e5a71450 | ||
|
|
c13faa8e3c | ||
|
|
7623bcd19e | ||
|
|
795d1c2892 | ||
|
|
6913b11e0a | ||
|
|
1e57c06295 | ||
|
|
ea464cef8d | ||
|
|
a8e3cd3256 | ||
|
|
686cf1f304 | ||
|
|
9fbfb87723 | ||
|
|
d2fa21d07b | ||
|
|
d3768cca36 | ||
|
|
0889ddd001 | ||
|
|
f46fbf188a | ||
|
|
f2d15139f5 | ||
|
|
041646b728 | ||
|
|
b990de2e12 | ||
|
|
fe585157d2 | ||
|
|
eed6a36e5d | ||
|
|
eb0f38544c |
@@ -4,6 +4,7 @@
|
|||||||
--license GPL-3.0-or-later
|
--license GPL-3.0-or-later
|
||||||
--description "The universal proxy platform."
|
--description "The universal proxy platform."
|
||||||
--url "https://sing-box.sagernet.org/"
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--vendor SagerNet
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
--no-deb-generate-changes
|
--no-deb-generate-changes
|
||||||
|
|||||||
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
|||||||
2fef65f9dba90ddb89a87d00a6eb6165487c10c1
|
e4926ba205fae5351e3d3eeafff7e7029654424a
|
||||||
|
|||||||
33
.github/detect_track.sh
vendored
Executable file
33
.github/detect_track.sh
vendored
Executable 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"
|
||||||
2
.github/setup_go_for_macos1013.sh
vendored
2
.github/setup_go_for_macos1013.sh
vendored
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="1.25.8"
|
VERSION="1.25.9"
|
||||||
PATCH_COMMITS=(
|
PATCH_COMMITS=(
|
||||||
"afe69d3cec1c6dcf0f1797b20546795730850070"
|
"afe69d3cec1c6dcf0f1797b20546795730850070"
|
||||||
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
|
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
|
||||||
|
|||||||
2
.github/setup_go_for_windows7.sh
vendored
2
.github/setup_go_for_windows7.sh
vendored
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="1.25.8"
|
VERSION="1.25.9"
|
||||||
PATCH_COMMITS=(
|
PATCH_COMMITS=(
|
||||||
"466f6c7a29bc098b0d4c987b803c779222894a11"
|
"466f6c7a29bc098b0d4c987b803c779222894a11"
|
||||||
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
|
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
|
||||||
|
|||||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -124,7 +124,7 @@ jobs:
|
|||||||
if: ${{ ! matrix.legacy_win7 }}
|
if: ${{ ! matrix.legacy_win7 }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Cache Go for Windows 7
|
- name: Cache Go for Windows 7
|
||||||
if: matrix.legacy_win7
|
if: matrix.legacy_win7
|
||||||
id: cache-go-for-windows7
|
id: cache-go-for-windows7
|
||||||
@@ -641,7 +641,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -731,7 +731,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -830,7 +830,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
|
|||||||
21
.github/workflows/docker.yml
vendored
21
.github/workflows/docker.yml
vendored
@@ -19,7 +19,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build_binary:
|
build_binary:
|
||||||
name: Build binary
|
name: Build binary
|
||||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
@@ -56,7 +55,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Clone cronet-go
|
- name: Clone cronet-go
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
@@ -260,13 +259,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "ref=$ref"
|
echo "ref=$ref"
|
||||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
if [[ $ref == *"-"* ]]; then
|
- name: Checkout
|
||||||
latest=latest-beta
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
else
|
with:
|
||||||
latest=latest
|
ref: ${{ steps.ref.outputs.ref }}
|
||||||
fi
|
fetch-depth: 0
|
||||||
echo "latest=$latest"
|
- name: Detect track
|
||||||
echo "latest=$latest" >> $GITHUB_OUTPUT
|
run: bash .github/detect_track.sh
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
@@ -286,11 +285,11 @@ jobs:
|
|||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
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 }}" \
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
if: github.event_name != 'push'
|
if: github.event_name != 'push'
|
||||||
run: |
|
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 }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||||
|
|||||||
20
.github/workflows/linux.yml
vendored
20
.github/workflows/linux.yml
vendored
@@ -11,11 +11,6 @@ on:
|
|||||||
description: "Version name"
|
description: "Version name"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
forceBeta:
|
|
||||||
description: "Force beta"
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
@@ -23,7 +18,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
calculate_version:
|
calculate_version:
|
||||||
name: Calculate version
|
name: Calculate version
|
||||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
@@ -35,7 +29,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -78,7 +72,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ~1.25.9
|
||||||
- name: Clone cronet-go
|
- name: Clone cronet-go
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
@@ -168,14 +162,8 @@ jobs:
|
|||||||
- name: Set mtime
|
- name: Set mtime
|
||||||
run: |-
|
run: |-
|
||||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||||
- name: Set name
|
- name: Detect track
|
||||||
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
run: bash .github/detect_track.sh
|
||||||
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: Set version
|
- name: Set version
|
||||||
run: |-
|
run: |-
|
||||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
|||||||
21
adapter/certificate/adapter.go
Normal file
21
adapter/certificate/adapter.go
Normal 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
|
||||||
|
}
|
||||||
158
adapter/certificate/manager.go
Normal file
158
adapter/certificate/manager.go
Normal 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
|
||||||
|
}
|
||||||
72
adapter/certificate/registry.go
Normal file
72
adapter/certificate/registry.go
Normal 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
|
||||||
|
}
|
||||||
38
adapter/certificate_provider.go
Normal file
38
adapter/certificate_provider.go
Normal 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
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -25,18 +26,19 @@ type DNSRouter interface {
|
|||||||
|
|
||||||
type DNSClient interface {
|
type DNSClient interface {
|
||||||
Start()
|
Start()
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, 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(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
||||||
ClearCache()
|
ClearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSQueryOptions struct {
|
type DNSQueryOptions struct {
|
||||||
Transport DNSTransport
|
Transport DNSTransport
|
||||||
Strategy C.DomainStrategy
|
Strategy C.DomainStrategy
|
||||||
LookupStrategy C.DomainStrategy
|
LookupStrategy C.DomainStrategy
|
||||||
DisableCache bool
|
DisableCache bool
|
||||||
RewriteTTL *uint32
|
DisableOptimisticCache bool
|
||||||
ClientSubnet netip.Prefix
|
RewriteTTL *uint32
|
||||||
|
ClientSubnet netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||||
@@ -49,11 +51,12 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
|
|||||||
return nil, E.New("domain resolver not found: " + options.Server)
|
return nil, E.New("domain resolver not found: " + options.Server)
|
||||||
}
|
}
|
||||||
return &DNSQueryOptions{
|
return &DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: C.DomainStrategy(options.Strategy),
|
Strategy: C.DomainStrategy(options.Strategy),
|
||||||
DisableCache: options.DisableCache,
|
DisableCache: options.DisableCache,
|
||||||
RewriteTTL: options.RewriteTTL,
|
DisableOptimisticCache: options.DisableOptimisticCache,
|
||||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
RewriteTTL: options.RewriteTTL,
|
||||||
|
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +66,13 @@ type RDRCStore interface {
|
|||||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DNSCacheStore interface {
|
||||||
|
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
||||||
|
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
||||||
|
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
||||||
|
ClearDNSCache() error
|
||||||
|
}
|
||||||
|
|
||||||
type DNSTransport interface {
|
type DNSTransport interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
Type() string
|
Type() string
|
||||||
@@ -72,11 +82,6 @@ type DNSTransport interface {
|
|||||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyDNSTransport interface {
|
|
||||||
LegacyStrategy() C.DomainStrategy
|
|
||||||
LegacyClientSubnet() netip.Prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSTransportRegistry interface {
|
type DNSTransportRegistry interface {
|
||||||
option.DNSTransportOptionsRegistry
|
option.DNSTransportOptionsRegistry
|
||||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ type CacheFile interface {
|
|||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
RDRCStore
|
RDRCStore
|
||||||
|
|
||||||
|
StoreDNS() bool
|
||||||
|
DNSCacheStore
|
||||||
|
|
||||||
|
SetDisableExpire(disableExpire bool)
|
||||||
|
SetOptimisticTimeout(timeout time.Duration)
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -9,6 +10,8 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Inbound interface {
|
type Inbound interface {
|
||||||
@@ -78,12 +81,16 @@ type InboundContext struct {
|
|||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
DestinationAddresses []netip.Addr
|
DestinationAddresses []netip.Addr
|
||||||
SourceGeoIPCode string
|
DNSResponse *dns.Msg
|
||||||
GeoIPCode string
|
DestinationAddressMatchFromResponse bool
|
||||||
ProcessInfo *ConnectionOwner
|
SourceGeoIPCode string
|
||||||
QueryType uint16
|
GeoIPCode string
|
||||||
FakeIP bool
|
ProcessInfo *ConnectionOwner
|
||||||
|
SourceMACAddress net.HardwareAddr
|
||||||
|
SourceHostname string
|
||||||
|
QueryType uint16
|
||||||
|
FakeIP bool
|
||||||
|
|
||||||
// rule cache
|
// rule cache
|
||||||
|
|
||||||
@@ -101,6 +108,10 @@ type InboundContext struct {
|
|||||||
func (c *InboundContext) ResetRuleCache() {
|
func (c *InboundContext) ResetRuleCache() {
|
||||||
c.IPCIDRMatchSource = false
|
c.IPCIDRMatchSource = false
|
||||||
c.IPCIDRAcceptEmpty = false
|
c.IPCIDRAcceptEmpty = false
|
||||||
|
c.ResetRuleMatchCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InboundContext) ResetRuleMatchCache() {
|
||||||
c.SourceAddressMatch = false
|
c.SourceAddressMatch = false
|
||||||
c.SourcePortMatch = false
|
c.SourcePortMatch = false
|
||||||
c.DestinationAddressMatch = false
|
c.DestinationAddressMatch = false
|
||||||
@@ -108,6 +119,51 @@ func (c *InboundContext) ResetRuleCache() {
|
|||||||
c.DidMatch = false
|
c.DidMatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
||||||
|
return DNSResponseAddresses(c.DNSResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
||||||
|
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||||
|
for _, rawRecord := range response.Answer {
|
||||||
|
switch record := rawRecord.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
addr := M.AddrFromIP(record.A)
|
||||||
|
if addr.IsValid() {
|
||||||
|
addresses = append(addresses, addr)
|
||||||
|
}
|
||||||
|
case *dns.AAAA:
|
||||||
|
addr := M.AddrFromIP(record.AAAA)
|
||||||
|
if addr.IsValid() {
|
||||||
|
addresses = append(addresses, addr)
|
||||||
|
}
|
||||||
|
case *dns.HTTPS:
|
||||||
|
for _, value := range record.SVCB.Value {
|
||||||
|
switch hint := value.(type) {
|
||||||
|
case *dns.SVCBIPv4Hint:
|
||||||
|
for _, ip := range hint.Hint {
|
||||||
|
addr := M.AddrFromIP(ip).Unmap()
|
||||||
|
if addr.IsValid() {
|
||||||
|
addresses = append(addresses, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *dns.SVCBIPv6Hint:
|
||||||
|
for _, ip := range hint.Hint {
|
||||||
|
addr := M.AddrFromIP(ip)
|
||||||
|
if addr.IsValid() {
|
||||||
|
addresses = append(addresses, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
|
|
||||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||||
|
|||||||
45
adapter/inbound_test.go
Normal file
45
adapter/inbound_test.go
Normal 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
23
adapter/neighbor.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -36,6 +36,10 @@ type PlatformInterface interface {
|
|||||||
|
|
||||||
UsePlatformNotification() bool
|
UsePlatformNotification() bool
|
||||||
SendNotification(notification *Notification) error
|
SendNotification(notification *Notification) error
|
||||||
|
|
||||||
|
UsePlatformNeighborResolver() bool
|
||||||
|
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
|
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindConnectionOwnerRequest struct {
|
type FindConnectionOwnerRequest struct {
|
||||||
@@ -47,11 +51,11 @@ type FindConnectionOwnerRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionOwner struct {
|
type ConnectionOwner struct {
|
||||||
ProcessID uint32
|
ProcessID uint32
|
||||||
UserId int32
|
UserId int32
|
||||||
UserName string
|
UserName string
|
||||||
ProcessPath string
|
ProcessPath string
|
||||||
AndroidPackageName string
|
AndroidPackageNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type Router interface {
|
|||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
NeedFindProcess() bool
|
NeedFindProcess() bool
|
||||||
|
NeedFindNeighbor() bool
|
||||||
|
NeighborResolver() NeighborResolver
|
||||||
AppendTracker(tracker ConnectionTracker)
|
AppendTracker(tracker ConnectionTracker)
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
@@ -64,10 +66,16 @@ type RuleSet interface {
|
|||||||
|
|
||||||
type RuleSetUpdateCallback func(it RuleSet)
|
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 {
|
type RuleSetMetadata struct {
|
||||||
ContainsProcessRule bool
|
ContainsProcessRule bool
|
||||||
ContainsWIFIRule bool
|
ContainsWIFIRule bool
|
||||||
ContainsIPCIDRRule bool
|
ContainsIPCIDRRule bool
|
||||||
|
ContainsDNSQueryTypeRule bool
|
||||||
}
|
}
|
||||||
type HTTPStartContext struct {
|
type HTTPStartContext struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HeadlessRule interface {
|
type HeadlessRule interface {
|
||||||
@@ -18,8 +20,9 @@ type Rule interface {
|
|||||||
|
|
||||||
type DNSRule interface {
|
type DNSRule interface {
|
||||||
Rule
|
Rule
|
||||||
|
LegacyPreMatch(metadata *InboundContext) bool
|
||||||
WithAddressLimit() bool
|
WithAddressLimit() bool
|
||||||
MatchAddressLimit(metadata *InboundContext) bool
|
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleAction interface {
|
type RuleAction interface {
|
||||||
@@ -29,7 +32,7 @@ type RuleAction interface {
|
|||||||
|
|
||||||
func IsFinalAction(action RuleAction) bool {
|
func IsFinalAction(action RuleAction) bool {
|
||||||
switch action.Type() {
|
switch action.Type() {
|
||||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
|
|||||||
49
adapter/tailscale.go
Normal file
49
adapter/tailscale.go
Normal 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
|
||||||
|
}
|
||||||
143
box.go
143
box.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"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/endpoint"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
@@ -19,7 +20,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"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"
|
||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -37,20 +37,21 @@ import (
|
|||||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
network *route.NetworkManager
|
network *route.NetworkManager
|
||||||
endpoint *endpoint.Manager
|
endpoint *endpoint.Manager
|
||||||
inbound *inbound.Manager
|
inbound *inbound.Manager
|
||||||
outbound *outbound.Manager
|
outbound *outbound.Manager
|
||||||
service *boxService.Manager
|
service *boxService.Manager
|
||||||
dnsTransport *dns.TransportManager
|
certificateProvider *boxCertificate.Manager
|
||||||
dnsRouter *dns.Router
|
dnsTransport *dns.TransportManager
|
||||||
connection *route.ConnectionManager
|
dnsRouter *dns.Router
|
||||||
router *route.Router
|
connection *route.ConnectionManager
|
||||||
internalService []adapter.LifecycleService
|
router *route.Router
|
||||||
done chan struct{}
|
internalService []adapter.LifecycleService
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -66,6 +67,7 @@ func Context(
|
|||||||
endpointRegistry adapter.EndpointRegistry,
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
serviceRegistry adapter.ServiceRegistry,
|
serviceRegistry adapter.ServiceRegistry,
|
||||||
|
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
||||||
) context.Context {
|
) context.Context {
|
||||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||||
@@ -90,6 +92,10 @@ func Context(
|
|||||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||||
ctx = service.ContextWith[adapter.ServiceRegistry](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
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +112,7 @@ func New(options Options) (*Box, error) {
|
|||||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||||
|
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
||||||
|
|
||||||
if endpointRegistry == nil {
|
if endpointRegistry == nil {
|
||||||
return nil, E.New("missing endpoint registry in context")
|
return nil, E.New("missing endpoint registry in context")
|
||||||
@@ -122,6 +129,9 @@ func New(options Options) (*Box, error) {
|
|||||||
if serviceRegistry == nil {
|
if serviceRegistry == nil {
|
||||||
return nil, E.New("missing service registry in context")
|
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)
|
ctx = pause.WithDefaultManager(ctx)
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
@@ -179,13 +189,19 @@ func New(options Options) (*Box, error) {
|
|||||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||||
|
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
||||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||||
|
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize DNS router")
|
||||||
|
}
|
||||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
|
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
@@ -272,6 +288,24 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
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 {
|
for i, outboundOptions := range options.Outbounds {
|
||||||
var tag string
|
var tag string
|
||||||
if outboundOptions.Tag != "" {
|
if outboundOptions.Tag != "" {
|
||||||
@@ -298,22 +332,22 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, serviceOptions := range options.Services {
|
for i, certificateProviderOptions := range options.CertificateProviders {
|
||||||
var tag string
|
var tag string
|
||||||
if serviceOptions.Tag != "" {
|
if certificateProviderOptions.Tag != "" {
|
||||||
tag = serviceOptions.Tag
|
tag = certificateProviderOptions.Tag
|
||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
err = serviceManager.Create(
|
err = certificateProviderManager.Create(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
serviceOptions.Type,
|
certificateProviderOptions.Type,
|
||||||
serviceOptions.Options,
|
certificateProviderOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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) {
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
@@ -326,11 +360,12 @@ func New(options Options) (*Box, error) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
return local.NewTransport(
|
return dnsTransportRegistry.CreateDNSTransport(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger("dns/local"),
|
logFactory.NewLogger("dns/local"),
|
||||||
"local",
|
"local",
|
||||||
option.LocalDNSServerOptions{},
|
C.DNSTypeLocal,
|
||||||
|
&option.LocalDNSServerOptions{},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
@@ -340,7 +375,7 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
internalServices = append(internalServices, cacheFile)
|
internalServices = append(internalServices, cacheFile)
|
||||||
}
|
}
|
||||||
@@ -383,20 +418,21 @@ func New(options Options) (*Box, error) {
|
|||||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
inbound: inboundManager,
|
inbound: inboundManager,
|
||||||
outbound: outboundManager,
|
outbound: outboundManager,
|
||||||
dnsTransport: dnsTransportManager,
|
dnsTransport: dnsTransportManager,
|
||||||
service: serviceManager,
|
service: serviceManager,
|
||||||
dnsRouter: dnsRouter,
|
certificateProvider: certificateProviderManager,
|
||||||
connection: connectionManager,
|
dnsRouter: dnsRouter,
|
||||||
router: router,
|
connection: connectionManager,
|
||||||
createdAt: createdAt,
|
router: router,
|
||||||
logFactory: logFactory,
|
createdAt: createdAt,
|
||||||
logger: logFactory.Logger(),
|
logFactory: logFactory,
|
||||||
internalService: internalServices,
|
logger: logFactory.Logger(),
|
||||||
done: make(chan struct{}),
|
internalService: internalServices,
|
||||||
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,11 +486,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -470,11 +506,19 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -482,7 +526,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -506,8 +550,9 @@ func (s *Box) Close() error {
|
|||||||
service adapter.Lifecycle
|
service adapter.Lifecycle
|
||||||
}{
|
}{
|
||||||
{"service", s.service},
|
{"service", s.service},
|
||||||
{"endpoint", s.endpoint},
|
|
||||||
{"inbound", s.inbound},
|
{"inbound", s.inbound},
|
||||||
|
{"certificate-provider", s.certificateProvider},
|
||||||
|
{"endpoint", s.endpoint},
|
||||||
{"outbound", s.outbound},
|
{"outbound", s.outbound},
|
||||||
{"router", s.router},
|
{"router", s.router},
|
||||||
{"connection", s.connection},
|
{"connection", s.connection},
|
||||||
@@ -555,6 +600,10 @@ func (s *Box) Outbound() adapter.OutboundManager {
|
|||||||
return s.outbound
|
return s.outbound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||||
|
return s.endpoint
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Box) LogFactory() log.Factory {
|
func (s *Box) LogFactory() log.Factory {
|
||||||
return s.logFactory
|
return s.logFactory
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule clients/android updated: 7777469b5d...ab09918615
Submodule clients/apple updated: c19945f65b...ad7434d676
@@ -82,6 +82,11 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
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 {
|
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||||
len(rule.DefaultInterfaceAddress) > 0
|
len(rule.DefaultInterfaceAddress) > 0
|
||||||
|
|||||||
121
cmd/sing-box/cmd_tools_networkquality.go
Normal file
121
cmd/sing-box/cmd_tools_networkquality.go
Normal 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
|
||||||
|
}
|
||||||
79
cmd/sing-box/cmd_tools_stun.go
Normal file
79
cmd/sing-box/cmd_tools_stun.go
Normal 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
|
||||||
|
}
|
||||||
@@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPConnectTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
if !options.DisableTCPKeepAlive {
|
if options.DisableTCPKeepAlive {
|
||||||
|
dialer.KeepAlive = -1
|
||||||
|
dialer.KeepAliveConfig.Enable = false
|
||||||
|
} else {
|
||||||
keepIdle := time.Duration(options.TCPKeepAlive)
|
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||||
if keepIdle == 0 {
|
if keepIdle == 0 {
|
||||||
keepIdle = C.TCPKeepAliveInitial
|
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) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
} else if address.IsFqdn() {
|
} else if address.IsDomain() {
|
||||||
return nil, E.New("domain not resolved")
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
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 {
|
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
|
||||||
if !destination.Is6() {
|
if !destination.Is6() {
|
||||||
return d.dialer6.Dialer
|
|
||||||
} else {
|
|
||||||
return d.dialer4.Dialer
|
return d.dialer4.Dialer
|
||||||
|
} else {
|
||||||
|
return d.dialer6.Dialer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,11 +87,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
server = dialOptions.DomainResolver.Server
|
server = dialOptions.DomainResolver.Server
|
||||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
||||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||||
|
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||||
}
|
}
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
} else if options.DirectResolver {
|
} else if options.DirectResolver {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Conn) Read(b []byte) (int, error) {
|
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)
|
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
|
||||||
data, typ, err = c.rawConn.In.Decrypt(record)
|
data, typ, err = c.rawConn.In.Decrypt(record)
|
||||||
if err != nil {
|
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
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
|||||||
if l.listenOptions.ReuseAddr {
|
if l.listenOptions.ReuseAddr {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, control.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)
|
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||||
if keepIdle == 0 {
|
if keepIdle == 0 {
|
||||||
keepIdle = C.TCPKeepAliveInitial
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
|
|||||||
142
common/networkquality/http.go
Normal file
142
common/networkquality/http.go
Normal 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)
|
||||||
|
}
|
||||||
55
common/networkquality/http3.go
Normal file
55
common/networkquality/http3.go
Normal 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
|
||||||
|
}
|
||||||
12
common/networkquality/http3_stub.go
Normal file
12
common/networkquality/http3_stub.go
Normal 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
|
||||||
|
}
|
||||||
1413
common/networkquality/networkquality.go
Normal file
1413
common/networkquality/networkquality.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type Searcher interface {
|
type Searcher interface {
|
||||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNotFound = E.New("process not found")
|
var ErrNotFound = E.New("process not found")
|
||||||
@@ -28,7 +29,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.UserId != -1 {
|
if info.UserId != -1 && info.UserName == "" {
|
||||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||||
if osUser != nil {
|
if osUser != nil {
|
||||||
info.UserName = osUser.Username
|
info.UserName = osUser.Username
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*androidSearcher)(nil)
|
var _ Searcher = (*androidSearcher)(nil)
|
||||||
@@ -18,22 +19,30 @@ func NewSearcher(config Config) (Searcher, error) {
|
|||||||
return &androidSearcher{config.PackageManager}, nil
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
_, uid, err := querySocketDiagOnce(family, protocol, source)
|
||||||
return &adapter.ConnectionOwner{
|
if err != nil {
|
||||||
UserId: int32(uid),
|
return nil, err
|
||||||
AndroidPackageName: sharedPackage,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
appID := uid % 100000
|
||||||
return &adapter.ConnectionOwner{
|
var packageNames []string
|
||||||
UserId: int32(uid),
|
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
|
||||||
AndroidPackageName: packageName,
|
packageNames = append(packageNames, sharedPackage)
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*darwinSearcher)(nil)
|
var _ Searcher = (*darwinSearcher)(nil)
|
||||||
@@ -24,12 +20,12 @@ func NewSearcher(_ Config) (Searcher, error) {
|
|||||||
return &darwinSearcher{}, nil
|
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) {
|
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()))
|
return FindDarwinConnectionOwner(network, source, destination)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var structSize = func() int {
|
var structSize = func() int {
|
||||||
@@ -47,107 +43,3 @@ var structSize = func() int {
|
|||||||
return 384
|
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]))
|
|
||||||
}
|
|
||||||
|
|||||||
269
common/process/searcher_darwin_shared.go
Normal file
269
common/process/searcher_darwin_shared.go
Normal 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
|
||||||
|
}
|
||||||
@@ -4,33 +4,82 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*linuxSearcher)(nil)
|
var _ Searcher = (*linuxSearcher)(nil)
|
||||||
|
|
||||||
type linuxSearcher struct {
|
type linuxSearcher struct {
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
|
diagConns [4]*socketDiagConn
|
||||||
|
processPathCache *uidProcessPathCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSearcher(config Config) (Searcher, error) {
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
processPath, err := resolveProcessNameByProcSearch(inode, uid)
|
processInfo := &adapter.ConnectionOwner{
|
||||||
|
UserId: int32(uid),
|
||||||
|
}
|
||||||
|
processPath, err := s.processPathCache.findProcessPath(inode, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||||
|
} else {
|
||||||
|
processInfo.ProcessPath = processPath
|
||||||
}
|
}
|
||||||
return &adapter.ConnectionOwner{
|
return processInfo, nil
|
||||||
UserId: int32(uid),
|
}
|
||||||
ProcessPath: processPath,
|
|
||||||
}, 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,43 +3,67 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"errors"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
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 (
|
const (
|
||||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
sizeOfSocketDiagRequestData = 56
|
||||||
socketDiagByFamily = 20
|
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||||
pathProc = "/proc"
|
socketDiagResponseMinSize = 72
|
||||||
|
socketDiagByFamily = 20
|
||||||
|
pathProc = "/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
type socketDiagConn struct {
|
||||||
var family uint8
|
access sync.Mutex
|
||||||
var protocol uint8
|
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 {
|
switch network {
|
||||||
case N.NetworkTCP:
|
case N.NetworkTCP:
|
||||||
protocol = syscall.IPPROTO_TCP
|
protocol = syscall.IPPROTO_TCP
|
||||||
@@ -48,151 +72,308 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
|||||||
default:
|
default:
|
||||||
return 0, 0, os.ErrInvalid
|
return 0, 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
switch {
|
||||||
if source.Addr().Is4() {
|
case source.Addr().Is4():
|
||||||
family = syscall.AF_INET
|
family = syscall.AF_INET
|
||||||
} else {
|
case source.Addr().Is6():
|
||||||
family = syscall.AF_INET6
|
family = syscall.AF_INET6
|
||||||
|
default:
|
||||||
|
return 0, 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
return family, protocol, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
|
||||||
s := make([]byte, 16)
|
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
|
||||||
copy(s, source.Addr().AsSlice())
|
cache.SetLifetime(ttl)
|
||||||
|
return &uidProcessPathCache{cache: cache}
|
||||||
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 unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
|
||||||
if len(msg.Data) < 72 {
|
if cached, ok := c.cache.Get(uid); ok {
|
||||||
return 0, 0
|
if processPath, found := cached.entries[targetInode]; found {
|
||||||
|
return processPath, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
processPaths, err := buildProcessPathByUIDCache(uid)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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)
|
buffer := make([]byte, syscall.PathMax)
|
||||||
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
processPaths := make(map[uint32]string)
|
||||||
|
for _, file := range files {
|
||||||
for _, f := range files {
|
if !file.IsDir() || !isPid(file.Name()) {
|
||||||
if !f.IsDir() || !isPid(f.Name()) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
info, err := file.Info()
|
||||||
info, err := f.Info()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
if isIgnorableProcError(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
processPath := filepath.Join(pathProc, file.Name())
|
||||||
processPath := path.Join(pathProc, f.Name())
|
fdPath := filepath.Join(processPath, "fd")
|
||||||
fdPath := path.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)
|
fds, err := os.ReadDir(fdPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fd := range fds {
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
inode, ok := parseSocketInode(buffer[:n])
|
||||||
if bytes.Equal(buffer[:n], socket) {
|
if !ok {
|
||||||
return os.Readlink(path.Join(processPath, "exe"))
|
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 {
|
func isPid(s string) bool {
|
||||||
|
|||||||
60
common/process/searcher_linux_shared_test.go
Normal file
60
common/process/searcher_linux_shared_test.go
Normal 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))
|
||||||
|
}
|
||||||
@@ -28,6 +28,10 @@ func initWin32API() error {
|
|||||||
return winiphlpapi.LoadExtendedTable()
|
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) {
|
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
pid, err := winiphlpapi.FindPid(network, source)
|
pid, err := winiphlpapi.FindPid(network, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const (
|
|||||||
ruleItemNetworkIsConstrained
|
ruleItemNetworkIsConstrained
|
||||||
ruleItemNetworkInterfaceAddress
|
ruleItemNetworkInterfaceAddress
|
||||||
ruleItemDefaultInterfaceAddress
|
ruleItemDefaultInterfaceAddress
|
||||||
|
ruleItemPackageNameRegex
|
||||||
ruleItemFinal uint8 = 0xFF
|
ruleItemFinal uint8 = 0xFF
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -215,6 +216,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
|||||||
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
||||||
case ruleItemPackageName:
|
case ruleItemPackageName:
|
||||||
rule.PackageName, err = readRuleItemString(reader)
|
rule.PackageName, err = readRuleItemString(reader)
|
||||||
|
case ruleItemPackageNameRegex:
|
||||||
|
rule.PackageNameRegex, err = readRuleItemString(reader)
|
||||||
case ruleItemWIFISSID:
|
case ruleItemWIFISSID:
|
||||||
rule.WIFISSID, err = readRuleItemString(reader)
|
rule.WIFISSID, err = readRuleItemString(reader)
|
||||||
case ruleItemWIFIBSSID:
|
case ruleItemWIFIBSSID:
|
||||||
@@ -394,6 +397,15 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
|||||||
return err
|
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 len(rule.NetworkType) > 0 {
|
||||||
if generateVersion < C.RuleSetVersion3 {
|
if generateVersion < C.RuleSetVersion3 {
|
||||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||||
|
|||||||
612
common/stun/stun.go
Normal file
612
common/stun/stun.go
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
package stun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
"github.com/sagernet/sing/common/bufio/deadline"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultServer = "stun.voipgate.com:3478"
|
||||||
|
|
||||||
|
magicCookie = 0x2112A442
|
||||||
|
headerSize = 20
|
||||||
|
|
||||||
|
bindingRequest = 0x0001
|
||||||
|
bindingSuccessResponse = 0x0101
|
||||||
|
bindingErrorResponse = 0x0111
|
||||||
|
|
||||||
|
attrMappedAddress = 0x0001
|
||||||
|
attrChangeRequest = 0x0003
|
||||||
|
attrErrorCode = 0x0009
|
||||||
|
attrXORMappedAddress = 0x0020
|
||||||
|
attrOtherAddress = 0x802c
|
||||||
|
|
||||||
|
familyIPv4 = 0x01
|
||||||
|
familyIPv6 = 0x02
|
||||||
|
|
||||||
|
changeIP = 0x04
|
||||||
|
changePort = 0x02
|
||||||
|
|
||||||
|
defaultRTO = 500 * time.Millisecond
|
||||||
|
minRTO = 250 * time.Millisecond
|
||||||
|
maxRetransmit = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Phase int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
PhaseBinding Phase = iota
|
||||||
|
PhaseNATMapping
|
||||||
|
PhaseNATFiltering
|
||||||
|
PhaseDone
|
||||||
|
)
|
||||||
|
|
||||||
|
type NATMapping int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
NATMappingUnknown NATMapping = iota
|
||||||
|
_ // reserved
|
||||||
|
NATMappingEndpointIndependent
|
||||||
|
NATMappingAddressDependent
|
||||||
|
NATMappingAddressAndPortDependent
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m NATMapping) String() string {
|
||||||
|
switch m {
|
||||||
|
case NATMappingEndpointIndependent:
|
||||||
|
return "Endpoint Independent"
|
||||||
|
case NATMappingAddressDependent:
|
||||||
|
return "Address Dependent"
|
||||||
|
case NATMappingAddressAndPortDependent:
|
||||||
|
return "Address and Port Dependent"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NATFiltering int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
NATFilteringUnknown NATFiltering = iota
|
||||||
|
NATFilteringEndpointIndependent
|
||||||
|
NATFilteringAddressDependent
|
||||||
|
NATFilteringAddressAndPortDependent
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f NATFiltering) String() string {
|
||||||
|
switch f {
|
||||||
|
case NATFilteringEndpointIndependent:
|
||||||
|
return "Endpoint Independent"
|
||||||
|
case NATFilteringAddressDependent:
|
||||||
|
return "Address Dependent"
|
||||||
|
case NATFilteringAddressAndPortDependent:
|
||||||
|
return "Address and Port Dependent"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionID [12]byte
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Server string
|
||||||
|
Dialer N.Dialer
|
||||||
|
Context context.Context
|
||||||
|
OnProgress func(Progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Progress struct {
|
||||||
|
Phase Phase
|
||||||
|
ExternalAddr string
|
||||||
|
LatencyMs int32
|
||||||
|
NATMapping NATMapping
|
||||||
|
NATFiltering NATFiltering
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
ExternalAddr string
|
||||||
|
LatencyMs int32
|
||||||
|
NATMapping NATMapping
|
||||||
|
NATFiltering NATFiltering
|
||||||
|
NATTypeSupported bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type parsedResponse struct {
|
||||||
|
xorMappedAddr netip.AddrPort
|
||||||
|
mappedAddr netip.AddrPort
|
||||||
|
otherAddr netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
|
||||||
|
if r.xorMappedAddr.IsValid() {
|
||||||
|
return r.xorMappedAddr, true
|
||||||
|
}
|
||||||
|
if r.mappedAddr.IsValid() {
|
||||||
|
return r.mappedAddr, true
|
||||||
|
}
|
||||||
|
return netip.AddrPort{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type stunAttribute struct {
|
||||||
|
typ uint16
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTransactionID() TransactionID {
|
||||||
|
var id TransactionID
|
||||||
|
_, _ = rand.Read(id[:])
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
|
||||||
|
attrLen := 0
|
||||||
|
for _, attr := range attrs {
|
||||||
|
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, headerSize+attrLen)
|
||||||
|
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
|
||||||
|
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
|
||||||
|
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
|
||||||
|
copy(buf[8:20], txID[:])
|
||||||
|
|
||||||
|
offset := headerSize
|
||||||
|
for _, attr := range attrs {
|
||||||
|
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
|
||||||
|
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
|
||||||
|
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
|
||||||
|
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeRequestAttr(flags byte) stunAttribute {
|
||||||
|
return stunAttribute{
|
||||||
|
typ: attrChangeRequest,
|
||||||
|
value: []byte{0, 0, 0, flags},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
|
||||||
|
if len(data) < headerSize {
|
||||||
|
return nil, E.New("response too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
msgType := binary.BigEndian.Uint16(data[0:2])
|
||||||
|
if msgType&0xC000 != 0 {
|
||||||
|
return nil, E.New("invalid STUN message: top 2 bits not zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie := binary.BigEndian.Uint32(data[4:8])
|
||||||
|
if cookie != magicCookie {
|
||||||
|
return nil, E.New("invalid magic cookie")
|
||||||
|
}
|
||||||
|
|
||||||
|
var txID TransactionID
|
||||||
|
copy(txID[:], data[8:20])
|
||||||
|
if txID != expectedTxID {
|
||||||
|
return nil, E.New("transaction ID mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
|
||||||
|
if msgLen > len(data)-headerSize {
|
||||||
|
return nil, E.New("message length exceeds data")
|
||||||
|
}
|
||||||
|
|
||||||
|
attrData := data[headerSize : headerSize+msgLen]
|
||||||
|
|
||||||
|
if msgType == bindingErrorResponse {
|
||||||
|
return nil, parseErrorResponse(attrData)
|
||||||
|
}
|
||||||
|
if msgType != bindingSuccessResponse {
|
||||||
|
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &parsedResponse{}
|
||||||
|
offset := 0
|
||||||
|
for offset+4 <= len(attrData) {
|
||||||
|
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
|
||||||
|
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
|
||||||
|
if offset+4+attrLen > len(attrData) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attrValue := attrData[offset+4 : offset+4+attrLen]
|
||||||
|
|
||||||
|
switch attrType {
|
||||||
|
case attrXORMappedAddress:
|
||||||
|
addr, err := parseXORMappedAddress(attrValue, txID)
|
||||||
|
if err == nil {
|
||||||
|
resp.xorMappedAddr = addr
|
||||||
|
}
|
||||||
|
case attrMappedAddress:
|
||||||
|
addr, err := parseMappedAddress(attrValue)
|
||||||
|
if err == nil {
|
||||||
|
resp.mappedAddr = addr
|
||||||
|
}
|
||||||
|
case attrOtherAddress:
|
||||||
|
addr, err := parseMappedAddress(attrValue)
|
||||||
|
if err == nil {
|
||||||
|
resp.otherAddr = addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += 4 + attrLen + paddingLen(attrLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseErrorResponse(data []byte) error {
|
||||||
|
offset := 0
|
||||||
|
for offset+4 <= len(data) {
|
||||||
|
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
||||||
|
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
|
||||||
|
if offset+4+attrLen > len(data) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if attrType == attrErrorCode && attrLen >= 4 {
|
||||||
|
attrValue := data[offset+4 : offset+4+attrLen]
|
||||||
|
class := int(attrValue[2] & 0x07)
|
||||||
|
number := int(attrValue[3])
|
||||||
|
code := class*100 + number
|
||||||
|
if attrLen > 4 {
|
||||||
|
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
|
||||||
|
}
|
||||||
|
return E.New("STUN error ", code)
|
||||||
|
}
|
||||||
|
offset += 4 + attrLen + paddingLen(attrLen)
|
||||||
|
}
|
||||||
|
return E.New("STUN error response")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
family := data[1]
|
||||||
|
xPort := binary.BigEndian.Uint16(data[2:4])
|
||||||
|
port := xPort ^ uint16(magicCookie>>16)
|
||||||
|
|
||||||
|
switch family {
|
||||||
|
case familyIPv4:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
|
||||||
|
}
|
||||||
|
var ip [4]byte
|
||||||
|
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
|
||||||
|
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
|
||||||
|
case familyIPv6:
|
||||||
|
if len(data) < 20 {
|
||||||
|
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
|
||||||
|
}
|
||||||
|
var ip [16]byte
|
||||||
|
var xorKey [16]byte
|
||||||
|
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
|
||||||
|
copy(xorKey[4:16], txID[:])
|
||||||
|
for i := range 16 {
|
||||||
|
ip[i] = data[4+i] ^ xorKey[i]
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||||
|
default:
|
||||||
|
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
family := data[1]
|
||||||
|
port := binary.BigEndian.Uint16(data[2:4])
|
||||||
|
|
||||||
|
switch family {
|
||||||
|
case familyIPv4:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(
|
||||||
|
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
|
||||||
|
), nil
|
||||||
|
case familyIPv6:
|
||||||
|
if len(data) < 20 {
|
||||||
|
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
|
||||||
|
}
|
||||||
|
var ip [16]byte
|
||||||
|
copy(ip[:], data[4:20])
|
||||||
|
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||||
|
default:
|
||||||
|
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
|
||||||
|
request := buildBindingRequest(txID, attrs...)
|
||||||
|
currentRTO := rto
|
||||||
|
retransmitCount := 0
|
||||||
|
|
||||||
|
sendTime := time.Now()
|
||||||
|
_, err := conn.WriteTo(request, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, E.Cause(err, "send STUN request")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, E.Cause(err, "set read deadline")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _, readErr := conn.ReadFrom(buf)
|
||||||
|
if readErr != nil {
|
||||||
|
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
|
||||||
|
retransmitCount++
|
||||||
|
currentRTO *= 2
|
||||||
|
sendTime = time.Now()
|
||||||
|
_, err = conn.WriteTo(request, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, E.Cause(err, "retransmit STUN request")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, 0, E.Cause(readErr, "read STUN response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < headerSize || buf[0]&0xC0 != 0 ||
|
||||||
|
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var receivedTxID TransactionID
|
||||||
|
copy(receivedTxID[:], buf[8:20])
|
||||||
|
if receivedTxID != txID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
latency := time.Since(sendTime)
|
||||||
|
|
||||||
|
resp, parseErr := parseResponse(buf[:n], txID)
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, 0, parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, latency, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(options Options) (*Result, error) {
|
||||||
|
ctx := options.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
server := options.Server
|
||||||
|
if server == "" {
|
||||||
|
server = DefaultServer
|
||||||
|
}
|
||||||
|
serverSocksaddr := M.ParseSocksaddr(server)
|
||||||
|
if serverSocksaddr.Port == 0 {
|
||||||
|
serverSocksaddr.Port = 3478
|
||||||
|
}
|
||||||
|
|
||||||
|
reportProgress := options.OnProgress
|
||||||
|
if reportProgress == nil {
|
||||||
|
reportProgress = func(Progress) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
packetConn net.PacketConn
|
||||||
|
serverAddr net.Addr
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if options.Dialer != nil {
|
||||||
|
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create UDP socket")
|
||||||
|
}
|
||||||
|
serverAddr = serverSocksaddr
|
||||||
|
} else {
|
||||||
|
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
|
||||||
|
if resolveErr != nil {
|
||||||
|
return nil, E.Cause(resolveErr, "resolve STUN server")
|
||||||
|
}
|
||||||
|
packetConn, err = net.ListenPacket("udp", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create UDP socket")
|
||||||
|
}
|
||||||
|
serverAddr = serverUDPAddr
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = packetConn.Close()
|
||||||
|
}()
|
||||||
|
if deadline.NeedAdditionalReadDeadline(packetConn) {
|
||||||
|
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
rto := defaultRTO
|
||||||
|
|
||||||
|
// Phase 1: Binding
|
||||||
|
reportProgress(Progress{Phase: PhaseBinding})
|
||||||
|
|
||||||
|
txID := newTransactionID()
|
||||||
|
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "binding request")
|
||||||
|
}
|
||||||
|
|
||||||
|
rto = max(minRTO, 3*latency)
|
||||||
|
|
||||||
|
externalAddr, ok := resp.externalAddr()
|
||||||
|
if !ok {
|
||||||
|
return nil, E.New("no mapped address in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &Result{
|
||||||
|
ExternalAddr: externalAddr.String(),
|
||||||
|
LatencyMs: int32(latency.Milliseconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
reportProgress(Progress{
|
||||||
|
Phase: PhaseBinding,
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
})
|
||||||
|
|
||||||
|
otherAddr := resp.otherAddr
|
||||||
|
if !otherAddr.IsValid() {
|
||||||
|
result.NATTypeSupported = false
|
||||||
|
reportProgress(Progress{
|
||||||
|
Phase: PhaseDone,
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
})
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
result.NATTypeSupported = true
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return result, nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
|
||||||
|
reportProgress(Progress{
|
||||||
|
Phase: PhaseNATMapping,
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
})
|
||||||
|
|
||||||
|
result.NATMapping = detectNATMapping(
|
||||||
|
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
|
||||||
|
)
|
||||||
|
|
||||||
|
reportProgress(Progress{
|
||||||
|
Phase: PhaseNATMapping,
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
NATMapping: result.NATMapping,
|
||||||
|
})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return result, nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
|
||||||
|
reportProgress(Progress{
|
||||||
|
Phase: PhaseNATFiltering,
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
NATMapping: result.NATMapping,
|
||||||
|
})
|
||||||
|
|
||||||
|
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
|
||||||
|
|
||||||
|
reportProgress(Progress{
|
||||||
|
Phase: PhaseDone,
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
NATMapping: result.NATMapping,
|
||||||
|
NATFiltering: result.NATFiltering,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectNATMapping(
|
||||||
|
conn net.PacketConn,
|
||||||
|
serverPort uint16,
|
||||||
|
externalAddr netip.AddrPort,
|
||||||
|
otherAddr netip.AddrPort,
|
||||||
|
rto time.Duration,
|
||||||
|
) NATMapping {
|
||||||
|
// Mapping Test II: Send to other_ip:server_port
|
||||||
|
testIIAddr := net.UDPAddrFromAddrPort(
|
||||||
|
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
|
||||||
|
)
|
||||||
|
txID2 := newTransactionID()
|
||||||
|
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
|
||||||
|
if err != nil {
|
||||||
|
return NATMappingUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
externalAddr2, ok := resp2.externalAddr()
|
||||||
|
if !ok {
|
||||||
|
return NATMappingUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
if externalAddr == externalAddr2 {
|
||||||
|
return NATMappingEndpointIndependent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping Test III: Send to other_ip:other_port
|
||||||
|
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
|
||||||
|
txID3 := newTransactionID()
|
||||||
|
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
|
||||||
|
if err != nil {
|
||||||
|
return NATMappingUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
externalAddr3, ok := resp3.externalAddr()
|
||||||
|
if !ok {
|
||||||
|
return NATMappingUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
if externalAddr2 == externalAddr3 {
|
||||||
|
return NATMappingAddressDependent
|
||||||
|
}
|
||||||
|
return NATMappingAddressAndPortDependent
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectNATFiltering(
|
||||||
|
conn net.PacketConn,
|
||||||
|
serverAddr net.Addr,
|
||||||
|
rto time.Duration,
|
||||||
|
) NATFiltering {
|
||||||
|
// Filtering Test II: Request response from different IP and port
|
||||||
|
txID := newTransactionID()
|
||||||
|
_, _, err := roundTrip(conn, serverAddr, txID,
|
||||||
|
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
|
||||||
|
if err == nil {
|
||||||
|
return NATFilteringEndpointIndependent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtering Test III: Request response from different port only
|
||||||
|
txID = newTransactionID()
|
||||||
|
_, _, err = roundTrip(conn, serverAddr, txID,
|
||||||
|
[]stunAttribute{changeRequestAttr(changePort)}, rto)
|
||||||
|
if err == nil {
|
||||||
|
return NATFilteringAddressDependent
|
||||||
|
}
|
||||||
|
|
||||||
|
return NATFilteringAddressAndPortDependent
|
||||||
|
}
|
||||||
|
|
||||||
|
func paddingLen(n int) int {
|
||||||
|
if n%4 == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 4 - n%4
|
||||||
|
}
|
||||||
@@ -38,37 +38,6 @@ func (w *acmeWrapper) Close() error {
|
|||||||
return nil
|
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) {
|
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||||
var acmeServer string
|
var acmeServer string
|
||||||
switch options.Provider {
|
switch options.Provider {
|
||||||
@@ -91,8 +60,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
|||||||
storage = certmagic.Default.Storage
|
storage = certmagic.Default.Storage
|
||||||
}
|
}
|
||||||
zapLogger := zap.New(zapcore.NewCore(
|
zapLogger := zap.New(zapcore.NewCore(
|
||||||
zapcore.NewConsoleEncoder(encoderConfig()),
|
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
|
||||||
&acmeLogWriter{logger: logger},
|
&ACMELogWriter{Logger: logger},
|
||||||
zap.DebugLevel,
|
zap.DebugLevel,
|
||||||
))
|
))
|
||||||
config := &certmagic.Config{
|
config := &certmagic.Config{
|
||||||
@@ -158,7 +127,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
|||||||
} else {
|
} else {
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
GetCertificate: config.GetCertificate,
|
GetCertificate: config.GetCertificate,
|
||||||
NextProtos: []string{ACMETLS1Protocol},
|
NextProtos: []string{C.ACMETLS1Protocol},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
||||||
|
|||||||
41
common/tls/acme_logger.go
Normal file
41
common/tls/acme_logger.go
Normal 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
|
||||||
|
}
|
||||||
@@ -32,6 +32,10 @@ type RealityServerConfig struct {
|
|||||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
var tlsConfig utls.RealityConfig
|
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 {
|
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||||
return nil, E.New("acme is unavailable in reality")
|
return nil, E.New("acme is unavailable in reality")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,87 @@ import (
|
|||||||
"github.com/sagernet/fswatch"
|
"github.com/sagernet/fswatch"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
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/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInsecureUnused = E.New("tls: insecure unused")
|
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 {
|
type STDServerConfig struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
|
certificateProvider managedCertificateProvider
|
||||||
acmeService adapter.SimpleLifecycle
|
acmeService adapter.SimpleLifecycle
|
||||||
certificate []byte
|
certificate []byte
|
||||||
key []byte
|
key []byte
|
||||||
@@ -53,18 +121,17 @@ func (c *STDServerConfig) SetServerName(serverName string) {
|
|||||||
func (c *STDServerConfig) NextProtos() []string {
|
func (c *STDServerConfig) NextProtos() []string {
|
||||||
c.access.RLock()
|
c.access.RLock()
|
||||||
defer c.access.RUnlock()
|
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:]
|
return c.config.NextProtos[1:]
|
||||||
} else {
|
|
||||||
return c.config.NextProtos
|
|
||||||
}
|
}
|
||||||
|
return c.config.NextProtos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||||
c.access.Lock()
|
c.access.Lock()
|
||||||
defer c.access.Unlock()
|
defer c.access.Unlock()
|
||||||
config := c.config.Clone()
|
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...)
|
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||||
} else {
|
} else {
|
||||||
config.NextProtos = nextProto
|
config.NextProtos = nextProto
|
||||||
@@ -72,6 +139,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
|||||||
c.config = config
|
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) {
|
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
|
||||||
return c.config, nil
|
return c.config, nil
|
||||||
}
|
}
|
||||||
@@ -91,15 +170,39 @@ func (c *STDServerConfig) Clone() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Start() error {
|
func (c *STDServerConfig) Start() error {
|
||||||
if c.acmeService != nil {
|
if c.certificateProvider != nil {
|
||||||
return c.acmeService.Start()
|
err := c.certificateProvider.Start()
|
||||||
} else {
|
|
||||||
err := c.startWatcher()
|
|
||||||
if err != nil {
|
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 {
|
func (c *STDServerConfig) startWatcher() error {
|
||||||
@@ -203,23 +306,34 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Close() error {
|
func (c *STDServerConfig) Close() error {
|
||||||
if c.acmeService != nil {
|
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
|
||||||
return c.acmeService.Close()
|
|
||||||
}
|
|
||||||
if c.watcher != nil {
|
|
||||||
return c.watcher.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
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 tlsConfig *tls.Config
|
||||||
|
var certificateProvider managedCertificateProvider
|
||||||
var acmeService adapter.SimpleLifecycle
|
var acmeService adapter.SimpleLifecycle
|
||||||
var err error
|
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
|
//nolint:staticcheck
|
||||||
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -272,7 +386,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
certificate []byte
|
certificate []byte
|
||||||
key []byte
|
key []byte
|
||||||
)
|
)
|
||||||
if acmeService == nil {
|
if certificateProvider == nil && acmeService == nil {
|
||||||
if len(options.Certificate) > 0 {
|
if len(options.Certificate) > 0 {
|
||||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
} else if options.CertificatePath != "" {
|
} else if options.CertificatePath != "" {
|
||||||
@@ -360,6 +474,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
serverConfig := &STDServerConfig{
|
serverConfig := &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
certificateProvider: certificateProvider,
|
||||||
acmeService: acmeService,
|
acmeService: acmeService,
|
||||||
certificate: certificate,
|
certificate: certificate,
|
||||||
key: key,
|
key: key,
|
||||||
@@ -369,8 +484,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
echKeyPath: echKeyPath,
|
echKeyPath: echKeyPath,
|
||||||
}
|
}
|
||||||
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
serverConfig.access.Lock()
|
serverConfig.access.RLock()
|
||||||
defer serverConfig.access.Unlock()
|
defer serverConfig.access.RUnlock()
|
||||||
return serverConfig.config, nil
|
return serverConfig.config, nil
|
||||||
}
|
}
|
||||||
var config ServerConfig = serverConfig
|
var config ServerConfig = serverConfig
|
||||||
@@ -387,3 +502,27 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
}
|
}
|
||||||
return config, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,19 +15,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DNSTypeLegacy = "legacy"
|
DNSTypeLegacy = "legacy"
|
||||||
DNSTypeLegacyRcode = "legacy_rcode"
|
DNSTypeUDP = "udp"
|
||||||
DNSTypeUDP = "udp"
|
DNSTypeTCP = "tcp"
|
||||||
DNSTypeTCP = "tcp"
|
DNSTypeTLS = "tls"
|
||||||
DNSTypeTLS = "tls"
|
DNSTypeHTTPS = "https"
|
||||||
DNSTypeHTTPS = "https"
|
DNSTypeQUIC = "quic"
|
||||||
DNSTypeQUIC = "quic"
|
DNSTypeHTTP3 = "h3"
|
||||||
DNSTypeHTTP3 = "h3"
|
DNSTypeLocal = "local"
|
||||||
DNSTypeLocal = "local"
|
DNSTypeHosts = "hosts"
|
||||||
DNSTypeHosts = "hosts"
|
DNSTypeFakeIP = "fakeip"
|
||||||
DNSTypeFakeIP = "fakeip"
|
DNSTypeDHCP = "dhcp"
|
||||||
DNSTypeDHCP = "dhcp"
|
DNSTypeTailscale = "tailscale"
|
||||||
DNSTypeTailscale = "tailscale"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,36 +1,39 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeTun = "tun"
|
TypeTun = "tun"
|
||||||
TypeRedirect = "redirect"
|
TypeRedirect = "redirect"
|
||||||
TypeTProxy = "tproxy"
|
TypeTProxy = "tproxy"
|
||||||
TypeDirect = "direct"
|
TypeDirect = "direct"
|
||||||
TypeBlock = "block"
|
TypeBlock = "block"
|
||||||
TypeDNS = "dns"
|
TypeDNS = "dns"
|
||||||
TypeSOCKS = "socks"
|
TypeSOCKS = "socks"
|
||||||
TypeHTTP = "http"
|
TypeHTTP = "http"
|
||||||
TypeMixed = "mixed"
|
TypeMixed = "mixed"
|
||||||
TypeShadowsocks = "shadowsocks"
|
TypeShadowsocks = "shadowsocks"
|
||||||
TypeVMess = "vmess"
|
TypeVMess = "vmess"
|
||||||
TypeTrojan = "trojan"
|
TypeTrojan = "trojan"
|
||||||
TypeNaive = "naive"
|
TypeNaive = "naive"
|
||||||
TypeWireGuard = "wireguard"
|
TypeWireGuard = "wireguard"
|
||||||
TypeHysteria = "hysteria"
|
TypeHysteria = "hysteria"
|
||||||
TypeTor = "tor"
|
TypeTor = "tor"
|
||||||
TypeSSH = "ssh"
|
TypeSSH = "ssh"
|
||||||
TypeShadowTLS = "shadowtls"
|
TypeShadowTLS = "shadowtls"
|
||||||
TypeAnyTLS = "anytls"
|
TypeAnyTLS = "anytls"
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeVLESS = "vless"
|
TypeVLESS = "vless"
|
||||||
TypeTUIC = "tuic"
|
TypeTUIC = "tuic"
|
||||||
TypeHysteria2 = "hysteria2"
|
TypeHysteria2 = "hysteria2"
|
||||||
TypeTailscale = "tailscale"
|
TypeTailscale = "tailscale"
|
||||||
TypeDERP = "derp"
|
TypeCloudflared = "cloudflared"
|
||||||
TypeResolved = "resolved"
|
TypeDERP = "derp"
|
||||||
TypeSSMAPI = "ssm-api"
|
TypeResolved = "resolved"
|
||||||
TypeCCM = "ccm"
|
TypeSSMAPI = "ssm-api"
|
||||||
TypeOCM = "ocm"
|
TypeCCM = "ccm"
|
||||||
TypeOOMKiller = "oom-killer"
|
TypeOCM = "ocm"
|
||||||
|
TypeOOMKiller = "oom-killer"
|
||||||
|
TypeACME = "acme"
|
||||||
|
TypeCloudflareOriginCA = "cloudflare-origin-ca"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -88,6 +91,8 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "AnyTLS"
|
return "AnyTLS"
|
||||||
case TypeTailscale:
|
case TypeTailscale:
|
||||||
return "Tailscale"
|
return "Tailscale"
|
||||||
|
case TypeCloudflared:
|
||||||
|
return "Cloudflared"
|
||||||
case TypeSelector:
|
case TypeSelector:
|
||||||
return "Selector"
|
return "Selector"
|
||||||
case TypeURLTest:
|
case TypeURLTest:
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ const (
|
|||||||
RuleSetVersion2
|
RuleSetVersion2
|
||||||
RuleSetVersion3
|
RuleSetVersion3
|
||||||
RuleSetVersion4
|
RuleSetVersion4
|
||||||
RuleSetVersionCurrent = RuleSetVersion4
|
RuleSetVersion5
|
||||||
|
RuleSetVersionCurrent = RuleSetVersion5
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RuleActionTypeRoute = "route"
|
RuleActionTypeRoute = "route"
|
||||||
RuleActionTypeRouteOptions = "route-options"
|
RuleActionTypeRouteOptions = "route-options"
|
||||||
|
RuleActionTypeEvaluate = "evaluate"
|
||||||
|
RuleActionTypeRespond = "respond"
|
||||||
RuleActionTypeDirect = "direct"
|
RuleActionTypeDirect = "direct"
|
||||||
RuleActionTypeBypass = "bypass"
|
RuleActionTypeBypass = "bypass"
|
||||||
RuleActionTypeReject = "reject"
|
RuleActionTypeReject = "reject"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package tls
|
package constant
|
||||||
|
|
||||||
const ACMETLS1Protocol = "acme-tls/1"
|
const ACMETLS1Protocol = "acme-tls/1"
|
||||||
@@ -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 {
|
if !common.Any(options.Services, func(it option.Service) bool {
|
||||||
return it.Type == C.TypeOOMKiller
|
return it.Type == C.TypeOOMKiller
|
||||||
}) {
|
}) {
|
||||||
|
oomOptions := &option.OOMKillerServiceOptions{
|
||||||
|
KillerDisabled: s.oomKillerDisabled,
|
||||||
|
MemoryLimitOverride: s.oomMemoryLimit,
|
||||||
|
}
|
||||||
options.Services = append(options.Services, option.Service{
|
options.Services = append(options.Services, option.Service{
|
||||||
Type: C.TypeOOMKiller,
|
Type: C.TypeOOMKiller,
|
||||||
|
Options: oomOptions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ type PlatformHandler interface {
|
|||||||
ServiceReload() error
|
ServiceReload() error
|
||||||
SystemProxyStatus() (*SystemProxyStatus, error)
|
SystemProxyStatus() (*SystemProxyStatus, error)
|
||||||
SetSystemProxyEnabled(enabled bool) error
|
SetSystemProxyEnabled(enabled bool) error
|
||||||
|
TriggerNativeCrash() error
|
||||||
WriteDebugMessage(message string)
|
WriteDebugMessage(message string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,20 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"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"
|
"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"
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/protocol/group"
|
"github.com/sagernet/sing-box/protocol/group"
|
||||||
|
"github.com/sagernet/sing-box/service/oomkiller"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/batch"
|
"github.com/sagernet/sing/common/batch"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -24,6 +30,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,10 +40,12 @@ var _ StartedServiceServer = (*StartedService)(nil)
|
|||||||
type StartedService struct {
|
type StartedService struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
// platform adapter.PlatformInterface
|
// platform adapter.PlatformInterface
|
||||||
handler PlatformHandler
|
handler PlatformHandler
|
||||||
debug bool
|
debug bool
|
||||||
logMaxLines int
|
logMaxLines int
|
||||||
oomKiller bool
|
oomKillerEnabled bool
|
||||||
|
oomKillerDisabled bool
|
||||||
|
oomMemoryLimit uint64
|
||||||
// workingDirectory string
|
// workingDirectory string
|
||||||
// tempDirectory string
|
// tempDirectory string
|
||||||
// userID int
|
// userID int
|
||||||
@@ -64,10 +74,12 @@ type StartedService struct {
|
|||||||
type ServiceOptions struct {
|
type ServiceOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Platform adapter.PlatformInterface
|
// Platform adapter.PlatformInterface
|
||||||
Handler PlatformHandler
|
Handler PlatformHandler
|
||||||
Debug bool
|
Debug bool
|
||||||
LogMaxLines int
|
LogMaxLines int
|
||||||
OOMKiller bool
|
OOMKillerEnabled bool
|
||||||
|
OOMKillerDisabled bool
|
||||||
|
OOMMemoryLimit uint64
|
||||||
// WorkingDirectory string
|
// WorkingDirectory string
|
||||||
// TempDirectory string
|
// TempDirectory string
|
||||||
// UserID int
|
// UserID int
|
||||||
@@ -79,10 +91,12 @@ func NewStartedService(options ServiceOptions) *StartedService {
|
|||||||
s := &StartedService{
|
s := &StartedService{
|
||||||
ctx: options.Context,
|
ctx: options.Context,
|
||||||
// platform: options.Platform,
|
// platform: options.Platform,
|
||||||
handler: options.Handler,
|
handler: options.Handler,
|
||||||
debug: options.Debug,
|
debug: options.Debug,
|
||||||
logMaxLines: options.LogMaxLines,
|
logMaxLines: options.LogMaxLines,
|
||||||
oomKiller: options.OOMKiller,
|
oomKillerEnabled: options.OOMKillerEnabled,
|
||||||
|
oomKillerDisabled: options.OOMKillerDisabled,
|
||||||
|
oomMemoryLimit: options.OOMMemoryLimit,
|
||||||
// workingDirectory: options.WorkingDirectory,
|
// workingDirectory: options.WorkingDirectory,
|
||||||
// tempDirectory: options.TempDirectory,
|
// tempDirectory: options.TempDirectory,
|
||||||
// userID: options.UserID,
|
// userID: options.UserID,
|
||||||
@@ -168,7 +182,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
|
|||||||
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
||||||
s.serviceAccess.Lock()
|
s.serviceAccess.Lock()
|
||||||
switch s.serviceStatus.Status {
|
switch s.serviceStatus.Status {
|
||||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
|
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
|
||||||
default:
|
default:
|
||||||
s.serviceAccess.Unlock()
|
s.serviceAccess.Unlock()
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -226,13 +240,14 @@ func (s *StartedService) CloseService() error {
|
|||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
s.updateStatus(ServiceStatus_STOPPING)
|
s.updateStatus(ServiceStatus_STOPPING)
|
||||||
if s.instance != nil {
|
instance := s.instance
|
||||||
err := s.instance.Close()
|
s.instance = nil
|
||||||
|
if instance != nil {
|
||||||
|
err := instance.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s.updateStatusError(err)
|
return s.updateStatusError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.instance = nil
|
|
||||||
s.startedAt = time.Time{}
|
s.startedAt = time.Time{}
|
||||||
s.updateStatus(ServiceStatus_IDLE)
|
s.updateStatus(ServiceStatus_IDLE)
|
||||||
s.serviceAccess.Unlock()
|
s.serviceAccess.Unlock()
|
||||||
@@ -681,7 +696,42 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||||
@@ -949,11 +999,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
|||||||
var processInfo *ProcessInfo
|
var processInfo *ProcessInfo
|
||||||
if metadata.Metadata.ProcessInfo != nil {
|
if metadata.Metadata.ProcessInfo != nil {
|
||||||
processInfo = &ProcessInfo{
|
processInfo = &ProcessInfo{
|
||||||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||||
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
|
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Connection{
|
return &Connection{
|
||||||
@@ -1018,9 +1068,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
|
|||||||
return &DeprecatedWarnings{
|
return &DeprecatedWarnings{
|
||||||
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
||||||
return &DeprecatedWarning{
|
return &DeprecatedWarning{
|
||||||
Message: it.Message(),
|
Message: it.Message(),
|
||||||
Impending: it.Impending(),
|
Impending: it.Impending(),
|
||||||
MigrationLink: it.MigrationLink,
|
MigrationLink: it.MigrationLink,
|
||||||
|
Description: it.Description,
|
||||||
|
DeprecatedVersion: it.DeprecatedVersion,
|
||||||
|
ScheduledVersion: it.ScheduledVersion,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -1032,6 +1085,386 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
|
|||||||
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
|
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
|
||||||
|
err := s.waitForStarted(server.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
subscription, done, err := s.urlTestObserver.Subscribe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.urlTestObserver.UnSubscribe(subscription)
|
||||||
|
for {
|
||||||
|
s.serviceAccess.RLock()
|
||||||
|
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||||
|
s.serviceAccess.RUnlock()
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
boxService := s.instance
|
||||||
|
s.serviceAccess.RUnlock()
|
||||||
|
historyStorage := boxService.urlTestHistoryStorage
|
||||||
|
var list OutboundList
|
||||||
|
for _, ob := range boxService.instance.Outbound().Outbounds() {
|
||||||
|
item := &GroupItem{
|
||||||
|
Tag: ob.Tag(),
|
||||||
|
Type: ob.Type(),
|
||||||
|
}
|
||||||
|
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
|
||||||
|
item.UrlTestTime = history.Time.Unix()
|
||||||
|
item.UrlTestDelay = int32(history.Delay)
|
||||||
|
}
|
||||||
|
list.Outbounds = append(list.Outbounds, item)
|
||||||
|
}
|
||||||
|
for _, ep := range boxService.instance.Endpoint().Endpoints() {
|
||||||
|
item := &GroupItem{
|
||||||
|
Tag: ep.Tag(),
|
||||||
|
Type: ep.Type(),
|
||||||
|
}
|
||||||
|
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
|
||||||
|
item.UrlTestTime = history.Time.Unix()
|
||||||
|
item.UrlTestDelay = int32(history.Delay)
|
||||||
|
}
|
||||||
|
list.Outbounds = append(list.Outbounds, item)
|
||||||
|
}
|
||||||
|
err = server.Send(&list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-subscription:
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return s.ctx.Err()
|
||||||
|
case <-server.Context().Done():
|
||||||
|
return server.Context().Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
|
||||||
|
if tag == "" {
|
||||||
|
return instance.instance.Outbound().Default(), nil
|
||||||
|
}
|
||||||
|
outbound, loaded := instance.instance.Outbound().Outbound(tag)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound not found: ", tag)
|
||||||
|
}
|
||||||
|
return outbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StartedService) StartNetworkQualityTest(
|
||||||
|
request *NetworkQualityTestRequest,
|
||||||
|
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
|
||||||
|
) error {
|
||||||
|
err := s.waitForStarted(server.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.serviceAccess.RLock()
|
||||||
|
boxService := s.instance
|
||||||
|
s.serviceAccess.RUnlock()
|
||||||
|
|
||||||
|
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
||||||
|
httpClient := networkquality.NewHTTPClient(resolvedDialer)
|
||||||
|
defer httpClient.CloseIdleConnections()
|
||||||
|
|
||||||
|
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, nqErr := networkquality.Run(networkquality.Options{
|
||||||
|
ConfigURL: request.ConfigURL,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
NewMeasurementClient: measurementClientFactory,
|
||||||
|
Serial: request.Serial,
|
||||||
|
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
|
||||||
|
Context: server.Context(),
|
||||||
|
OnProgress: func(p networkquality.Progress) {
|
||||||
|
_ = server.Send(&NetworkQualityTestProgress{
|
||||||
|
Phase: int32(p.Phase),
|
||||||
|
DownloadCapacity: p.DownloadCapacity,
|
||||||
|
UploadCapacity: p.UploadCapacity,
|
||||||
|
DownloadRPM: p.DownloadRPM,
|
||||||
|
UploadRPM: p.UploadRPM,
|
||||||
|
IdleLatencyMs: p.IdleLatencyMs,
|
||||||
|
ElapsedMs: p.ElapsedMs,
|
||||||
|
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
|
||||||
|
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
|
||||||
|
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
|
||||||
|
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if nqErr != nil {
|
||||||
|
return server.Send(&NetworkQualityTestProgress{
|
||||||
|
IsFinal: true,
|
||||||
|
Error: nqErr.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return server.Send(&NetworkQualityTestProgress{
|
||||||
|
Phase: int32(networkquality.PhaseDone),
|
||||||
|
DownloadCapacity: result.DownloadCapacity,
|
||||||
|
UploadCapacity: result.UploadCapacity,
|
||||||
|
DownloadRPM: result.DownloadRPM,
|
||||||
|
UploadRPM: result.UploadRPM,
|
||||||
|
IdleLatencyMs: result.IdleLatencyMs,
|
||||||
|
IsFinal: true,
|
||||||
|
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
|
||||||
|
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
|
||||||
|
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
|
||||||
|
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StartedService) StartSTUNTest(
|
||||||
|
request *STUNTestRequest,
|
||||||
|
server grpc.ServerStreamingServer[STUNTestProgress],
|
||||||
|
) error {
|
||||||
|
err := s.waitForStarted(server.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.serviceAccess.RLock()
|
||||||
|
boxService := s.instance
|
||||||
|
s.serviceAccess.RUnlock()
|
||||||
|
|
||||||
|
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
||||||
|
|
||||||
|
result, stunErr := stun.Run(stun.Options{
|
||||||
|
Server: request.Server,
|
||||||
|
Dialer: resolvedDialer,
|
||||||
|
Context: server.Context(),
|
||||||
|
OnProgress: func(p stun.Progress) {
|
||||||
|
_ = server.Send(&STUNTestProgress{
|
||||||
|
Phase: int32(p.Phase),
|
||||||
|
ExternalAddr: p.ExternalAddr,
|
||||||
|
LatencyMs: p.LatencyMs,
|
||||||
|
NatMapping: int32(p.NATMapping),
|
||||||
|
NatFiltering: int32(p.NATFiltering),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if stunErr != nil {
|
||||||
|
return server.Send(&STUNTestProgress{
|
||||||
|
IsFinal: true,
|
||||||
|
Error: stunErr.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return server.Send(&STUNTestProgress{
|
||||||
|
Phase: int32(stun.PhaseDone),
|
||||||
|
ExternalAddr: result.ExternalAddr,
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
NatMapping: int32(result.NATMapping),
|
||||||
|
NatFiltering: int32(result.NATFiltering),
|
||||||
|
IsFinal: true,
|
||||||
|
NatTypeSupported: result.NATTypeSupported,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StartedService) SubscribeTailscaleStatus(
|
||||||
|
_ *emptypb.Empty,
|
||||||
|
server grpc.ServerStreamingServer[TailscaleStatusUpdate],
|
||||||
|
) error {
|
||||||
|
err := s.waitForStarted(server.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.serviceAccess.RLock()
|
||||||
|
boxService := s.instance
|
||||||
|
s.serviceAccess.RUnlock()
|
||||||
|
|
||||||
|
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
||||||
|
if endpointManager == nil {
|
||||||
|
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
type tailscaleEndpoint struct {
|
||||||
|
tag string
|
||||||
|
provider adapter.TailscaleEndpoint
|
||||||
|
}
|
||||||
|
var endpoints []tailscaleEndpoint
|
||||||
|
for _, endpoint := range endpointManager.Endpoints() {
|
||||||
|
if endpoint.Type() != C.TypeTailscale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||||
|
if !loaded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, tailscaleEndpoint{
|
||||||
|
tag: endpoint.Tag(),
|
||||||
|
provider: provider,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(endpoints) == 0 {
|
||||||
|
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
||||||
|
}
|
||||||
|
|
||||||
|
type taggedStatus struct {
|
||||||
|
tag string
|
||||||
|
status *adapter.TailscaleEndpointStatus
|
||||||
|
}
|
||||||
|
updates := make(chan taggedStatus, len(endpoints))
|
||||||
|
ctx, cancel := context.WithCancel(server.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var waitGroup sync.WaitGroup
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
waitGroup.Add(1)
|
||||||
|
go func(tag string, provider adapter.TailscaleEndpoint) {
|
||||||
|
defer waitGroup.Done()
|
||||||
|
_ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) {
|
||||||
|
select {
|
||||||
|
case updates <- taggedStatus{tag: tag, status: endpointStatus}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(endpoint.tag, endpoint.provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
waitGroup.Wait()
|
||||||
|
close(updates)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var tags []string
|
||||||
|
statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints))
|
||||||
|
for update := range updates {
|
||||||
|
if _, exists := statuses[update.tag]; !exists {
|
||||||
|
tags = append(tags, update.tag)
|
||||||
|
}
|
||||||
|
statuses[update.tag] = update.status
|
||||||
|
protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses))
|
||||||
|
for _, tag := range tags {
|
||||||
|
protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag]))
|
||||||
|
}
|
||||||
|
sendErr := server.Send(&TailscaleStatusUpdate{
|
||||||
|
Endpoints: protoEndpoints,
|
||||||
|
})
|
||||||
|
if sendErr != nil {
|
||||||
|
return sendErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus {
|
||||||
|
userGroups := make([]*TailscaleUserGroup, len(s.UserGroups))
|
||||||
|
for i, group := range s.UserGroups {
|
||||||
|
peers := make([]*TailscalePeer, len(group.Peers))
|
||||||
|
for j, peer := range group.Peers {
|
||||||
|
peers[j] = tailscalePeerToProto(peer)
|
||||||
|
}
|
||||||
|
userGroups[i] = &TailscaleUserGroup{
|
||||||
|
UserID: group.UserID,
|
||||||
|
LoginName: group.LoginName,
|
||||||
|
DisplayName: group.DisplayName,
|
||||||
|
ProfilePicURL: group.ProfilePicURL,
|
||||||
|
Peers: peers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := &TailscaleEndpointStatus{
|
||||||
|
EndpointTag: tag,
|
||||||
|
BackendState: s.BackendState,
|
||||||
|
AuthURL: s.AuthURL,
|
||||||
|
NetworkName: s.NetworkName,
|
||||||
|
MagicDNSSuffix: s.MagicDNSSuffix,
|
||||||
|
UserGroups: userGroups,
|
||||||
|
}
|
||||||
|
if s.Self != nil {
|
||||||
|
result.Self = tailscalePeerToProto(s.Self)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
|
||||||
|
return &TailscalePeer{
|
||||||
|
HostName: peer.HostName,
|
||||||
|
DnsName: peer.DNSName,
|
||||||
|
Os: peer.OS,
|
||||||
|
TailscaleIPs: peer.TailscaleIPs,
|
||||||
|
Online: peer.Online,
|
||||||
|
ExitNode: peer.ExitNode,
|
||||||
|
ExitNodeOption: peer.ExitNodeOption,
|
||||||
|
Active: peer.Active,
|
||||||
|
RxBytes: peer.RxBytes,
|
||||||
|
TxBytes: peer.TxBytes,
|
||||||
|
KeyExpiry: peer.KeyExpiry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StartedService) StartTailscalePing(
|
||||||
|
request *TailscalePingRequest,
|
||||||
|
server grpc.ServerStreamingServer[TailscalePingResponse],
|
||||||
|
) error {
|
||||||
|
err := s.waitForStarted(server.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.serviceAccess.RLock()
|
||||||
|
boxService := s.instance
|
||||||
|
s.serviceAccess.RUnlock()
|
||||||
|
|
||||||
|
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
||||||
|
if endpointManager == nil {
|
||||||
|
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider adapter.TailscaleEndpoint
|
||||||
|
if request.EndpointTag != "" {
|
||||||
|
endpoint, loaded := endpointManager.Get(request.EndpointTag)
|
||||||
|
if !loaded {
|
||||||
|
return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag)
|
||||||
|
}
|
||||||
|
if endpoint.Type() != C.TypeTailscale {
|
||||||
|
return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag)
|
||||||
|
}
|
||||||
|
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||||
|
if !loaded {
|
||||||
|
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
|
||||||
|
}
|
||||||
|
provider = pingProvider
|
||||||
|
} else {
|
||||||
|
for _, endpoint := range endpointManager.Endpoints() {
|
||||||
|
if endpoint.Type() != C.TypeTailscale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||||
|
if loaded {
|
||||||
|
provider = pingProvider
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if provider == nil {
|
||||||
|
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
|
||||||
|
_ = server.Send(&TailscalePingResponse{
|
||||||
|
LatencyMs: result.LatencyMs,
|
||||||
|
IsDirect: result.IsDirect,
|
||||||
|
Endpoint: result.Endpoint,
|
||||||
|
DerpRegionID: result.DERPRegionID,
|
||||||
|
DerpRegionCode: result.DERPRegionCode,
|
||||||
|
Error: result.Error,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,12 +26,20 @@ service StartedService {
|
|||||||
|
|
||||||
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
||||||
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
|
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 SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
|
||||||
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
||||||
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||||
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
||||||
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
|
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 {
|
message ServiceStatus {
|
||||||
@@ -141,6 +149,15 @@ message SetSystemProxyEnabledRequest {
|
|||||||
bool enabled = 1;
|
bool enabled = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DebugCrashRequest {
|
||||||
|
enum Type {
|
||||||
|
GO = 0;
|
||||||
|
NATIVE = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type type = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message SubscribeConnectionsRequest {
|
message SubscribeConnectionsRequest {
|
||||||
int64 interval = 1;
|
int64 interval = 1;
|
||||||
}
|
}
|
||||||
@@ -195,7 +212,7 @@ message ProcessInfo {
|
|||||||
int32 userId = 2;
|
int32 userId = 2;
|
||||||
string userName = 3;
|
string userName = 3;
|
||||||
string processPath = 4;
|
string processPath = 4;
|
||||||
string packageName = 5;
|
repeated string packageNames = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CloseConnectionRequest {
|
message CloseConnectionRequest {
|
||||||
@@ -210,8 +227,105 @@ message DeprecatedWarning {
|
|||||||
string message = 1;
|
string message = 1;
|
||||||
bool impending = 2;
|
bool impending = 2;
|
||||||
string migrationLink = 3;
|
string migrationLink = 3;
|
||||||
|
string description = 4;
|
||||||
|
string deprecatedVersion = 5;
|
||||||
|
string scheduledVersion = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartedAt {
|
message StartedAt {
|
||||||
int64 startedAt = 1;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,27 +15,34 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
||||||
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
||||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||||
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
||||||
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
||||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
|
||||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
|
||||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
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.
|
// 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)
|
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, 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)
|
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)
|
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)
|
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)
|
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)
|
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
|
||||||
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, 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 {
|
type startedServiceClient struct {
|
||||||
@@ -278,6 +292,26 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
|
|||||||
return out, nil
|
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) {
|
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
|
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
|
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.
|
// StartedServiceServer is the server API for StartedService service.
|
||||||
// All implementations must embed UnimplementedStartedServiceServer
|
// All implementations must embed UnimplementedStartedServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -357,11 +486,18 @@ type StartedServiceServer interface {
|
|||||||
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
||||||
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
||||||
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, 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
|
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
|
||||||
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
||||||
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||||
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
||||||
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, 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()
|
mustEmbedUnimplementedStartedServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +572,14 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context,
|
|||||||
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
|
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 {
|
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||||
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
|
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) {
|
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
|
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) mustEmbedUnimplementedStartedServiceServer() {}
|
||||||
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -729,6 +893,42 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.
|
|||||||
return interceptor(ctx, in, info, handler)
|
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 {
|
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
m := new(SubscribeConnectionsRequest)
|
m := new(SubscribeConnectionsRequest)
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
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)
|
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.
|
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -863,6 +1118,14 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SetSystemProxyEnabled",
|
MethodName: "SetSystemProxyEnabled",
|
||||||
Handler: _StartedService_SetSystemProxyEnabled_Handler,
|
Handler: _StartedService_SetSystemProxyEnabled_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "TriggerDebugCrash",
|
||||||
|
Handler: _StartedService_TriggerDebugCrash_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "TriggerOOMReport",
|
||||||
|
Handler: _StartedService_TriggerOOMReport_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "CloseConnection",
|
MethodName: "CloseConnection",
|
||||||
Handler: _StartedService_CloseConnection_Handler,
|
Handler: _StartedService_CloseConnection_Handler,
|
||||||
@@ -911,6 +1174,31 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _StartedService_SubscribeConnections_Handler,
|
Handler: _StartedService_SubscribeConnections_Handler,
|
||||||
ServerStreams: true,
|
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",
|
Metadata: "daemon/started_service.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
596
dns/client.go
596
dns/client.go
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/sagernet/sing/common/task"
|
"github.com/sagernet/sing/common/task"
|
||||||
"github.com/sagernet/sing/contrab/freelru"
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
"github.com/sagernet/sing/contrab/maphash"
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
@@ -32,59 +30,63 @@ var (
|
|||||||
var _ adapter.DNSClient = (*Client)(nil)
|
var _ adapter.DNSClient = (*Client)(nil)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
timeout time.Duration
|
ctx context.Context
|
||||||
disableCache bool
|
timeout time.Duration
|
||||||
disableExpire bool
|
disableCache bool
|
||||||
independentCache bool
|
disableExpire bool
|
||||||
clientSubnet netip.Prefix
|
optimisticTimeout time.Duration
|
||||||
rdrc adapter.RDRCStore
|
cacheCapacity uint32
|
||||||
initRDRCFunc func() adapter.RDRCStore
|
clientSubnet netip.Prefix
|
||||||
logger logger.ContextLogger
|
rdrc adapter.RDRCStore
|
||||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
initRDRCFunc func() adapter.RDRCStore
|
||||||
cacheLock compatible.Map[dns.Question, chan struct{}]
|
dnsCache adapter.DNSCacheStore
|
||||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
initDNSCacheFunc func() adapter.DNSCacheStore
|
||||||
transportCacheLock compatible.Map[dns.Question, chan struct{}]
|
logger logger.ContextLogger
|
||||||
|
cache freelru.Cache[dnsCacheKey, *dns.Msg]
|
||||||
|
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
|
||||||
|
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
Timeout time.Duration
|
Context context.Context
|
||||||
DisableCache bool
|
Timeout time.Duration
|
||||||
DisableExpire bool
|
DisableCache bool
|
||||||
IndependentCache bool
|
DisableExpire bool
|
||||||
CacheCapacity uint32
|
OptimisticTimeout time.Duration
|
||||||
ClientSubnet netip.Prefix
|
CacheCapacity uint32
|
||||||
RDRC func() adapter.RDRCStore
|
ClientSubnet netip.Prefix
|
||||||
Logger logger.ContextLogger
|
RDRC func() adapter.RDRCStore
|
||||||
|
DNSCache func() adapter.DNSCacheStore
|
||||||
|
Logger logger.ContextLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(options ClientOptions) *Client {
|
func NewClient(options ClientOptions) *Client {
|
||||||
client := &Client{
|
|
||||||
timeout: options.Timeout,
|
|
||||||
disableCache: options.DisableCache,
|
|
||||||
disableExpire: options.DisableExpire,
|
|
||||||
independentCache: options.IndependentCache,
|
|
||||||
clientSubnet: options.ClientSubnet,
|
|
||||||
initRDRCFunc: options.RDRC,
|
|
||||||
logger: options.Logger,
|
|
||||||
}
|
|
||||||
if client.timeout == 0 {
|
|
||||||
client.timeout = C.DNSTimeout
|
|
||||||
}
|
|
||||||
cacheCapacity := options.CacheCapacity
|
cacheCapacity := options.CacheCapacity
|
||||||
if cacheCapacity < 1024 {
|
if cacheCapacity < 1024 {
|
||||||
cacheCapacity = 1024
|
cacheCapacity = 1024
|
||||||
}
|
}
|
||||||
if !client.disableCache {
|
client := &Client{
|
||||||
if !client.independentCache {
|
ctx: options.Context,
|
||||||
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
timeout: options.Timeout,
|
||||||
} else {
|
disableCache: options.DisableCache,
|
||||||
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
disableExpire: options.DisableExpire,
|
||||||
}
|
optimisticTimeout: options.OptimisticTimeout,
|
||||||
|
cacheCapacity: cacheCapacity,
|
||||||
|
clientSubnet: options.ClientSubnet,
|
||||||
|
initRDRCFunc: options.RDRC,
|
||||||
|
initDNSCacheFunc: options.DNSCache,
|
||||||
|
logger: options.Logger,
|
||||||
|
}
|
||||||
|
if client.timeout == 0 {
|
||||||
|
client.timeout = C.DNSTimeout
|
||||||
|
}
|
||||||
|
if !client.disableCache && client.initDNSCacheFunc == nil {
|
||||||
|
client.initializeMemoryCache()
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
type transportCacheKey struct {
|
type dnsCacheKey struct {
|
||||||
dns.Question
|
dns.Question
|
||||||
transportTag string
|
transportTag string
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,19 @@ func (c *Client) Start() {
|
|||||||
if c.initRDRCFunc != nil {
|
if c.initRDRCFunc != nil {
|
||||||
c.rdrc = c.initRDRCFunc()
|
c.rdrc = c.initRDRCFunc()
|
||||||
}
|
}
|
||||||
|
if c.initDNSCacheFunc != nil {
|
||||||
|
c.dnsCache = c.initDNSCacheFunc()
|
||||||
|
}
|
||||||
|
if c.dnsCache == nil {
|
||||||
|
c.initializeMemoryCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) initializeMemoryCache() {
|
||||||
|
if c.disableCache || c.cache != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||||
@@ -109,7 +124,38 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
|||||||
return 0, false
|
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 computeTimeToLive(response *dns.Msg) uint32 {
|
||||||
|
var timeToLive uint32
|
||||||
|
if len(response.Answer) == 0 {
|
||||||
|
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||||
|
return soaTTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||||
|
timeToLive = record.Header().Ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return timeToLive
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
record.Header().Ttl = timeToLive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
|
||||||
if len(message.Question) == 0 {
|
if len(message.Question) == 0 {
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||||
@@ -123,13 +169,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
||||||
}
|
}
|
||||||
clientSubnet := options.ClientSubnet
|
message = c.prepareExchangeMessage(message, options)
|
||||||
if !clientSubnet.IsValid() {
|
|
||||||
clientSubnet = c.clientSubnet
|
|
||||||
}
|
|
||||||
if clientSubnet.IsValid() {
|
|
||||||
message = SetClientSubnet(message, clientSubnet)
|
|
||||||
}
|
|
||||||
|
|
||||||
isSimpleRequest := len(message.Question) == 1 &&
|
isSimpleRequest := len(message.Question) == 1 &&
|
||||||
len(message.Ns) == 0 &&
|
len(message.Ns) == 0 &&
|
||||||
@@ -141,40 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
!options.ClientSubnet.IsValid()
|
!options.ClientSubnet.IsValid()
|
||||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||||
if !disableCache {
|
if !disableCache {
|
||||||
if c.cache != nil {
|
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
|
||||||
if loaded {
|
if loaded {
|
||||||
select {
|
select {
|
||||||
case <-cond:
|
case <-cond:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
c.cacheLock.Delete(question)
|
|
||||||
close(cond)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
} else if c.transportCache != nil {
|
|
||||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
|
||||||
if loaded {
|
|
||||||
select {
|
|
||||||
case <-cond:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
c.transportCacheLock.Delete(question)
|
|
||||||
close(cond)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
c.cacheLock.Delete(cacheKey)
|
||||||
|
close(cond)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
response, ttl := c.loadResponse(question, transport)
|
response, ttl, isStale := c.loadResponse(question, transport)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
if isStale && !options.DisableOptimisticCache {
|
||||||
response.Id = message.Id
|
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
|
||||||
return response, nil
|
logOptimisticResponse(c.logger, ctx, response)
|
||||||
|
response.Id = message.Id
|
||||||
|
return response, nil
|
||||||
|
} else if !isStale {
|
||||||
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
|
response.Id = message.Id
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,62 +222,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return nil, ErrResponseRejectedCached
|
return nil, ErrResponseRejectedCached
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||||
response, err := transport.Exchange(ctx, message)
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var rcodeError RcodeError
|
return nil, err
|
||||||
if errors.As(err, &rcodeError) {
|
|
||||||
response = FixedResponseStatus(message, int(rcodeError))
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
|
||||||
validResponse := response
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
var (
|
|
||||||
addresses int
|
|
||||||
queryCNAME string
|
|
||||||
)
|
|
||||||
for _, rawRR := range validResponse.Answer {
|
|
||||||
switch rr := rawRR.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
break loop
|
|
||||||
case *dns.AAAA:
|
|
||||||
break loop
|
|
||||||
case *dns.CNAME:
|
|
||||||
queryCNAME = rr.Target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if queryCNAME == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
exMessage := *message
|
|
||||||
exMessage.Question = []dns.Question{{
|
|
||||||
Name: queryCNAME,
|
|
||||||
Qtype: question.Qtype,
|
|
||||||
}}
|
|
||||||
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if validResponse != response {
|
|
||||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
||||||
if responseChecker != nil {
|
if responseChecker != nil {
|
||||||
var rejected bool
|
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 {
|
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||||
rejected = true
|
rejected = true
|
||||||
} else if len(response.Answer) == 0 {
|
|
||||||
rejected = !responseChecker(nil)
|
|
||||||
} else {
|
} else {
|
||||||
rejected = !responseChecker(MessageToAddresses(response))
|
rejected = !responseChecker(response)
|
||||||
}
|
}
|
||||||
if rejected {
|
if rejected {
|
||||||
if !disableCache && c.rdrc != nil {
|
if !disableCache && c.rdrc != nil {
|
||||||
@@ -255,48 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return response, ErrResponseRejected
|
return response, ErrResponseRejected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if question.Qtype == dns.TypeHTTPS {
|
timeToLive := applyResponseOptions(question, response, options)
|
||||||
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
|
|
||||||
for _, rr := range response.Answer {
|
|
||||||
https, isHTTPS := rr.(*dns.HTTPS)
|
|
||||||
if !isHTTPS {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content := https.SVCB
|
|
||||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
|
||||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
return it.Key() != dns.SVCB_IPV6HINT
|
|
||||||
} else {
|
|
||||||
return it.Key() != dns.SVCB_IPV4HINT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
https.SVCB = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var timeToLive uint32
|
|
||||||
if len(response.Answer) == 0 {
|
|
||||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
|
||||||
timeToLive = soaTTL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if timeToLive == 0 {
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
|
||||||
timeToLive = record.Header().Ttl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.RewriteTTL != nil {
|
|
||||||
timeToLive = *options.RewriteTTL
|
|
||||||
}
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
record.Header().Ttl = timeToLive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !disableCache {
|
if !disableCache {
|
||||||
c.storeCache(transport, question, response, timeToLive)
|
c.storeCache(transport, question, response, timeToLive)
|
||||||
}
|
}
|
||||||
@@ -315,7 +261,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return response, nil
|
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)
|
domain = FqdnToDomain(domain)
|
||||||
dnsName := dns.Fqdn(domain)
|
dnsName := dns.Fqdn(domain)
|
||||||
var strategy C.DomainStrategy
|
var strategy C.DomainStrategy
|
||||||
@@ -362,8 +308,12 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
|||||||
func (c *Client) ClearCache() {
|
func (c *Client) ClearCache() {
|
||||||
if c.cache != nil {
|
if c.cache != nil {
|
||||||
c.cache.Purge()
|
c.cache.Purge()
|
||||||
} else if c.transportCache != nil {
|
}
|
||||||
c.transportCache.Purge()
|
if c.dnsCache != nil {
|
||||||
|
err := c.dnsCache.ClearDNSCache()
|
||||||
|
if err != nil && c.logger != nil {
|
||||||
|
c.logger.Warn("clear DNS cache: ", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,46 +329,44 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
|||||||
if timeToLive == 0 {
|
if timeToLive == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if c.dnsCache != nil {
|
||||||
|
packed, err := message.Pack()
|
||||||
|
if err == nil {
|
||||||
|
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
|
||||||
|
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.cache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||||
if c.disableExpire {
|
if c.disableExpire {
|
||||||
if !c.independentCache {
|
c.cache.Add(key, message.Copy())
|
||||||
c.cache.Add(question, message)
|
|
||||||
} else {
|
|
||||||
c.transportCache.Add(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
}, message)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if !c.independentCache {
|
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
|
||||||
} else {
|
|
||||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
}, message, 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{
|
question := dns.Question{
|
||||||
Name: name,
|
Name: name,
|
||||||
Qtype: qType,
|
Qtype: qType,
|
||||||
Qclass: dns.ClassINET,
|
Qclass: dns.ClassINET,
|
||||||
}
|
}
|
||||||
disableCache := c.disableCache || options.DisableCache
|
|
||||||
if !disableCache {
|
|
||||||
cachedAddresses, err := c.questionCache(question, transport)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return cachedAddresses, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message := dns.Msg{
|
message := dns.Msg{
|
||||||
MsgHdr: dns.MsgHdr{
|
MsgHdr: dns.MsgHdr{
|
||||||
RecursionDesired: true,
|
RecursionDesired: true,
|
||||||
},
|
},
|
||||||
Question: []dns.Question{question},
|
Question: []dns.Question{question},
|
||||||
}
|
}
|
||||||
|
disableCache := c.disableCache || options.DisableCache
|
||||||
|
if !disableCache {
|
||||||
|
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
|
||||||
|
if err != ErrNotCached {
|
||||||
|
return cachedAddresses, err
|
||||||
|
}
|
||||||
|
}
|
||||||
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -429,111 +377,181 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
|
|||||||
return MessageToAddresses(response), nil
|
return MessageToAddresses(response), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||||
response, _ := c.loadResponse(question, transport)
|
question := message.Question[0]
|
||||||
|
response, _, isStale := c.loadResponse(question, transport)
|
||||||
if response == nil {
|
if response == nil {
|
||||||
return nil, ErrNotCached
|
return nil, ErrNotCached
|
||||||
}
|
}
|
||||||
|
if isStale {
|
||||||
|
if options.DisableOptimisticCache {
|
||||||
|
return nil, ErrNotCached
|
||||||
|
}
|
||||||
|
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
|
||||||
|
logOptimisticResponse(c.logger, ctx, response)
|
||||||
|
}
|
||||||
if response.Rcode != dns.RcodeSuccess {
|
if response.Rcode != dns.RcodeSuccess {
|
||||||
return nil, RcodeError(response.Rcode)
|
return nil, RcodeError(response.Rcode)
|
||||||
}
|
}
|
||||||
return MessageToAddresses(response), nil
|
return MessageToAddresses(response), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
||||||
var (
|
if c.dnsCache != nil {
|
||||||
response *dns.Msg
|
return c.loadPersistentResponse(question, transport)
|
||||||
loaded bool
|
|
||||||
)
|
|
||||||
if c.disableExpire {
|
|
||||||
if !c.independentCache {
|
|
||||||
response, loaded = c.cache.Get(question)
|
|
||||||
} else {
|
|
||||||
response, loaded = c.transportCache.Get(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
return response.Copy(), 0
|
|
||||||
} else {
|
|
||||||
var expireAt time.Time
|
|
||||||
if !c.independentCache {
|
|
||||||
response, expireAt, loaded = c.cache.GetWithLifetime(question)
|
|
||||||
} else {
|
|
||||||
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
timeNow := time.Now()
|
|
||||||
if timeNow.After(expireAt) {
|
|
||||||
if !c.independentCache {
|
|
||||||
c.cache.Remove(question)
|
|
||||||
} else {
|
|
||||||
c.transportCache.Remove(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
var originTTL int
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
|
||||||
originTTL = int(record.Header().Ttl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
|
||||||
if nowTTL < 0 {
|
|
||||||
nowTTL = 0
|
|
||||||
}
|
|
||||||
response = response.Copy()
|
|
||||||
if originTTL > 0 {
|
|
||||||
duration := uint32(originTTL - nowTTL)
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
record.Header().Ttl = record.Header().Ttl - duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
record.Header().Ttl = uint32(nowTTL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response, nowTTL
|
|
||||||
}
|
}
|
||||||
|
if c.cache == nil {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||||
|
if c.disableExpire {
|
||||||
|
response, loaded := c.cache.Get(key)
|
||||||
|
if !loaded {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
return response.Copy(), 0, false
|
||||||
|
}
|
||||||
|
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
|
||||||
|
if !loaded {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
timeNow := time.Now()
|
||||||
|
if timeNow.After(expireAt) {
|
||||||
|
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
||||||
|
response = response.Copy()
|
||||||
|
normalizeTTL(response, 1)
|
||||||
|
return response, 0, true
|
||||||
|
}
|
||||||
|
c.cache.Remove(key)
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||||
|
if nowTTL < 0 {
|
||||||
|
nowTTL = 0
|
||||||
|
}
|
||||||
|
response = response.Copy()
|
||||||
|
normalizeTTL(response, uint32(nowTTL))
|
||||||
|
return response, nowTTL, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
||||||
|
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
|
||||||
|
if !loaded {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
response := new(dns.Msg)
|
||||||
|
err := response.Unpack(rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
if c.disableExpire {
|
||||||
|
return response, 0, false
|
||||||
|
}
|
||||||
|
timeNow := time.Now()
|
||||||
|
if timeNow.After(expireAt) {
|
||||||
|
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
||||||
|
normalizeTTL(response, 1)
|
||||||
|
return response, 0, true
|
||||||
|
}
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||||
|
if nowTTL < 0 {
|
||||||
|
nowTTL = 0
|
||||||
|
}
|
||||||
|
normalizeTTL(response, uint32(nowTTL))
|
||||||
|
return response, nowTTL, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
|
||||||
|
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
|
||||||
|
for _, rr := range response.Answer {
|
||||||
|
https, isHTTPS := rr.(*dns.HTTPS)
|
||||||
|
if !isHTTPS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content := https.SVCB
|
||||||
|
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||||
|
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||||
|
return it.Key() != dns.SVCB_IPV6HINT
|
||||||
|
}
|
||||||
|
return it.Key() != dns.SVCB_IPV4HINT
|
||||||
|
})
|
||||||
|
https.SVCB = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timeToLive := computeTimeToLive(response)
|
||||||
|
if options.RewriteTTL != nil {
|
||||||
|
timeToLive = *options.RewriteTTL
|
||||||
|
}
|
||||||
|
normalizeTTL(response, timeToLive)
|
||||||
|
return timeToLive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
|
||||||
|
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||||
|
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
|
||||||
|
if loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer c.backgroundRefresh.Delete(key)
|
||||||
|
ctx := contextWithTransportTag(c.ctx, transport.Tag())
|
||||||
|
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||||
|
if err != nil {
|
||||||
|
if c.logger != nil {
|
||||||
|
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if responseChecker != nil {
|
||||||
|
var rejected bool
|
||||||
|
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||||
|
rejected = true
|
||||||
|
} else {
|
||||||
|
rejected = !responseChecker(response)
|
||||||
|
}
|
||||||
|
if rejected {
|
||||||
|
if c.rdrc != nil {
|
||||||
|
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeToLive := applyResponseOptions(question, response, options)
|
||||||
|
c.storeCache(transport, question, response, timeToLive)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
|
||||||
|
clientSubnet := options.ClientSubnet
|
||||||
|
if !clientSubnet.IsValid() {
|
||||||
|
clientSubnet = c.clientSubnet
|
||||||
|
}
|
||||||
|
if clientSubnet.IsValid() {
|
||||||
|
message = SetClientSubnet(message, clientSubnet)
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
|
defer cancel()
|
||||||
|
response, err := transport.Exchange(ctx, message)
|
||||||
|
if err == nil {
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
var rcodeError RcodeError
|
||||||
|
if errors.As(err, &rcodeError) {
|
||||||
|
return FixedResponseStatus(message, int(rcodeError)), nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
return adapter.DNSResponseAddresses(response)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapError(err error) error {
|
func wrapError(err error) error {
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
||||||
|
if logger == nil || len(response.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
domain := FqdnToDomain(response.Question[0].Name)
|
||||||
|
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
||||||
if logger == nil || len(response.Question) == 0 {
|
if logger == nil || len(response.Question) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
RcodeServerFailure RcodeError = mDNS.RcodeServerFailure
|
||||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||||
|
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||||
)
|
)
|
||||||
|
|
||||||
type RcodeError int
|
type RcodeError int
|
||||||
|
|||||||
111
dns/repro_test.go
Normal file
111
dns/repro_test.go
Normal 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))
|
||||||
|
}
|
||||||
847
dns/router.go
847
dns/router.go
File diff suppressed because it is too large
Load Diff
2547
dns/router_test.go
Normal file
2547
dns/router_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,12 @@ type contextKeyConnecting struct{}
|
|||||||
|
|
||||||
var errRecursiveConnectorDial = E.New("recursive connector dial")
|
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) {
|
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||||
var zero T
|
var zero T
|
||||||
for {
|
for {
|
||||||
@@ -100,41 +106,37 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
|||||||
return zero, err
|
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()
|
c.access.Unlock()
|
||||||
|
|
||||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
go func() {
|
||||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
connection, cancel, err := c.dialWithCancellation(dialContext)
|
||||||
|
dialResult <- connectorDialResult[T]{
|
||||||
|
connection: connection,
|
||||||
|
cancel: cancel,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
c.access.Lock()
|
select {
|
||||||
close(c.connecting)
|
case result := <-dialResult:
|
||||||
c.connecting = nil
|
return c.completeDial(ctx, connecting, result)
|
||||||
|
case <-ctx.Done():
|
||||||
if err != nil {
|
go func() {
|
||||||
c.access.Unlock()
|
result := <-dialResult
|
||||||
return zero, err
|
_, _ = c.completeDial(ctx, connecting, result)
|
||||||
}
|
}()
|
||||||
|
return zero, ctx.Err()
|
||||||
if c.closed {
|
case <-c.closeCtx.Done():
|
||||||
cancel()
|
go func() {
|
||||||
c.callbacks.Close(connection)
|
result := <-dialResult
|
||||||
c.access.Unlock()
|
_, _ = c.completeDial(ctx, connecting, result)
|
||||||
|
}()
|
||||||
return zero, ErrTransportClosed
|
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
|
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) {
|
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
|
||||||
var zero T
|
var zero T
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
|
|||||||
@@ -188,13 +188,157 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
|
|||||||
err := <-result
|
err := <-result
|
||||||
require.ErrorIs(t, err, context.Canceled)
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
require.EqualValues(t, 1, dialCount.Load())
|
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())
|
_, err = connector.Get(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, 2, dialCount.Load())
|
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) {
|
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
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)
|
results := make(chan queryResult)
|
||||||
startRacer := func(ctx context.Context, fqdn string) {
|
startRacer := func(ctx context.Context, fqdn string) {
|
||||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
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 {
|
select {
|
||||||
case results <- queryResult{response, err}:
|
case results <- queryResult{response, err}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
|
|||||||
@@ -23,16 +23,25 @@ var _ adapter.FakeIPTransport = (*Transport)(nil)
|
|||||||
|
|
||||||
type Transport struct {
|
type Transport struct {
|
||||||
dns.TransportAdapter
|
dns.TransportAdapter
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
store adapter.FakeIPStore
|
store adapter.FakeIPStore
|
||||||
|
inet4Enabled bool
|
||||||
|
inet6Enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{}))
|
inet4Range := options.Inet4Range.Build(netip.Prefix{})
|
||||||
|
inet6Range := options.Inet6Range.Build(netip.Prefix{})
|
||||||
|
if !inet4Range.IsValid() && !inet6Range.IsValid() {
|
||||||
|
return nil, E.New("at least one of inet4_range or inet6_range must be set")
|
||||||
|
}
|
||||||
|
store := NewStore(ctx, logger, inet4Range, inet6Range)
|
||||||
return &Transport{
|
return &Transport{
|
||||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
store: store,
|
store: store,
|
||||||
|
inet4Enabled: inet4Range.IsValid(),
|
||||||
|
inet6Enabled: inet6Range.IsValid(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +64,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
|||||||
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
||||||
return nil, E.New("only IP queries are supported by fakeip")
|
return nil, E.New("only IP queries are supported by fakeip")
|
||||||
}
|
}
|
||||||
|
if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled {
|
||||||
|
return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil
|
||||||
|
}
|
||||||
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ package local
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
@@ -14,7 +12,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
@@ -35,10 +32,8 @@ type Transport struct {
|
|||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
hosts *hosts.File
|
hosts *hosts.File
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
preferGo bool
|
|
||||||
fallback bool
|
fallback bool
|
||||||
dhcpTransport dhcpTransport
|
dhcpTransport dhcpTransport
|
||||||
resolver net.Resolver
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dhcpTransport interface {
|
type dhcpTransport interface {
|
||||||
@@ -52,14 +47,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
|
|
||||||
return &Transport{
|
return &Transport{
|
||||||
TransportAdapter: transportAdapter,
|
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||||
dialer: transportDialer,
|
dialer: transportDialer,
|
||||||
preferGo: options.PreferGo,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,44 +90,3 @@ func (t *Transport) Reset() {
|
|||||||
t.dhcpTransport.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.")
|
|
||||||
}
|
|
||||||
|
|||||||
249
dns/transport/local/local_darwin_cgo.go
Normal file
249
dns/transport/local/local_darwin_cgo.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <dns.h>
|
||||||
|
#include <resolv.h>
|
||||||
|
|
||||||
|
static void *cgo_dns_open_super() {
|
||||||
|
return (void *)dns_open(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cgo_dns_close(void *opaque) {
|
||||||
|
if (opaque != NULL) dns_free((dns_handle_t)opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
|
||||||
|
unsigned char *answer, int anslen) {
|
||||||
|
dns_handle_t handle = (dns_handle_t)opaque;
|
||||||
|
struct sockaddr_storage from;
|
||||||
|
uint32_t fromlen = sizeof(from);
|
||||||
|
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *cgo_res_init() {
|
||||||
|
res_state state = calloc(1, sizeof(struct __res_state));
|
||||||
|
if (state == NULL) return NULL;
|
||||||
|
if (res_ninit(state) != 0) {
|
||||||
|
free(state);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cgo_res_destroy(void *opaque) {
|
||||||
|
res_state state = (res_state)opaque;
|
||||||
|
res_ndestroy(state);
|
||||||
|
free(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
|
||||||
|
unsigned char *answer, int anslen,
|
||||||
|
int timeout_seconds,
|
||||||
|
int *out_h_errno) {
|
||||||
|
res_state state = (res_state)opaque;
|
||||||
|
state->retrans = timeout_seconds;
|
||||||
|
state->retry = 1;
|
||||||
|
int n = res_nsearch(state, dname, class, type, answer, anslen);
|
||||||
|
if (n < 0) {
|
||||||
|
*out_h_errno = state->res_h_errno;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
boxC "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
darwinResolverHostNotFound = 1
|
||||||
|
darwinResolverTryAgain = 2
|
||||||
|
darwinResolverNoRecovery = 3
|
||||||
|
darwinResolverNoData = 4
|
||||||
|
|
||||||
|
darwinResolverMaxPacketSize = 65535
|
||||||
|
)
|
||||||
|
|
||||||
|
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
|
||||||
|
|
||||||
|
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||||
|
response, err := darwinSearchWithSystemRouting(name, class, qtype)
|
||||||
|
if err == nil {
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
|
||||||
|
if fallbackErr == nil || fallbackResponse != nil {
|
||||||
|
return fallbackResponse, fallbackErr
|
||||||
|
}
|
||||||
|
return nil, E.Errors(
|
||||||
|
E.Cause(err, "dns_search"),
|
||||||
|
E.Cause(fallbackErr, "res_nsearch"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
|
||||||
|
handle := C.cgo_dns_open_super()
|
||||||
|
if handle == nil {
|
||||||
|
return nil, E.New("dns_open failed")
|
||||||
|
}
|
||||||
|
defer C.cgo_dns_close(handle)
|
||||||
|
|
||||||
|
cName := C.CString(name)
|
||||||
|
defer C.free(unsafe.Pointer(cName))
|
||||||
|
|
||||||
|
bufSize := 1232
|
||||||
|
for {
|
||||||
|
answer := make([]byte, bufSize)
|
||||||
|
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
|
||||||
|
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
|
||||||
|
if n <= 0 {
|
||||||
|
return nil, E.New("dns_search failed for ", name)
|
||||||
|
}
|
||||||
|
if int(n) > bufSize {
|
||||||
|
bufSize = int(n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||||
|
state := C.cgo_res_init()
|
||||||
|
if state == nil {
|
||||||
|
return nil, E.New("res_ninit failed")
|
||||||
|
}
|
||||||
|
defer C.cgo_res_destroy(state)
|
||||||
|
|
||||||
|
cName := C.CString(name)
|
||||||
|
defer C.free(unsafe.Pointer(cName))
|
||||||
|
|
||||||
|
bufSize := 1232
|
||||||
|
for {
|
||||||
|
answer := make([]byte, bufSize)
|
||||||
|
var hErrno C.int
|
||||||
|
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
|
||||||
|
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
|
||||||
|
C.int(timeoutSeconds),
|
||||||
|
&hErrno)
|
||||||
|
if n >= 0 {
|
||||||
|
if int(n) > bufSize {
|
||||||
|
bufSize = int(n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
|
||||||
|
}
|
||||||
|
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
|
||||||
|
if err == nil {
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
|
||||||
|
bufSize *= 2
|
||||||
|
if bufSize > darwinResolverMaxPacketSize {
|
||||||
|
bufSize = darwinResolverMaxPacketSize
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
|
||||||
|
var response mDNS.Msg
|
||||||
|
err := response.Unpack(packet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "unpack ", source, " response")
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
|
||||||
|
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
|
||||||
|
if err == nil && response.Response {
|
||||||
|
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
|
||||||
|
return nil, errDarwinNeedLargerBuffer
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
return nil, darwinResolverHErrno(name, hErrno)
|
||||||
|
}
|
||||||
|
|
||||||
|
func darwinResolverHErrno(name string, hErrno int) error {
|
||||||
|
switch hErrno {
|
||||||
|
case darwinResolverHostNotFound:
|
||||||
|
return dns.RcodeNameError
|
||||||
|
case darwinResolverTryAgain:
|
||||||
|
return dns.RcodeServerFailure
|
||||||
|
case darwinResolverNoRecovery:
|
||||||
|
return dns.RcodeServerFailure
|
||||||
|
case darwinResolverNoData:
|
||||||
|
return dns.RcodeSuccess
|
||||||
|
default:
|
||||||
|
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
question := message.Question[0]
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
||||||
|
if len(addresses) > 0 {
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.fallback && t.dhcpTransport != nil {
|
||||||
|
dhcpServers := t.dhcpTransport.Fetch()
|
||||||
|
if len(dhcpServers) > 0 {
|
||||||
|
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name := question.Name
|
||||||
|
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
|
||||||
|
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining <= 0 {
|
||||||
|
return nil, context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
seconds := int(remaining.Seconds())
|
||||||
|
if seconds < 1 {
|
||||||
|
seconds = 1
|
||||||
|
}
|
||||||
|
timeoutSeconds = seconds
|
||||||
|
}
|
||||||
|
type resolvResult struct {
|
||||||
|
response *mDNS.Msg
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultCh := make(chan resolvResult, 1)
|
||||||
|
go func() {
|
||||||
|
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||||
|
resultCh <- resolvResult{response, err}
|
||||||
|
}()
|
||||||
|
var result resolvResult
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case result = <-resultCh:
|
||||||
|
}
|
||||||
|
if result.err != nil {
|
||||||
|
var rcodeError dns.RcodeError
|
||||||
|
if errors.As(result.err, &rcodeError) {
|
||||||
|
return dns.FixedResponseStatus(message, int(rcodeError)), nil
|
||||||
|
}
|
||||||
|
return nil, result.err
|
||||||
|
}
|
||||||
|
result.response.Id = message.Id
|
||||||
|
return result.response, nil
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
|
||||||
package local
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,7 +9,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -49,13 +50,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
|||||||
results := make(chan queryResult)
|
results := make(chan queryResult)
|
||||||
startRacer := func(ctx context.Context, fqdn string) {
|
startRacer := func(ctx context.Context, fqdn string) {
|
||||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
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 {
|
select {
|
||||||
case results <- queryResult{response, err}:
|
case results <- queryResult{response, err}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
|
|
||||||
|
|
||||||
type TransportAdapter struct {
|
type TransportAdapter struct {
|
||||||
transportType string
|
transportType string
|
||||||
transportTag string
|
transportTag string
|
||||||
dependencies []string
|
dependencies []string
|
||||||
strategy C.DomainStrategy
|
|
||||||
clientSubnet netip.Prefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
||||||
@@ -35,8 +27,6 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
|
|||||||
transportType: transportType,
|
transportType: transportType,
|
||||||
transportTag: transportTag,
|
transportTag: transportTag,
|
||||||
dependencies: dependencies,
|
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 != "" {
|
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
||||||
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
||||||
}
|
}
|
||||||
if remoteOptions.LegacyAddressResolver != "" {
|
|
||||||
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
|
|
||||||
}
|
|
||||||
return TransportAdapter{
|
return TransportAdapter{
|
||||||
transportType: transportType,
|
transportType: transportType,
|
||||||
transportTag: transportTag,
|
transportTag: transportTag,
|
||||||
dependencies: dependencies,
|
dependencies: dependencies,
|
||||||
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
|
|
||||||
clientSubnet: remoteOptions.LegacyClientSubnet,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +53,3 @@ func (a *TransportAdapter) Tag() string {
|
|||||||
func (a *TransportAdapter) Dependencies() []string {
|
func (a *TransportAdapter) Dependencies() []string {
|
||||||
return a.dependencies
|
return a.dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
|
||||||
return a.strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
|
|
||||||
return a.clientSubnet
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,104 +2,25 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"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"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
||||||
if options.LegacyDefaultDialer {
|
return dialer.NewWithOptions(dialer.Options{
|
||||||
return dialer.NewDefaultOutbound(ctx), nil
|
Context: ctx,
|
||||||
} else {
|
Options: options.DialerOptions,
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
DirectResolver: true,
|
||||||
Context: ctx,
|
})
|
||||||
Options: options.DialerOptions,
|
|
||||||
DirectResolver: true,
|
|
||||||
LegacyDNSDialer: options.Legacy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
||||||
if options.LegacyDefaultDialer {
|
return dialer.NewWithOptions(dialer.Options{
|
||||||
transportDialer := dialer.NewDefaultOutbound(ctx)
|
Context: ctx,
|
||||||
if options.LegacyAddressResolver != "" {
|
Options: options.DialerOptions,
|
||||||
transport := service.FromContext[adapter.DNSTransportManager](ctx)
|
RemoteIsDomain: options.ServerIsDomain(),
|
||||||
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
|
DirectResolver: true,
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,220 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.12
|
||||||
|
|
||||||
|
* Fix fake-ip DNS server should return SUCCESS when another address type is not configured
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.8
|
||||||
|
|
||||||
|
* Update naiveproxy to v147.0.7727.49-1
|
||||||
|
* Fix fake-ip DNS server should return SUCCESS when another address type is not configured
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.11
|
||||||
|
|
||||||
|
* Add optimistic DNS cache **1**
|
||||||
|
* Update NaiveProxy to 147.0.7727.49
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Optimistic DNS cache returns an expired cached response immediately while
|
||||||
|
refreshing it in the background, reducing tail latency for repeated
|
||||||
|
queries. Enabled via [`optimistic`](/configuration/dns/#optimistic)
|
||||||
|
in DNS options, and can be persisted across restarts with the new
|
||||||
|
[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache
|
||||||
|
file option. A per-query
|
||||||
|
[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache)
|
||||||
|
field is also available on DNS rule actions and the `resolve` route rule
|
||||||
|
action.
|
||||||
|
|
||||||
|
This deprecates the `independent_cache` DNS option (the DNS cache now
|
||||||
|
always keys by transport) and the `store_rdrc` cache file option
|
||||||
|
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
|
||||||
|
See [Migration](/migration/#migrate-independent-dns-cache).
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.10
|
||||||
|
|
||||||
|
* Add `evaluate` DNS rule action and Response Match Fields **1**
|
||||||
|
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
|
||||||
|
* Add `package_name_regex` route, DNS and headless rule item **3**
|
||||||
|
* Add cloudflared inbound **4**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Response Match Fields
|
||||||
|
([`response_rcode`](/configuration/dns/rule/#response_rcode),
|
||||||
|
[`response_answer`](/configuration/dns/rule/#response_answer),
|
||||||
|
[`response_ns`](/configuration/dns/rule/#response_ns),
|
||||||
|
and [`response_extra`](/configuration/dns/rule/#response_extra))
|
||||||
|
match the evaluated DNS response. They are gated by the new
|
||||||
|
[`match_response`](/configuration/dns/rule/#match_response) field and
|
||||||
|
populated by a preceding
|
||||||
|
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
|
||||||
|
the evaluated response can also be returned directly by a
|
||||||
|
[`respond`](/configuration/dns/rule_action/#respond) action.
|
||||||
|
|
||||||
|
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
|
||||||
|
`ip_is_private` without `match_response`) in DNS rules, the Legacy
|
||||||
|
`strategy` DNS rule action option, and the Legacy
|
||||||
|
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
|
||||||
|
in sing-box 1.16.0.
|
||||||
|
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
`ip_version` and `query_type` in DNS rules, together with `query_type` in
|
||||||
|
referenced rule-sets, now take effect on every DNS rule evaluation,
|
||||||
|
including matches from internal domain resolutions that do not target a
|
||||||
|
specific DNS server (for example a `resolve` route rule action without
|
||||||
|
`server` set). In earlier versions they were silently ignored in that
|
||||||
|
path. Combining these fields with any of the legacy DNS fields deprecated
|
||||||
|
in **1** in the same DNS configuration is no longer supported and is
|
||||||
|
rejected at startup.
|
||||||
|
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [Route Rule](/configuration/route/rule/#package_name_regex),
|
||||||
|
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
|
||||||
|
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
|
||||||
|
|
||||||
|
**4**:
|
||||||
|
|
||||||
|
See [Cloudflared](/configuration/inbound/cloudflared/).
|
||||||
|
|
||||||
|
#### 1.13.7
|
||||||
|
|
||||||
|
* Fixes and improvement
|
||||||
|
|
||||||
|
#### 1.13.6
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.8
|
||||||
|
|
||||||
|
* Add BBR profile and hop interval randomization for Hysteria2 **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.8
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.5
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.7
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.4
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.4
|
||||||
|
|
||||||
|
* Refactor ACME support to certificate provider system **1**
|
||||||
|
* Add Cloudflare Origin CA certificate provider **2**
|
||||||
|
* Add Tailscale certificate provider **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
|
||||||
|
|
||||||
|
#### 1.13.3
|
||||||
|
|
||||||
|
* Add OpenWrt and Alpine APK packages to release **1**
|
||||||
|
* Backport to macOS 10.13 High Sierra **2**
|
||||||
|
* OCM service: Add WebSocket support for Responses API **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||||
|
|
||||||
|
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||||
|
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||||
|
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||||
|
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [OCM](/configuration/service/ocm).
|
||||||
|
|
||||||
|
#### 1.12.24
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.2
|
||||||
|
|
||||||
|
* Add OpenWrt and Alpine APK packages to release **1**
|
||||||
|
* Backport to macOS 10.13 High Sierra **2**
|
||||||
|
* OCM service: Add WebSocket support for Responses API **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||||
|
|
||||||
|
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||||
|
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||||
|
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||||
|
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [OCM](/configuration/service/ocm).
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.1
|
||||||
|
|
||||||
|
* Add `source_mac_address` and `source_hostname` rule items **1**
|
||||||
|
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
|
||||||
|
* Update NaiveProxy to 145.0.7632.159 **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
|
||||||
|
Supported on Linux, macOS, or in graphical clients on Android and macOS.
|
||||||
|
|
||||||
|
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Limit or exclude devices from TUN routing by MAC address.
|
||||||
|
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||||
|
|
||||||
|
See [TUN](/configuration/inbound/tun/#include_mac_address).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
|
||||||
|
|
||||||
#### 1.13.2
|
#### 1.13.2
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
@@ -623,7 +837,7 @@ DNS servers are refactored for better performance and scalability.
|
|||||||
|
|
||||||
See [DNS server](/configuration/dns/server/).
|
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.
|
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||||
|
|
||||||
@@ -1093,7 +1307,7 @@ DNS servers are refactored for better performance and scalability.
|
|||||||
|
|
||||||
See [DNS server](/configuration/dns/server/).
|
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.
|
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||||
|
|
||||||
@@ -1929,7 +2143,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
|
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.
|
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.
|
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||||
|
|
||||||
@@ -1943,7 +2157,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users
|
|||||||
**5**:
|
**5**:
|
||||||
|
|
||||||
The new feature allows you to cache the check results of
|
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**:
|
**6**:
|
||||||
|
|
||||||
@@ -2124,7 +2338,7 @@ See [TUN](/configuration/inbound/tun) inbound.
|
|||||||
**1**:
|
**1**:
|
||||||
|
|
||||||
The new feature allows you to cache the check results of
|
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
|
#### 1.9.0-alpha.7
|
||||||
|
|
||||||
@@ -2171,7 +2385,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
|
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.
|
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.
|
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|
|||||||
| `process_path` | :material-close: | No permission |
|
| `process_path` | :material-close: | No permission |
|
||||||
| `process_path_regex` | :material-close: | No permission |
|
| `process_path_regex` | :material-close: | No permission |
|
||||||
| `package_name` | :material-check: | / |
|
| `package_name` | :material-check: | / |
|
||||||
|
| `package_name_regex` | :material-check: | / |
|
||||||
| `user` | :material-close: | Use `package_name` instead |
|
| `user` | :material-close: | Use `package_name` instead |
|
||||||
| `user_id` | :material-close: | Use `package_name` instead |
|
| `user_id` | :material-close: | Use `package_name` instead |
|
||||||
| `wifi_ssid` | :material-check: | Fine location permission required |
|
| `wifi_ssid` | :material-check: | Fine location permission required |
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
|
|||||||
| `process_path` | :material-close: | No permission |
|
| `process_path` | :material-close: | No permission |
|
||||||
| `process_path_regex` | :material-close: | No permission |
|
| `process_path_regex` | :material-close: | No permission |
|
||||||
| `package_name` | :material-close: | / |
|
| `package_name` | :material-close: | / |
|
||||||
|
| `package_name_regex` | :material-close: | / |
|
||||||
| `user` | :material-close: | No permission |
|
| `user` | :material-close: | No permission |
|
||||||
| `user_id` | :material-close: | No permission |
|
| `user_id` | :material-close: | No permission |
|
||||||
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
||||||
|
|||||||
@@ -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
|
### Structure
|
||||||
|
|
||||||
@@ -26,6 +26,6 @@ Enable FakeIP service.
|
|||||||
|
|
||||||
IPv4 address range for FakeIP.
|
IPv4 address range for FakeIP.
|
||||||
|
|
||||||
#### inet6_address
|
#### inet6_range
|
||||||
|
|
||||||
IPv6 address range for FakeIP.
|
IPv6 address range for FakeIP.
|
||||||
|
|||||||
@@ -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-服务器格式)。
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.14.0"
|
||||||
|
|
||||||
|
:material-delete-clock: [independent_cache](#independent_cache)
|
||||||
|
:material-plus: [optimistic](#optimistic)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-decagram: [servers](#servers)
|
:material-decagram: [servers](#servers)
|
||||||
@@ -25,6 +30,7 @@ icon: material/alert-decagram
|
|||||||
"disable_expire": false,
|
"disable_expire": false,
|
||||||
"independent_cache": false,
|
"independent_cache": false,
|
||||||
"cache_capacity": 0,
|
"cache_capacity": 0,
|
||||||
|
"optimistic": false, // or {}
|
||||||
"reverse_mapping": false,
|
"reverse_mapping": false,
|
||||||
"client_subnet": "",
|
"client_subnet": "",
|
||||||
"fakeip": {}
|
"fakeip": {}
|
||||||
@@ -39,7 +45,7 @@ icon: material/alert-decagram
|
|||||||
|----------|---------------------------------|
|
|----------|---------------------------------|
|
||||||
| `server` | List of [DNS Server](./server/) |
|
| `server` | List of [DNS Server](./server/) |
|
||||||
| `rules` | List of [DNS Rule](./rule/) |
|
| `rules` | List of [DNS Rule](./rule/) |
|
||||||
| `fakeip` | [FakeIP](./fakeip/) |
|
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
|
||||||
|
|
||||||
#### final
|
#### final
|
||||||
|
|
||||||
@@ -57,12 +63,20 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
|||||||
|
|
||||||
Disable dns cache.
|
Disable dns cache.
|
||||||
|
|
||||||
|
Conflict with `optimistic`.
|
||||||
|
|
||||||
#### disable_expire
|
#### disable_expire
|
||||||
|
|
||||||
Disable dns cache expire.
|
Disable dns cache expire.
|
||||||
|
|
||||||
|
Conflict with `optimistic`.
|
||||||
|
|
||||||
#### independent_cache
|
#### independent_cache
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.14.0"
|
||||||
|
|
||||||
|
`independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache).
|
||||||
|
|
||||||
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
|
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
|
||||||
|
|
||||||
#### cache_capacity
|
#### cache_capacity
|
||||||
@@ -73,6 +87,34 @@ LRU cache capacity.
|
|||||||
|
|
||||||
Value less than 1024 will be ignored.
|
Value less than 1024 will be ignored.
|
||||||
|
|
||||||
|
#### optimistic
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window,
|
||||||
|
the stale response is returned immediately while a background refresh is triggered.
|
||||||
|
|
||||||
|
Conflict with `disable_cache` and `disable_expire`.
|
||||||
|
|
||||||
|
Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"timeout": "3d"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### enabled
|
||||||
|
|
||||||
|
Enable optimistic DNS caching.
|
||||||
|
|
||||||
|
##### timeout
|
||||||
|
|
||||||
|
The maximum time an expired cache entry can be served optimistically.
|
||||||
|
|
||||||
|
`3d` is used by default.
|
||||||
|
|
||||||
#### reverse_mapping
|
#### reverse_mapping
|
||||||
|
|
||||||
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
||||||
@@ -88,4 +130,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.
|
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`.
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.14.0 中的更改"
|
||||||
|
|
||||||
|
:material-delete-clock: [independent_cache](#independent_cache)
|
||||||
|
:material-plus: [optimistic](#optimistic)
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-decagram: [servers](#servers)
|
:material-decagram: [servers](#servers)
|
||||||
@@ -25,6 +30,7 @@ icon: material/alert-decagram
|
|||||||
"disable_expire": false,
|
"disable_expire": false,
|
||||||
"independent_cache": false,
|
"independent_cache": false,
|
||||||
"cache_capacity": 0,
|
"cache_capacity": 0,
|
||||||
|
"optimistic": false, // or {}
|
||||||
"reverse_mapping": false,
|
"reverse_mapping": false,
|
||||||
"client_subnet": "",
|
"client_subnet": "",
|
||||||
"fakeip": {}
|
"fakeip": {}
|
||||||
@@ -56,12 +62,20 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
禁用 DNS 缓存。
|
禁用 DNS 缓存。
|
||||||
|
|
||||||
|
与 `optimistic` 冲突。
|
||||||
|
|
||||||
#### disable_expire
|
#### disable_expire
|
||||||
|
|
||||||
禁用 DNS 缓存过期。
|
禁用 DNS 缓存过期。
|
||||||
|
|
||||||
|
与 `optimistic` 冲突。
|
||||||
|
|
||||||
#### independent_cache
|
#### independent_cache
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||||
|
|
||||||
|
`independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
|
||||||
|
|
||||||
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
||||||
|
|
||||||
#### cache_capacity
|
#### cache_capacity
|
||||||
@@ -72,6 +86,34 @@ LRU 缓存容量。
|
|||||||
|
|
||||||
小于 1024 的值将被忽略。
|
小于 1024 的值将被忽略。
|
||||||
|
|
||||||
|
#### optimistic
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时,
|
||||||
|
立即返回过期的响应,同时在后台触发刷新。
|
||||||
|
|
||||||
|
与 `disable_cache` 和 `disable_expire` 冲突。
|
||||||
|
|
||||||
|
接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"timeout": "3d"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### enabled
|
||||||
|
|
||||||
|
启用乐观 DNS 缓存。
|
||||||
|
|
||||||
|
##### timeout
|
||||||
|
|
||||||
|
过期缓存条目可被乐观提供的最长时间。
|
||||||
|
|
||||||
|
默认使用 `3d`。
|
||||||
|
|
||||||
#### reverse_mapping
|
#### reverse_mapping
|
||||||
|
|
||||||
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||||
@@ -88,6 +130,6 @@ LRU 缓存容量。
|
|||||||
|
|
||||||
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
||||||
|
|
||||||
#### fakeip
|
#### fakeip :material-note-remove:
|
||||||
|
|
||||||
[FakeIP](./fakeip/) 设置。
|
[FakeIP](./fakeip/) 设置。
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
icon: material/alert-decagram
|
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"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
:material-plus: [interface_address](#interface_address)
|
:material-plus: [interface_address](#interface_address)
|
||||||
@@ -89,12 +103,6 @@ icon: material/alert-decagram
|
|||||||
"192.168.0.1"
|
"192.168.0.1"
|
||||||
],
|
],
|
||||||
"source_ip_is_private": false,
|
"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": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -124,6 +132,9 @@ icon: material/alert-decagram
|
|||||||
"package_name": [
|
"package_name": [
|
||||||
"com.termux"
|
"com.termux"
|
||||||
],
|
],
|
||||||
|
"package_name_regex": [
|
||||||
|
"^com\\.termux.*"
|
||||||
|
],
|
||||||
"user": [
|
"user": [
|
||||||
"sekai"
|
"sekai"
|
||||||
],
|
],
|
||||||
@@ -149,6 +160,12 @@ icon: material/alert-decagram
|
|||||||
"default_interface_address": [
|
"default_interface_address": [
|
||||||
"2000::/3"
|
"2000::/3"
|
||||||
],
|
],
|
||||||
|
"source_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"source_hostname": [
|
||||||
|
"my-device"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -160,7 +177,17 @@ icon: material/alert-decagram
|
|||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
],
|
],
|
||||||
"rule_set_ip_cidr_match_source": false,
|
"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,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -170,6 +197,7 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
|
|
||||||
|
"rule_set_ip_cidr_accept_empty": false,
|
||||||
"rule_set_ipcidr_match_source": false,
|
"rule_set_ipcidr_match_source": false,
|
||||||
"geosite": [
|
"geosite": [
|
||||||
"cn"
|
"cn"
|
||||||
@@ -209,7 +237,7 @@ icon: material/alert-decagram
|
|||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`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
|
#### inbound
|
||||||
|
|
||||||
@@ -217,12 +245,46 @@ Tags of [Inbound](/configuration/inbound/).
|
|||||||
|
|
||||||
#### ip_version
|
#### 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).
|
4 (A DNS query) or 6 (AAAA DNS query).
|
||||||
|
|
||||||
Not limited if empty.
|
Not limited if empty.
|
||||||
|
|
||||||
#### query_type
|
#### 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.
|
DNS query type. Values can be integers or type name strings.
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
@@ -325,6 +387,12 @@ Match process path using regular expression.
|
|||||||
|
|
||||||
Match android package name.
|
Match android package name.
|
||||||
|
|
||||||
|
#### package_name_regex
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
Match android package name using regular expression.
|
||||||
|
|
||||||
#### user
|
#### user
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
@@ -408,6 +476,26 @@ Matches network interface (same values as `network_type`) address.
|
|||||||
|
|
||||||
Match default interface 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
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! 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.
|
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
|
||||||
|
|
||||||
Invert match result.
|
Invert match result.
|
||||||
@@ -490,7 +597,12 @@ See [DNS Rule Actions](../rule_action/) for details.
|
|||||||
|
|
||||||
Moved to [DNS Rule Action](../rule_action#route).
|
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.
|
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.
|
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
|
#### ip_is_private
|
||||||
|
|
||||||
!!! question "Since sing-box 1.9.0"
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
Match private IP with query response.
|
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
|
#### rule_set_ip_cidr_accept_empty
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! 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.
|
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
|
### Logical Fields
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
icon: material/alert-decagram
|
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 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [interface_address](#interface_address)
|
:material-plus: [interface_address](#interface_address)
|
||||||
@@ -89,12 +103,6 @@ icon: material/alert-decagram
|
|||||||
"192.168.0.1"
|
"192.168.0.1"
|
||||||
],
|
],
|
||||||
"source_ip_is_private": false,
|
"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": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -124,6 +132,9 @@ icon: material/alert-decagram
|
|||||||
"package_name": [
|
"package_name": [
|
||||||
"com.termux"
|
"com.termux"
|
||||||
],
|
],
|
||||||
|
"package_name_regex": [
|
||||||
|
"^com\\.termux.*"
|
||||||
|
],
|
||||||
"user": [
|
"user": [
|
||||||
"sekai"
|
"sekai"
|
||||||
],
|
],
|
||||||
@@ -149,6 +160,12 @@ icon: material/alert-decagram
|
|||||||
"default_interface_address": [
|
"default_interface_address": [
|
||||||
"2000::/3"
|
"2000::/3"
|
||||||
],
|
],
|
||||||
|
"source_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"source_hostname": [
|
||||||
|
"my-device"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -160,7 +177,17 @@ icon: material/alert-decagram
|
|||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
],
|
],
|
||||||
"rule_set_ip_cidr_match_source": false,
|
"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,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -169,6 +196,8 @@ icon: material/alert-decagram
|
|||||||
"server": "local",
|
"server": "local",
|
||||||
|
|
||||||
// 已弃用
|
// 已弃用
|
||||||
|
|
||||||
|
"rule_set_ip_cidr_accept_empty": false,
|
||||||
"rule_set_ipcidr_match_source": false,
|
"rule_set_ipcidr_match_source": false,
|
||||||
"geosite": [
|
"geosite": [
|
||||||
"cn"
|
"cn"
|
||||||
@@ -208,7 +237,7 @@ icon: material/alert-decagram
|
|||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
@@ -216,12 +245,38 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
#### ip_version
|
#### 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 查询)。
|
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
||||||
|
|
||||||
默认不限制。
|
默认不限制。
|
||||||
|
|
||||||
#### query_type
|
#### 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 查询类型。值可以为整数或者类型名称字符串。
|
DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
@@ -256,7 +311,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
!!! 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。
|
匹配 Geosite。
|
||||||
|
|
||||||
@@ -264,7 +319,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
!!! 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。
|
匹配源 GeoIP。
|
||||||
|
|
||||||
@@ -324,6 +379,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
匹配 Android 应用包名。
|
匹配 Android 应用包名。
|
||||||
|
|
||||||
|
#### package_name_regex
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
使用正则表达式匹配 Android 应用包名。
|
||||||
|
|
||||||
#### user
|
#### user
|
||||||
|
|
||||||
!!! quote ""
|
!!! 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
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
@@ -445,6 +526,23 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
使规则集中的 `ip_cidr` 规则匹配源 IP。
|
使规则集中的 `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
|
#### invert
|
||||||
|
|
||||||
反选匹配结果。
|
反选匹配结果。
|
||||||
@@ -453,7 +551,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
!!! 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).
|
已移动到 [DNS 规则动作](../rule_action#route).
|
||||||
|
|
||||||
### 地址筛选字段
|
### 旧版地址筛选字段
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||||
|
|
||||||
|
旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除,
|
||||||
|
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||||
|
|
||||||
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
||||||
|
|
||||||
@@ -505,7 +608,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
!!! 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。
|
与查询响应匹配 GeoIP。
|
||||||
@@ -516,24 +619,62 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
与查询响应匹配 IP CIDR。
|
与查询响应匹配 IP CIDR。
|
||||||
|
|
||||||
|
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
||||||
|
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||||
|
|
||||||
#### ip_is_private
|
#### ip_is_private
|
||||||
|
|
||||||
!!! question "自 sing-box 1.9.0 起"
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
与查询响应匹配非公开 IP。
|
与查询响应匹配非公开 IP。
|
||||||
|
|
||||||
#### ip_accept_any
|
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
||||||
|
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
|
||||||
|
|
||||||
匹配任意 IP。
|
|
||||||
|
|
||||||
#### rule_set_ip_cidr_accept_empty
|
#### rule_set_ip_cidr_accept_empty
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! 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` 规则接受空查询响应。
|
使规则集中的 `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
|
#### type
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.14.0"
|
||||||
|
|
||||||
|
:material-delete-clock: [strategy](#strategy)
|
||||||
|
:material-plus: [evaluate](#evaluate)
|
||||||
|
:material-plus: [respond](#respond)
|
||||||
|
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [strategy](#strategy)
|
:material-plus: [strategy](#strategy)
|
||||||
@@ -17,6 +24,7 @@ icon: material/new-box
|
|||||||
"server": "",
|
"server": "",
|
||||||
"strategy": "",
|
"strategy": "",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
|
"disable_optimistic_cache": false,
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
@@ -34,6 +42,10 @@ Tag of target server.
|
|||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! 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.
|
Set domain strategy for this query.
|
||||||
|
|
||||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||||
@@ -42,6 +54,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
|||||||
|
|
||||||
Disable cache and save cache in this query.
|
Disable cache and save cache in this query.
|
||||||
|
|
||||||
|
#### disable_optimistic_cache
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
Disable optimistic DNS caching in this query.
|
||||||
|
|
||||||
#### rewrite_ttl
|
#### rewrite_ttl
|
||||||
|
|
||||||
Rewrite TTL in DNS responses.
|
Rewrite TTL in DNS responses.
|
||||||
@@ -52,7 +70,75 @@ 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.
|
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,
|
||||||
|
"disable_optimistic_cache": false,
|
||||||
|
"rewrite_ttl": null,
|
||||||
|
"client_subnet": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules
|
||||||
|
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
|
||||||
|
Unlike `route`, it does **not** terminate rule evaluation.
|
||||||
|
|
||||||
|
Only allowed on top-level DNS rules (not inside logical sub-rules).
|
||||||
|
Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields
|
||||||
|
require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action
|
||||||
|
does not satisfy this requirement, because matching happens before the action runs.
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Tag of target server.
|
||||||
|
|
||||||
|
#### disable_cache
|
||||||
|
|
||||||
|
Disable cache and save cache in this query.
|
||||||
|
|
||||||
|
#### disable_optimistic_cache
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
Disable optimistic DNS caching in this query.
|
||||||
|
|
||||||
|
#### rewrite_ttl
|
||||||
|
|
||||||
|
Rewrite TTL in DNS responses.
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||||
|
|
||||||
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
|
Will override `dns.client_subnet`.
|
||||||
|
|
||||||
|
### respond
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "respond"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action.
|
||||||
|
|
||||||
|
This action does not send a new DNS query and has no extra options.
|
||||||
|
|
||||||
|
Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules.
|
||||||
|
|
||||||
### route-options
|
### route-options
|
||||||
|
|
||||||
@@ -60,6 +146,7 @@ Will overrides `dns.client_subnet`.
|
|||||||
{
|
{
|
||||||
"action": "route-options",
|
"action": "route-options",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
|
"disable_optimistic_cache": false,
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.14.0 中的更改"
|
||||||
|
|
||||||
|
:material-delete-clock: [strategy](#strategy)
|
||||||
|
:material-plus: [evaluate](#evaluate)
|
||||||
|
:material-plus: [respond](#respond)
|
||||||
|
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [strategy](#strategy)
|
:material-plus: [strategy](#strategy)
|
||||||
@@ -17,6 +24,7 @@ icon: material/new-box
|
|||||||
"server": "",
|
"server": "",
|
||||||
"strategy": "",
|
"strategy": "",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
|
"disable_optimistic_cache": false,
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
@@ -34,6 +42,10 @@ icon: material/new-box
|
|||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! 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`。
|
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||||
@@ -42,6 +54,12 @@ icon: material/new-box
|
|||||||
|
|
||||||
在此查询中禁用缓存。
|
在此查询中禁用缓存。
|
||||||
|
|
||||||
|
#### disable_optimistic_cache
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
在此查询中禁用乐观 DNS 缓存。
|
||||||
|
|
||||||
#### rewrite_ttl
|
#### rewrite_ttl
|
||||||
|
|
||||||
重写 DNS 回应中的 TTL。
|
重写 DNS 回应中的 TTL。
|
||||||
@@ -54,12 +72,79 @@ icon: material/new-box
|
|||||||
|
|
||||||
将覆盖 `dns.client_subnet`.
|
将覆盖 `dns.client_subnet`.
|
||||||
|
|
||||||
|
### evaluate
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "evaluate",
|
||||||
|
"server": "",
|
||||||
|
"disable_cache": false,
|
||||||
|
"disable_optimistic_cache": false,
|
||||||
|
"rewrite_ttl": null,
|
||||||
|
"client_subnet": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。
|
||||||
|
|
||||||
|
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
|
||||||
|
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
|
||||||
|
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
|
||||||
|
因为匹配发生在动作执行之前。
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
目标 DNS 服务器的标签。
|
||||||
|
|
||||||
|
#### disable_cache
|
||||||
|
|
||||||
|
在此查询中禁用缓存。
|
||||||
|
|
||||||
|
#### disable_optimistic_cache
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
在此查询中禁用乐观 DNS 缓存。
|
||||||
|
|
||||||
|
#### rewrite_ttl
|
||||||
|
|
||||||
|
重写 DNS 回应中的 TTL。
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||||
|
|
||||||
|
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||||
|
|
||||||
|
将覆盖 `dns.client_subnet`.
|
||||||
|
|
||||||
|
### respond
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "respond"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
|
||||||
|
|
||||||
|
此动作不会发起新的 DNS 查询,也没有额外选项。
|
||||||
|
|
||||||
|
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
|
||||||
|
|
||||||
### route-options
|
### route-options
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "route-options",
|
"action": "route-options",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
|
"disable_optimistic_cache": false,
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
@@ -84,7 +169,7 @@ icon: material/new-box
|
|||||||
- `default`: 返回 REFUSED。
|
- `default`: 返回 REFUSED。
|
||||||
- `drop`: 丢弃请求。
|
- `drop`: 丢弃请求。
|
||||||
|
|
||||||
默认使用 `defualt`。
|
默认使用 `default`。
|
||||||
|
|
||||||
#### no_drop
|
#### no_drop
|
||||||
|
|
||||||
|
|||||||
@@ -73,24 +73,55 @@ Example:
|
|||||||
|
|
||||||
=== "Use hosts if available"
|
=== "Use hosts if available"
|
||||||
|
|
||||||
```json
|
=== ":material-card-multiple: sing-box 1.14.0"
|
||||||
{
|
|
||||||
"dns": {
|
```json
|
||||||
"servers": [
|
{
|
||||||
{
|
"dns": {
|
||||||
...
|
"servers": [
|
||||||
},
|
{
|
||||||
{
|
...
|
||||||
"type": "hosts",
|
},
|
||||||
"tag": "hosts"
|
{
|
||||||
|
"type": "hosts",
|
||||||
|
"tag": "hosts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"action": "evaluate",
|
||||||
|
"server": "hosts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match_response": true,
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"action": "respond"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"rules": [
|
```
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
=== ":material-card-remove: sing-box < 1.14.0"
|
||||||
"server": "hosts"
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hosts",
|
||||||
|
"tag": "hosts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"server": "hosts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -73,24 +73,55 @@ hosts 文件路径列表。
|
|||||||
|
|
||||||
=== "如果可用则使用 hosts"
|
=== "如果可用则使用 hosts"
|
||||||
|
|
||||||
```json
|
=== ":material-card-multiple: sing-box 1.14.0"
|
||||||
{
|
|
||||||
"dns": {
|
```json
|
||||||
"servers": [
|
{
|
||||||
{
|
"dns": {
|
||||||
...
|
"servers": [
|
||||||
},
|
{
|
||||||
{
|
...
|
||||||
"type": "hosts",
|
},
|
||||||
"tag": "hosts"
|
{
|
||||||
|
"type": "hosts",
|
||||||
|
"tag": "hosts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"action": "evaluate",
|
||||||
|
"server": "hosts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match_response": true,
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"action": "respond"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"rules": [
|
```
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
=== ":material-card-remove: sing-box < 1.14.0"
|
||||||
"server": "hosts"
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hosts",
|
||||||
|
"tag": "hosts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"server": "hosts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ The type of the DNS server.
|
|||||||
|
|
||||||
| Type | Format |
|
| Type | Format |
|
||||||
|-----------------|---------------------------|
|
|-----------------|---------------------------|
|
||||||
| empty (default) | [Legacy](./legacy/) |
|
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
||||||
| `local` | [Local](./local/) |
|
| `local` | [Local](./local/) |
|
||||||
| `hosts` | [Hosts](./hosts/) |
|
| `hosts` | [Hosts](./hosts/) |
|
||||||
| `tcp` | [TCP](./tcp/) |
|
| `tcp` | [TCP](./tcp/) |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ DNS 服务器的类型。
|
|||||||
|
|
||||||
| 类型 | 格式 |
|
| 类型 | 格式 |
|
||||||
|-----------------|---------------------------|
|
|-----------------|---------------------------|
|
||||||
| empty (default) | [Legacy](./legacy/) |
|
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
||||||
| `local` | [Local](./local/) |
|
| `local` | [Local](./local/) |
|
||||||
| `hosts` | [Hosts](./hosts/) |
|
| `hosts` | [Hosts](./hosts/) |
|
||||||
| `tcp` | [TCP](./tcp/) |
|
| `tcp` | [TCP](./tcp/) |
|
||||||
|
|||||||
@@ -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"
|
!!! 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.
|
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`.
|
||||||
|
|||||||
@@ -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 中的更改"
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -43,29 +43,62 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc
|
|||||||
|
|
||||||
=== "Split DNS only"
|
=== "Split DNS only"
|
||||||
|
|
||||||
```json
|
=== ":material-card-multiple: sing-box 1.14.0"
|
||||||
{
|
|
||||||
"dns": {
|
```json
|
||||||
"servers": [
|
{
|
||||||
{
|
"dns": {
|
||||||
"type": "local",
|
"servers": [
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "resolved",
|
},
|
||||||
"tag": "resolved",
|
{
|
||||||
"service": "resolved"
|
"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,
|
=== ":material-card-remove: sing-box < 1.14.0"
|
||||||
"server": "resolved"
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "resolved",
|
||||||
|
"tag": "resolved",
|
||||||
|
"service": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"server": "resolved"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "Use as global DNS"
|
=== "Use as global DNS"
|
||||||
|
|
||||||
|
|||||||
@@ -42,29 +42,62 @@ icon: material/new-box
|
|||||||
|
|
||||||
=== "仅分割 DNS"
|
=== "仅分割 DNS"
|
||||||
|
|
||||||
```json
|
=== ":material-card-multiple: sing-box 1.14.0"
|
||||||
{
|
|
||||||
"dns": {
|
```json
|
||||||
"servers": [
|
{
|
||||||
{
|
"dns": {
|
||||||
"type": "local",
|
"servers": [
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "resolved",
|
},
|
||||||
"tag": "resolved",
|
{
|
||||||
"service": "resolved"
|
"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,
|
=== ":material-card-remove: sing-box < 1.14.0"
|
||||||
"server": "resolved"
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "resolved",
|
||||||
|
"tag": "resolved",
|
||||||
|
"service": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"server": "resolved"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "用作全局 DNS"
|
=== "用作全局 DNS"
|
||||||
|
|
||||||
|
|||||||
@@ -42,29 +42,62 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
|
|||||||
|
|
||||||
=== "MagicDNS only"
|
=== "MagicDNS only"
|
||||||
|
|
||||||
```json
|
=== ":material-card-multiple: sing-box 1.14.0"
|
||||||
{
|
|
||||||
"dns": {
|
```json
|
||||||
"servers": [
|
{
|
||||||
{
|
"dns": {
|
||||||
"type": "local",
|
"servers": [
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "tailscale",
|
},
|
||||||
"tag": "ts",
|
{
|
||||||
"endpoint": "ts-ep"
|
"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,
|
=== ":material-card-remove: sing-box < 1.14.0"
|
||||||
"server": "ts"
|
|
||||||
|
```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"
|
=== "Use as global DNS"
|
||||||
|
|
||||||
|
|||||||
@@ -42,29 +42,62 @@ icon: material/new-box
|
|||||||
|
|
||||||
=== "仅 MagicDNS"
|
=== "仅 MagicDNS"
|
||||||
|
|
||||||
```json
|
=== ":material-card-multiple: sing-box 1.14.0"
|
||||||
{
|
|
||||||
"dns": {
|
```json
|
||||||
"servers": [
|
{
|
||||||
{
|
"dns": {
|
||||||
"type": "local",
|
"servers": [
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "tailscale",
|
},
|
||||||
"tag": "ts",
|
{
|
||||||
"endpoint": "ts-ep"
|
"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,
|
=== ":material-card-remove: sing-box < 1.14.0"
|
||||||
"server": "ts"
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tailscale",
|
||||||
|
"tag": "ts",
|
||||||
|
"endpoint": "ts-ep"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_accept_any": true,
|
||||||
|
"server": "ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "用作全局 DNS"
|
=== "用作全局 DNS"
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user