mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 02:27:19 +10:00
Compare commits
57 Commits
cloudflare
...
dev-test-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1189d88048 | ||
|
|
82bc416985 | ||
|
|
276584be09 | ||
|
|
5d8f43cc6b | ||
|
|
ef92ed6795 | ||
|
|
e257b19bb3 | ||
|
|
e0e7a9ee96 | ||
|
|
170d22c4c5 | ||
|
|
b0a36b9fac | ||
|
|
079c491064 | ||
|
|
6794db201e | ||
|
|
a36080c51f | ||
|
|
324073f5dd | ||
|
|
ece54e8f2b | ||
|
|
566b97d5e5 | ||
|
|
d698e23a4d | ||
|
|
af1db56132 | ||
|
|
7a22d507b1 | ||
|
|
e9fb5faef6 | ||
|
|
7d6adf6933 | ||
|
|
f47803ca36 | ||
|
|
19b388fee4 | ||
|
|
d533f3e5c6 | ||
|
|
6435808a44 | ||
|
|
4975c3acac | ||
|
|
5c6fcc82ff | ||
|
|
652ea40c08 | ||
|
|
3488877fb9 | ||
|
|
a1547b337c | ||
|
|
d9dc7da43d | ||
|
|
a2fc5a04ce | ||
|
|
1a3c9ec2a4 | ||
|
|
3bbb0f4439 | ||
|
|
f982c02c34 | ||
|
|
ea77932276 | ||
|
|
3ef7cb1333 | ||
|
|
7035d803fc | ||
|
|
462587696b | ||
|
|
e4e0744da5 | ||
|
|
0c261c205b | ||
|
|
6249f076c6 | ||
|
|
9559d7b375 | ||
|
|
a160309182 | ||
|
|
16c3c682f8 | ||
|
|
f49536012e | ||
|
|
773cb3dcec | ||
|
|
8aaaa06d92 | ||
|
|
ce0ae1fa29 | ||
|
|
cf8889ba5d | ||
|
|
dbe8239ed9 | ||
|
|
28f937e13e | ||
|
|
a3c44f7160 | ||
|
|
469faff07c | ||
|
|
067ea0dccb | ||
|
|
e515c91bd2 | ||
|
|
2c648b7fbe | ||
|
|
b3ea0bd833 |
3
.github/goreleaser/README.md
vendored
Normal file
3
.github/goreleaser/README.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# goreleaser
|
||||||
|
|
||||||
|
I'm sorry for this, but I can't afford to renew for now because the subscription is too expensive for an open source project.
|
||||||
BIN
.github/goreleaser/ca.crt
vendored
Normal file
BIN
.github/goreleaser/ca.crt
vendored
Normal file
Binary file not shown.
87
.github/goreleaser/config.json
vendored
Normal file
87
.github/goreleaser/config.json
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "tls",
|
||||||
|
"server": "8.8.8.8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"address": [
|
||||||
|
"172.19.0.1/30",
|
||||||
|
"fdfe:dcba:9876::1/126"
|
||||||
|
],
|
||||||
|
"auto_route": true,
|
||||||
|
"auto_redirect": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"certificate": {
|
||||||
|
"tls_decryption": {
|
||||||
|
"enabled": true,
|
||||||
|
"key_pair_p12": "MIIKYQIBAzCCChEGCSqGSIb3DQEHAaCCCgIEggn+MIIJ+jCCBGgGCSqGSIb3DQEHBqCCBFkwggRVAgEAMIIETgYJKoZIhvcNAQcBMF0GCSqGSIb3DQEFDTBQMC8GCSqGSIb3DQEFDDAiBBBxLjkB6wrMHpRNPnq8KUnXAgIIADAKBggqhkiG9w0CCTAdBglghkgBZQMEASoEEHFou8IR0ZPb9O4NaLDC5LKAggPgL/7EoJRMEx5ZDVm2ZUQRuGyjS+lMB4JDZiykYfvfzMtQ2LZ+aO90rLxYFh4uBpbu+mmA0WDF/HU3GbE0nyY9beo0RAh0/u2Ak2kkfDSntRPVTl5zNBrT9hEtH9oSlN7tok9SMhWEJlsoIRhGinJwsDnDbXcIqkIj/oqtXlSJc6gA7CYf6AJRrVjP1Wtk80GMrMfYNvQw9bich5fs4biddf0xtR13YFV80rCPb+HtTT4KYa7Rzo5qR/cNHsMP/3v5BT2UszpaSIokPoW8ta1RWcQNXuH3OHjG4GMjg88w6xtyudIKrTyP0BTRfIJ2S2EtsWGHU2Gmr/MUY0a7abbtG+LVdSCRTgDoNeiY4C7lkQEOpefoZHWa3+jeGu17812YZHxfCZuhFy33rZgqngWRN1cdxoAbhozChtKmn0Uhdox7jqUw5M/Sj4DWHm0RNB8Ffvf39i/zvlfORzljIiwAKiB26FwpcKKRfx7rrjx4xRLkTLWl0DnJKxOcVz/oXSjglpHJvUSMgbpzXEHHQ7+d+K/WTnoj+dONifxiWBt1hQA8qoPiQceYWGY37oeWvGZI/Qv3ZSO5Mm/yVAuAFyOzJdpW5aC3Kq3gwNVbKNeeV5fWDtvP2K9XcgZFv8OqpNnvLmaL+iWHTPg5wYGvf0iWPr8NVU6OQpSZCOTodwOGfcpQ2YlCnkBgkjkJFLNuM4mi1U9kyTZWAYyZ6zVort0eezJcBoQGBBV2/GkFmwDNa9Q8mT8S7QTf7ZqAtyMnM9rBch7zIscBk6swG/KhgFRtUmDLpY6tpMb6vHHueu4duaUvIXvdjgTe4oE2Ou36VZ1+dC+RswmGCMwFlHqsZiIfU26SDiC3G9wH0iIg6th3LrDJYYD57l5Ps0pVjS7RAYYzu1lA2d2wGEFBJ3UEpJp257Wv2I6foeoTYXSX/XM1JUuFv1516qSqwPk4a1E6N6J+d+iWvM7BBcwakMG1XSUT4zhHrBzPPxXCCBeJHTcOoiaqXwaqsBBButSxViysvGZcBbyAxZNtmXCDh33a760XF4tb1f0mb2jW13CMARGOeubM3Z21eoc16tFkoKSD3wlzT2VlxVuUIgBT+wx4GOWgldngn2aXWInOkaEFdwABBLh5egxNBAI2tzirk6ijpRCq+gquTbEhxIwJavCfdYc0lqMevsEiZxqjoZHEf1EoId/rd3TEdclRf5OzLjbSbDICtFI82S5A+wDMXltVmB+Rw1mBZZUhvbUUOC7ARQ0pkE8DfwgPviFo2z8/i++3Mb02D57V1Mz1k6PB56QzlEOTJrmaBXSQs7U8Aiuln6CA+McwggWKBgkqhkiG9w0BBwGgggV7BIIFdzCCBXMwggVvBgsqhkiG9w0BDAoBAqCCBTcwggUzMF0GCSqGSIb3DQEFDTBQMC8GCSqGSIb3DQEFDDAiBBCl+G6epsuiNjP2afUFOwazAgIIADAKBggqhkiG9w0CCTAdBglghkgBZQMEASoEEG2FLRo+Ud+dbzCVbrer71YEggTQ81fiT0+gLnYWZpNq0MV/kPma4P+sws4wRd5CVG5rCMwmmr3JUCVk66uYLZTBXqHJ0qy3CPE2K1siImQJNS3DMD1q9WVCLPFEPLbO1ycsV73AOMc2UNJMkY7AgGCMpK+u/afMewsnAk/fmwjTw5qOm21TeesahwVvIMb3pQrkFu8FSIWK9IPRX7VCiYSa/KajiFKi0/lWEk9/LJEfikqGOB3FWYQkrV4jhhh+SNMm5LATgNgZ3FyhleruJZup0PN25W2IrpjcEBr9gHVU6gsCyB4PTTrVfopLq7goDWnOQeeAa4Y98QN6nT0EyqkfKU678/JeLz0gW8zijgdqzLwwucLg6cGE379d/2igE7/SJO8qa/JAjD3RDe88N97ysKW7vOOvIH6DnmkgQc8Cq/KKOyVlrDNx65YEft1oqVE3L5IfnmHT5ycbzyMJpdB6uL6OT9KqVLB2bHWDH47XfI8I8z56mzmKSXrWGm93beYV8u908Rokj82LHGEf9th6ttBZykWZgS+hQjc3jIU8xpa2/7mpPVFBCTiphBtp3+fCEVKmnubiiwe28Lw+xEvX8oAEGXhi5fNIGrAXvMk/rgpoh44wQwET6WnyiO8Ad8hOxvPtwgGD0m0FNFlv+yIGzY1PZeevquLKEwtvllo/A3g0OUbeGC2qC5s8VGkv11FRQPdUnOV2oXvosAWqxh6SnVrG8xbxc5L2xjJuUH8b70ne4iXzcfXo5FubtLuuJ6WNFWO9UasmvKaMqFZDlMK8FMcNTq6X0m8ilRZf056C3FDQAMxIa9mKyWebm3+4+LfxjgWo1dxvXR5HnMpzCbcoz/TIbSiUzSTaihxpzMi7Cvkc/JqPTTSkqjR+jLw7tOZucP8VtpQmQvqg2fd1hBgqam37qVC45D2765/V5v74+gtn5nc6HrGOEwpLlqcy5kojrjhQkNUkS7x5vg1KOFP/9uoC67qRFaGH5EM0XAdTVAyt1gn+StXVCXsNKvX93BPaNwL7we/zYZPpERFHaVD9R1Fw2Bz0+RzcdNQqP9yiq3mmmGNZHS0KSAKP3cmA3pwt9gPjpt/L1VNFgkVti2/YIDF37c3yuU9ZBI6kA7LhkcH5j0APr1ppS+Zxw6UKhsZDSGySqPyz3C2k4wy+R2+8mO1dN9haRW0smWWnziHWh8OFGhG+ghvc2HiX1tg2dTrByIFr9wixs4Kn9wDg0Qc1mS0+2+KacO8todl3jVYsLhcSTt5d2b/ZHuyAx4UPFtWPPF1vFdRMnf1jq83q/OEcTSfqkiEpEzs6NXpDEy1E0neq+LVHXi37IHzTGjjIvBnE2KZUoUdiFYitfDoUQpdhSpWKZTsmpqVXi/b7TZ20scvt5Qb6nfEWNds7hyGhnzAGQIV64xaDhKDB1p3QpDYxsJHvAGC1Yj9CY5w+sYpOjsfUo0qKeaFmu0fWX44s388GjZbid92/UvIxN9Lt/jri2xq+XPjJR194hc2ITUDrZvaaqeZ4odH4HXUC7FMqL6NVeX6MIv4g2QQkrt9DO35LokztOQCeuaA4rOZiM7mR3JJZIXf2jFNwElU0bvUJY2eYcQwTSQPXBzMz0AvhXxJiOFx3IQHHl55j4KMpab/NNbHChDUWJ7ptLX0/x8R3scJjCqwxJTAjBgkqhkiG9w0BCRUxFgQU6W6dFe0wnwEJqyaK7H5cUfUvxzQwRzAvMAsGCWCGSAFlAwQCAQQghQoDpo4gS46c+xoCeAykL69ZRT3zYrgNkgvL6s2UIVsEEHkNJl2cMvSFmZ8gi+cS/vQCAggA",
|
||||||
|
"key_pair_p12_password": "D173A3D9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mitm": {
|
||||||
|
"enabled": true,
|
||||||
|
"http2_enabled": true
|
||||||
|
},
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"action": "sniff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "or",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"network": "udp",
|
||||||
|
"port": 53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocol": "dns"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": "hijack-dns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_is_private": true,
|
||||||
|
"outbound": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "resolve"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "goreleaser.com",
|
||||||
|
"action": "route-options",
|
||||||
|
"mitm": {
|
||||||
|
"enabled": true,
|
||||||
|
"surge_map_local": [
|
||||||
|
"^https://goreleaser\\.com/static/latest-pro data-type=text data=\"(update check disabled)\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "api.gumroad.com",
|
||||||
|
"action": "route-options",
|
||||||
|
"mitm": {
|
||||||
|
"enabled": true,
|
||||||
|
"surge_map_local": [
|
||||||
|
"^https://api\\.gumroad\\.com/v2/licenses/verify data-type=file data=.github/goreleaser/response.json header=\"Content-Type:application/json\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.github/goreleaser/configure.sh
vendored
Executable file
11
.github/goreleaser/configure.sh
vendored
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
release/local/install_minimal.sh
|
||||||
|
sudo cp .github/goreleaser/config.json /usr/local/etc/sing-box/config.json
|
||||||
|
sudo mkdir -p /var/lib/sing-box/.github/goreleaser
|
||||||
|
sudo cp .github/goreleaser/response.json /var/lib/sing-box/.github/goreleaser/response.json
|
||||||
|
go run -v ./cmd/sing-box tools install-ca .github/goreleaser/ca.crt
|
||||||
|
sudo systemctl start sing-box
|
||||||
|
sleep 5
|
||||||
12
.github/goreleaser/response.json
vendored
Normal file
12
.github/goreleaser/response.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"purchase": {
|
||||||
|
"license_key": "fake-key",
|
||||||
|
"subscription_id": "fake-id",
|
||||||
|
"product_id": "7ev6hHL7RZc753daE5bRNw==",
|
||||||
|
"product_permalink": "https:\/\/beckersoft.gumroad.com\/l\/goreleaser",
|
||||||
|
"seller_id": "A2wDalJj66fJdFU_jwy_oA==",
|
||||||
|
"short_product_id": "CadfZ",
|
||||||
|
"permalink": "goreleaser"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
.github/setup_legacy_go.sh
vendored
Executable file
22
.github/setup_legacy_go.sh
vendored
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION="1.23.6"
|
||||||
|
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||||
|
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||||
|
mv go $HOME/go/go_legacy
|
||||||
|
cd $HOME/go/go_legacy
|
||||||
|
|
||||||
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
|
# this patch file only works on golang1.23.x
|
||||||
|
# that means after golang1.24 release it must be changed
|
||||||
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||||
|
# revert:
|
||||||
|
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||||
|
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||||
|
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||||
|
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||||
|
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
||||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -112,7 +112,6 @@ jobs:
|
|||||||
- name: darwin_amd64
|
- name: darwin_amd64
|
||||||
goos: darwin
|
goos: darwin
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
require_legacy_go: true
|
|
||||||
- name: android_arm64
|
- name: android_arm64
|
||||||
goos: android
|
goos: android
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
@@ -141,14 +140,11 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/go1.20.14
|
~/go/go_legacy
|
||||||
key: go120
|
key: go_legacy_1236
|
||||||
- name: Setup legacy Go
|
- name: Setup legacy Go
|
||||||
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||||
run: |-
|
run: bash .github/setup_legacy_go.sh
|
||||||
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
|
|
||||||
tar -xzf go1.20.14.linux-amd64.tar.gz
|
|
||||||
mv go $HOME/go/go1.20.14
|
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.goos == 'android'
|
if: matrix.goos == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
|
/*.js
|
||||||
/*.srs
|
/*.srs
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ linters-settings:
|
|||||||
- -SA1003
|
- -SA1003
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go: "1.23"
|
go: "1.24"
|
||||||
build-tags:
|
build-tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_script
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-dirs:
|
exclude-dirs:
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ builds:
|
|||||||
- -v
|
- -v
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
tags:
|
tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
targets:
|
targets:
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ builds:
|
|||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
|
- with_script
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
targets:
|
targets:
|
||||||
- linux_386
|
- linux_386
|
||||||
- linux_amd64_v1
|
- linux_amd64_v1
|
||||||
@@ -49,18 +51,20 @@ builds:
|
|||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
|
- with_script
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||||
tool: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
|
||||||
targets:
|
targets:
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_386
|
- windows_386
|
||||||
- darwin_amd64_v1
|
|
||||||
- id: android
|
- id: android
|
||||||
<<: *template
|
<<: *template
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
overrides:
|
overrides:
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: arm
|
goarch: arm
|
||||||
@@ -124,8 +128,8 @@ nfpms:
|
|||||||
- deb
|
- deb
|
||||||
- rpm
|
- rpm
|
||||||
- archlinux
|
- archlinux
|
||||||
# - apk
|
# - apk
|
||||||
# - ipk
|
# - ipk
|
||||||
priority: extra
|
priority: extra
|
||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ RUN set -ex \
|
|||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
|
|||||||
20
Makefile
20
Makefile
@@ -1,9 +1,9 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
||||||
TAGS_GO121 = with_ech
|
TAGS_GO123 = with_tailscale,with_script
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
|
TAGS ?= $(TAGS_GO120),$(TAGS_GO123)
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
@@ -17,14 +17,17 @@ PREFIX ?= $(shell go env GOPATH)
|
|||||||
.PHONY: test release docs build
|
.PHONY: test release docs build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
ci_build_go120:
|
ci_build_go120:
|
||||||
go build $(PARAMS) $(MAIN)
|
export GOTOOLCHAIN=local && \
|
||||||
|
go build $(PARAMS) $(MAIN) && \
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
go build $(PARAMS) $(MAIN)
|
export GOTOOLCHAIN=local && \
|
||||||
|
go build $(PARAMS) $(MAIN) && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
generate_completions:
|
generate_completions:
|
||||||
@@ -61,6 +64,9 @@ proto_install:
|
|||||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
|
||||||
|
update_certificates:
|
||||||
|
go run ./cmd/internal/update_certificates
|
||||||
|
|
||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
@@ -227,8 +233,8 @@ lib:
|
|||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.5
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.5
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
venv/bin/mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
|
|||||||
24
adapter/certificate.go
Normal file
24
adapter/certificate.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateStore interface {
|
||||||
|
LifecycleService
|
||||||
|
Pool() *x509.CertPool
|
||||||
|
TLSDecryptionEnabled() bool
|
||||||
|
TLSDecryptionCertificate() *x509.Certificate
|
||||||
|
TLSDecryptionPrivateKey() any
|
||||||
|
}
|
||||||
|
|
||||||
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
store := service.FromContext[CertificateStore](ctx)
|
||||||
|
if store == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return store.Pool()
|
||||||
|
}
|
||||||
73
adapter/dns.go
Normal file
73
adapter/dns.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSRouter interface {
|
||||||
|
Lifecycle
|
||||||
|
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||||
|
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
||||||
|
ClearCache()
|
||||||
|
LookupReverseMapping(ip netip.Addr) (string, bool)
|
||||||
|
ResetNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSClient interface {
|
||||||
|
Start()
|
||||||
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
|
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||||
|
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||||
|
ClearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSQueryOptions struct {
|
||||||
|
Transport DNSTransport
|
||||||
|
Strategy C.DomainStrategy
|
||||||
|
DisableCache bool
|
||||||
|
RewriteTTL *uint32
|
||||||
|
ClientSubnet netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
type RDRCStore interface {
|
||||||
|
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
||||||
|
SaveRDRC(transportName string, qName string, qType uint16) error
|
||||||
|
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSTransport interface {
|
||||||
|
Lifecycle
|
||||||
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
Dependencies() []string
|
||||||
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LegacyDNSTransport interface {
|
||||||
|
LegacyStrategy() C.DomainStrategy
|
||||||
|
LegacyClientSubnet() netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSTransportRegistry interface {
|
||||||
|
option.DNSTransportOptionsRegistry
|
||||||
|
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSTransportManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Transports() []DNSTransport
|
||||||
|
Transport(tag string) (DNSTransport, bool)
|
||||||
|
Default() DNSTransport
|
||||||
|
FakeIP() FakeIPTransport
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||||
|
}
|
||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
"github.com/sagernet/sing/common/varbin"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +14,20 @@ type ClashServer interface {
|
|||||||
ConnectionTracker
|
ConnectionTracker
|
||||||
Mode() string
|
Mode() string
|
||||||
ModeList() []string
|
ModeList() []string
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
HistoryStorage() URLTestHistoryStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLTestHistory struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Delay uint16 `json:"delay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLTestHistoryStorage interface {
|
||||||
|
SetHook(hook chan<- struct{})
|
||||||
|
LoadURLTestHistory(tag string) *URLTestHistory
|
||||||
|
DeleteURLTestHistory(tag string)
|
||||||
|
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayServer interface {
|
type V2RayServer interface {
|
||||||
@@ -31,7 +42,7 @@ type CacheFile interface {
|
|||||||
FakeIPStorage
|
FakeIPStorage
|
||||||
|
|
||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
dns.RDRCStore
|
RDRCStore
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
@@ -41,6 +52,10 @@ type CacheFile interface {
|
|||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedBinary
|
LoadRuleSet(tag string) *SavedBinary
|
||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
|
LoadScript(tag string) *SavedBinary
|
||||||
|
SaveScript(tag string, script *SavedBinary) error
|
||||||
|
SurgePersistentStoreRead(key string) string
|
||||||
|
SurgePersistentStoreWrite(key string, value string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedBinary struct {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +26,6 @@ type FakeIPStorage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FakeIPTransport interface {
|
type FakeIPTransport interface {
|
||||||
dns.Transport
|
DNSTransport
|
||||||
Store() FakeIPStore
|
Store() FakeIPStore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -58,6 +60,8 @@ type InboundContext struct {
|
|||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
PacketSniffError error
|
PacketSniffError error
|
||||||
|
HTTPRequest *http.Request
|
||||||
|
ClientHello *tls.ClientHelloInfo
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -72,14 +76,15 @@ type InboundContext struct {
|
|||||||
UDPDisableDomainUnmapping bool
|
UDPDisableDomainUnmapping bool
|
||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
|
TLSFragment bool
|
||||||
|
TLSFragmentFallbackDelay time.Duration
|
||||||
|
MITM *option.MITMRouteOptions
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy *C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
DNSServer string
|
|
||||||
|
|
||||||
DestinationAddresses []netip.Addr
|
DestinationAddresses []netip.Addr
|
||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import E "github.com/sagernet/sing/common/exceptions"
|
import (
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
type StartStage uint8
|
type StartStage uint8
|
||||||
|
|
||||||
@@ -45,6 +47,9 @@ type LifecycleService interface {
|
|||||||
|
|
||||||
func Start(stage StartStage, services ...Lifecycle) error {
|
func Start(stage StartStage, services ...Lifecycle) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
|
if service == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
13
adapter/mitm.go
Normal file
13
adapter/mitm.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MITMEngine interface {
|
||||||
|
Lifecycle
|
||||||
|
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
|
}
|
||||||
@@ -29,12 +29,14 @@ type NetworkManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NetworkOptions struct {
|
type NetworkOptions struct {
|
||||||
NetworkStrategy *C.NetworkStrategy
|
BindInterface string
|
||||||
NetworkType []C.InterfaceType
|
RoutingMark uint32
|
||||||
FallbackNetworkType []C.InterfaceType
|
DomainResolver string
|
||||||
FallbackDelay time.Duration
|
DomainResolveOptions DNSQueryOptions
|
||||||
BindInterface string
|
NetworkStrategy *C.NetworkStrategy
|
||||||
RoutingMark uint32
|
NetworkType []C.InterfaceType
|
||||||
|
FallbackNetworkType []C.InterfaceType
|
||||||
|
FallbackDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Manager struct {
|
|||||||
registry adapter.OutboundRegistry
|
registry adapter.OutboundRegistry
|
||||||
endpoint adapter.EndpointManager
|
endpoint adapter.EndpointManager
|
||||||
defaultTag string
|
defaultTag string
|
||||||
access sync.Mutex
|
access sync.RWMutex
|
||||||
started bool
|
started bool
|
||||||
stage adapter.StartStage
|
stage adapter.StartStage
|
||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
@@ -169,15 +169,15 @@ func (m *Manager) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Outbounds() []adapter.Outbound {
|
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||||
m.access.Lock()
|
m.access.RLock()
|
||||||
defer m.access.Unlock()
|
defer m.access.RUnlock()
|
||||||
return m.outbounds
|
return m.outbounds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||||
m.access.Lock()
|
m.access.RLock()
|
||||||
outbound, found := m.outboundByTag[tag]
|
outbound, found := m.outboundByTag[tag]
|
||||||
m.access.Unlock()
|
m.access.RUnlock()
|
||||||
if found {
|
if found {
|
||||||
return outbound, true
|
return outbound, true
|
||||||
}
|
}
|
||||||
@@ -185,8 +185,8 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Default() adapter.Outbound {
|
func (m *Manager) Default() adapter.Outbound {
|
||||||
m.access.Lock()
|
m.access.RLock()
|
||||||
defer m.access.Unlock()
|
defer m.access.RUnlock()
|
||||||
if m.defaultOutbound != nil {
|
if m.defaultOutbound != nil {
|
||||||
return m.defaultOutbound
|
return m.defaultOutbound
|
||||||
} else {
|
} else {
|
||||||
@@ -196,9 +196,9 @@ func (m *Manager) Default() adapter.Outbound {
|
|||||||
|
|
||||||
func (m *Manager) Remove(tag string) error {
|
func (m *Manager) Remove(tag string) error {
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
outbound, found := m.outboundByTag[tag]
|
outbound, found := m.outboundByTag[tag]
|
||||||
if !found {
|
if !found {
|
||||||
m.access.Unlock()
|
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
delete(m.outboundByTag, tag)
|
delete(m.outboundByTag, tag)
|
||||||
@@ -232,7 +232,6 @@ func (m *Manager) Remove(tag string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.access.Unlock()
|
|
||||||
if started {
|
if started {
|
||||||
return common.Close(outbound)
|
return common.Close(outbound)
|
||||||
}
|
}
|
||||||
@@ -247,8 +246,6 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.started {
|
if m.started {
|
||||||
for _, stage := range adapter.ListStartStages {
|
for _, stage := range adapter.ListStartStages {
|
||||||
err = adapter.LegacyStart(outbound, stage)
|
err = adapter.LegacyStart(outbound, stage)
|
||||||
@@ -257,6 +254,8 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||||
if m.started {
|
if m.started {
|
||||||
err = common.Close(existsOutbound)
|
err = common.Close(existsOutbound)
|
||||||
|
|||||||
@@ -2,44 +2,29 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geoip"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
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"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
mdns "github.com/miekg/dns"
|
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
|
|
||||||
FakeIPStore() FakeIPStore
|
|
||||||
|
|
||||||
ConnectionRouter
|
ConnectionRouter
|
||||||
PreMatch(metadata InboundContext) error
|
PreMatch(metadata InboundContext) error
|
||||||
ConnectionRouterEx
|
ConnectionRouterEx
|
||||||
|
|
||||||
GeoIPReader() *geoip.Reader
|
|
||||||
LoadGeosite(code string) (Rule, error)
|
|
||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
NeedWIFIState() bool
|
NeedWIFIState() bool
|
||||||
|
|
||||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
|
||||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
|
||||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
|
||||||
ClearDNSCache()
|
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
|
||||||
SetTracker(tracker ConnectionTracker)
|
SetTracker(tracker ConnectionTracker)
|
||||||
|
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +68,14 @@ type RuleSetMetadata struct {
|
|||||||
ContainsIPCIDRRule bool
|
ContainsIPCIDRRule bool
|
||||||
}
|
}
|
||||||
type HTTPStartContext struct {
|
type HTTPStartContext struct {
|
||||||
|
ctx context.Context
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
httpClientCache map[string]*http.Client
|
httpClientCache map[string]*http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPStartContext() *HTTPStartContext {
|
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||||
return &HTTPStartContext{
|
return &HTTPStartContext{
|
||||||
|
ctx: ctx,
|
||||||
httpClientCache: make(map[string]*http.Client),
|
httpClientCache: make(map[string]*http.Client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +93,10 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
|
|||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
},
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||||
|
RootCAs: RootPoolFromContext(c.ctx),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.httpClientCache[detour] = httpClient
|
c.httpClientCache[detour] = httpClient
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ type Rule interface {
|
|||||||
HeadlessRule
|
HeadlessRule
|
||||||
Service
|
Service
|
||||||
Type() string
|
Type() string
|
||||||
UpdateGeosite() error
|
|
||||||
Action() RuleAction
|
Action() RuleAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
adapter/script.go
Normal file
54
adapter/script.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Scripts() []Script
|
||||||
|
Script(name string) (Script, bool)
|
||||||
|
SurgeCache() *SurgeInMemoryCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type SurgeInMemoryCache struct {
|
||||||
|
sync.RWMutex
|
||||||
|
Data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Script interface {
|
||||||
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||||
|
PostStart() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SurgeScript interface {
|
||||||
|
Script
|
||||||
|
ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error
|
||||||
|
ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*HTTPRequestScriptResult, error)
|
||||||
|
ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*HTTPResponseScriptResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPRequestScriptResult struct {
|
||||||
|
URL string
|
||||||
|
Headers http.Header
|
||||||
|
Body []byte
|
||||||
|
Response *HTTPRequestScriptResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPRequestScriptResponse struct {
|
||||||
|
Status int
|
||||||
|
Headers http.Header
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPResponseScriptResult struct {
|
||||||
|
Status int
|
||||||
|
Headers http.Header
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
146
box.go
146
box.go
@@ -12,17 +12,22 @@ import (
|
|||||||
"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"
|
||||||
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"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/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/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/mitm"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
|
"github.com/sagernet/sing-box/script"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
@@ -34,17 +39,21 @@ import (
|
|||||||
var _ adapter.Service = (*Box)(nil)
|
var _ adapter.Service = (*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
|
||||||
connection *route.ConnectionManager
|
dnsTransport *dns.TransportManager
|
||||||
router *route.Router
|
dnsRouter *dns.Router
|
||||||
services []adapter.LifecycleService
|
connection *route.ConnectionManager
|
||||||
done chan struct{}
|
router *route.Router
|
||||||
|
script *script.Manager
|
||||||
|
mitm adapter.MITMEngine //*mitm.Engine
|
||||||
|
services []adapter.LifecycleService
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -58,6 +67,7 @@ func Context(
|
|||||||
inboundRegistry adapter.InboundRegistry,
|
inboundRegistry adapter.InboundRegistry,
|
||||||
outboundRegistry adapter.OutboundRegistry,
|
outboundRegistry adapter.OutboundRegistry,
|
||||||
endpointRegistry adapter.EndpointRegistry,
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
) 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 {
|
||||||
@@ -74,6 +84,10 @@ func Context(
|
|||||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||||
}
|
}
|
||||||
|
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||||
|
}
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +102,7 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
|
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](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")
|
||||||
@@ -131,33 +146,67 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var services []adapter.LifecycleService
|
||||||
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
services = append(services, certificateStore)
|
||||||
|
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
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)
|
||||||
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)
|
||||||
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
||||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
|
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize router")
|
return nil, E.Cause(err, "initialize router")
|
||||||
}
|
}
|
||||||
|
|
||||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
|
||||||
var timeService *tls.TimeServiceWrapper
|
var timeService *tls.TimeServiceWrapper
|
||||||
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
timeService = new(tls.TimeServiceWrapper)
|
timeService = new(tls.TimeServiceWrapper)
|
||||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
}
|
}
|
||||||
|
for i, transportOptions := range dnsOptions.Servers {
|
||||||
|
var tag string
|
||||||
|
if transportOptions.Tag != "" {
|
||||||
|
tag = transportOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = dnsTransportManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
transportOptions.Type,
|
||||||
|
transportOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize dns router")
|
||||||
|
}
|
||||||
for i, endpointOptions := range options.Endpoints {
|
for i, endpointOptions := range options.Endpoints {
|
||||||
var tag string
|
var tag string
|
||||||
if endpointOptions.Tag != "" {
|
if endpointOptions.Tag != "" {
|
||||||
@@ -181,7 +230,7 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointOptions.Options,
|
endpointOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, inboundOptions := range options.Inbounds {
|
for i, inboundOptions := range options.Inbounds {
|
||||||
@@ -238,13 +287,24 @@ func New(options Options) (*Box, error) {
|
|||||||
option.DirectOutboundOptions{},
|
option.DirectOutboundOptions{},
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
dnsTransportManager.Initialize(common.Must1(
|
||||||
|
local.NewTransport(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger("dns/local"),
|
||||||
|
"local",
|
||||||
|
option.LocalDNSServerOptions{},
|
||||||
|
)))
|
||||||
|
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize script manager")
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var services []adapter.LifecycleService
|
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
@@ -273,7 +333,7 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create NTP service")
|
return nil, E.Cause(err, "create NTP service")
|
||||||
}
|
}
|
||||||
@@ -288,18 +348,32 @@ func New(options Options) (*Box, error) {
|
|||||||
timeService.TimeService = ntpService
|
timeService.TimeService = ntpService
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
|
mitmOptions := common.PtrValueOrDefault(options.MITM)
|
||||||
|
var mitmEngine adapter.MITMEngine
|
||||||
|
if mitmOptions.Enabled {
|
||||||
|
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create MITM engine")
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.MITMEngine](ctx, engine)
|
||||||
|
mitmEngine = engine
|
||||||
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
inbound: inboundManager,
|
inbound: inboundManager,
|
||||||
outbound: outboundManager,
|
outbound: outboundManager,
|
||||||
connection: connectionManager,
|
dnsTransport: dnsTransportManager,
|
||||||
router: router,
|
dnsRouter: dnsRouter,
|
||||||
createdAt: createdAt,
|
connection: connectionManager,
|
||||||
logFactory: logFactory,
|
router: router,
|
||||||
logger: logFactory.Logger(),
|
script: scriptManager,
|
||||||
services: services,
|
mitm: mitmEngine,
|
||||||
done: make(chan struct{}),
|
createdAt: createdAt,
|
||||||
|
logFactory: logFactory,
|
||||||
|
logger: logFactory.Logger(),
|
||||||
|
services: services,
|
||||||
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,11 +427,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
|
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -381,7 +455,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -389,7 +463,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -408,7 +482,7 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.network,
|
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.services {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ var (
|
|||||||
debugFlags []string
|
debugFlags []string
|
||||||
sharedTags []string
|
sharedTags []string
|
||||||
iosTags []string
|
iosTags []string
|
||||||
|
memcTags []string
|
||||||
debugTags []string
|
debugTags []string
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,8 +59,9 @@ func init() {
|
|||||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_script")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||||
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,18 +101,19 @@ func buildAndroid() {
|
|||||||
"-javapkg=io.nekohasekai",
|
"-javapkg=io.nekohasekai",
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, debugFlags...)
|
args = append(args, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-tags")
|
tags := append(sharedTags, memcTags...)
|
||||||
if !debugEnabled {
|
if debugEnabled {
|
||||||
args = append(args, strings.Join(sharedTags, ","))
|
tags = append(tags, debugTags...)
|
||||||
} else {
|
|
||||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||||
@@ -148,7 +151,9 @@ func buildApple() {
|
|||||||
"-v",
|
"-v",
|
||||||
"-target", bindTarget,
|
"-target", bindTarget,
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
|
"-tags-macos=" + strings.Join(memcTags, ","),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
@@ -156,12 +161,11 @@ func buildApple() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags := append(sharedTags, iosTags...)
|
tags := append(sharedTags, iosTags...)
|
||||||
args = append(args, "-tags")
|
if debugEnabled {
|
||||||
if !debugEnabled {
|
tags = append(tags, debugTags...)
|
||||||
args = append(args, strings.Join(tags, ","))
|
|
||||||
} else {
|
|
||||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||||
|
|||||||
71
cmd/internal/update_certificates/main.go
Normal file
71
cmd/internal/update_certificates/main.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := updateMozillaIncludedRootCAs()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMozillaIncludedRootCAs() error {
|
||||||
|
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
reader := csv.NewReader(response.Body)
|
||||||
|
header, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
geoIndex := slices.Index(header, "Geographic Focus")
|
||||||
|
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||||
|
certIndex := slices.Index(header, "PEM Info")
|
||||||
|
|
||||||
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var mozillaIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mozillaIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
|
for {
|
||||||
|
record, err := reader.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if record[geoIndex] == "China" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[nameIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes
|
||||||
|
cert = cert[1 : len(cert)-1]
|
||||||
|
generated.WriteString(cert)
|
||||||
|
generated.WriteString("`))\n")
|
||||||
|
}
|
||||||
|
generated.WriteString("}\n")
|
||||||
|
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||||
|
}
|
||||||
@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
|||||||
configPaths = append(configPaths, "config.json")
|
configPaths = append(configPaths, "config.json")
|
||||||
}
|
}
|
||||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
||||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
|
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
|
||||||
}
|
}
|
||||||
|
|||||||
121
cmd/sing-box/cmd_generate_ca.go
Normal file
121
cmd/sing-box/cmd_generate_ca.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"software.sslmate.com/src/go-pkcs12"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagGenerateCAName string
|
||||||
|
flagGenerateCAPKCS12Password string
|
||||||
|
flagGenerateOutput string
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGenerateCAKeyPair = &cobra.Command{
|
||||||
|
Use: "ca-keypair",
|
||||||
|
Short: "Generate CA key pair",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := generateCAKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAName, "name", "n", "", "Set custom CA name")
|
||||||
|
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAPKCS12Password, "p12-password", "p", "", "Set custom PKCS12 password")
|
||||||
|
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateOutput, "output", "o", ".", "Set output directory")
|
||||||
|
commandGenerate.AddCommand(commandGenerateCAKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCAKeyPair() error {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
spkiASN1, err := x509.MarshalPKIXPublicKey(privateKey.Public())
|
||||||
|
var spki struct {
|
||||||
|
Algorithm pkix.AlgorithmIdentifier
|
||||||
|
SubjectPublicKey asn1.BitString
|
||||||
|
}
|
||||||
|
_, err = asn1.Unmarshal(spkiASN1, &spki)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
|
||||||
|
var caName string
|
||||||
|
if flagGenerateCAName != "" {
|
||||||
|
caName = flagGenerateCAName
|
||||||
|
} else {
|
||||||
|
caName = "sing-box Generated CA " + strings.ToUpper(hex.EncodeToString(skid[:4]))
|
||||||
|
}
|
||||||
|
caTpl := &x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{caName},
|
||||||
|
CommonName: caName,
|
||||||
|
},
|
||||||
|
SubjectKeyId: skid[:],
|
||||||
|
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
KeyUsage: x509.KeyUsageCertSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLenZero: true,
|
||||||
|
}
|
||||||
|
publicDer, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, privateKey.Public(), privateKey)
|
||||||
|
var caPassword string
|
||||||
|
if flagGenerateCAPKCS12Password != "" {
|
||||||
|
caPassword = flagGenerateCAPKCS12Password
|
||||||
|
} else {
|
||||||
|
caPassword = strings.ToUpper(hex.EncodeToString(skid[:4]))
|
||||||
|
}
|
||||||
|
caTpl.Raw = publicDer
|
||||||
|
p12Bytes, err := pkcs12.Modern.Encode(privateKey, caTpl, nil, caPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
||||||
|
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".private.pem"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}), 0o644)
|
||||||
|
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".crt"), publicDer, 0o644)
|
||||||
|
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".p12"), p12Bytes, 0o644)
|
||||||
|
var tlsDecryptionOptions option.TLSDecryptionOptions
|
||||||
|
tlsDecryptionOptions.Enabled = true
|
||||||
|
tlsDecryptionOptions.KeyPair = base64.StdEncoding.EncodeToString(p12Bytes)
|
||||||
|
tlsDecryptionOptions.KeyPairPassword = caPassword
|
||||||
|
var certificateOptions option.CertificateOptions
|
||||||
|
certificateOptions.TLSDecryption = &tlsDecryptionOptions
|
||||||
|
encoder := json.NewEncoder(os.Stdout)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
return encoder.Encode(certificateOptions)
|
||||||
|
}
|
||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pqSignatureSchemesEnabled bool
|
|
||||||
|
|
||||||
var commandGenerateECHKeyPair = &cobra.Command{
|
var commandGenerateECHKeyPair = &cobra.Command{
|
||||||
Use: "ech-keypair <plain_server_name>",
|
Use: "ech-keypair <plain_server_name>",
|
||||||
Short: "Generate TLS ECH key pair",
|
Short: "Generate TLS ECH key pair",
|
||||||
@@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
|
||||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateECHKeyPair(serverName string) error {
|
func generateECHKeyPair(serverName string) error {
|
||||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,36 +12,5 @@ var commandTools = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
|
||||||
mainCommand.AddCommand(commandTools)
|
mainCommand.AddCommand(commandTools)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPreStartedClient() (*box.Box, error) {
|
|
||||||
options, err := readConfigAndMerge()
|
|
||||||
if err != nil {
|
|
||||||
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create service")
|
|
||||||
}
|
|
||||||
err = instance.PreStart()
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "start service")
|
|
||||||
}
|
|
||||||
return instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
|
||||||
if outboundTag == "" {
|
|
||||||
return instance.Outbound().Default(), nil
|
|
||||||
} else {
|
|
||||||
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("outbound not found: ", outboundTag)
|
|
||||||
}
|
|
||||||
return outbound, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/task"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandConnectFlagNetwork string
|
|
||||||
|
|
||||||
var commandConnect = &cobra.Command{
|
|
||||||
Use: "connect <address>",
|
|
||||||
Short: "Connect to an address",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := connect(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
|
|
||||||
commandTools.AddCommand(commandConnect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func connect(address string) error {
|
|
||||||
switch N.NetworkName(commandConnectFlagNetwork) {
|
|
||||||
case N.NetworkTCP, N.NetworkUDP:
|
|
||||||
default:
|
|
||||||
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
|
|
||||||
}
|
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "connect to server")
|
|
||||||
}
|
|
||||||
var group task.Group
|
|
||||||
group.Append("upload", func(ctx context.Context) error {
|
|
||||||
return common.Error(bufio.Copy(conn, os.Stdin))
|
|
||||||
})
|
|
||||||
group.Append("download", func(ctx context.Context) error {
|
|
||||||
return common.Error(bufio.Copy(os.Stdout, conn))
|
|
||||||
})
|
|
||||||
group.Cleanup(func() {
|
|
||||||
conn.Close()
|
|
||||||
})
|
|
||||||
err = group.Run(context.Background())
|
|
||||||
if E.IsClosed(err) {
|
|
||||||
log.Info(err)
|
|
||||||
} else {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandFetch = &cobra.Command{
|
|
||||||
Use: "fetch",
|
|
||||||
Short: "Fetch an URL",
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := fetch(args)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandTools.AddCommand(commandFetch)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
httpClient *http.Client
|
|
||||||
http3Client *http.Client
|
|
||||||
)
|
|
||||||
|
|
||||||
func fetch(args []string) error {
|
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
httpClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
},
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
defer httpClient.CloseIdleConnections()
|
|
||||||
if C.WithQUIC {
|
|
||||||
err = initializeHTTP3Client(instance)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer http3Client.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
for _, urlString := range args {
|
|
||||||
var parsedURL *url.URL
|
|
||||||
parsedURL, err = url.Parse(urlString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch parsedURL.Scheme {
|
|
||||||
case "":
|
|
||||||
parsedURL.Scheme = "http"
|
|
||||||
fallthrough
|
|
||||||
case "http", "https":
|
|
||||||
err = fetchHTTP(httpClient, parsedURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "http3":
|
|
||||||
if !C.WithQUIC {
|
|
||||||
return C.ErrQUICNotIncluded
|
|
||||||
}
|
|
||||||
parsedURL.Scheme = "https"
|
|
||||||
err = fetchHTTP(http3Client, parsedURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return E.New("unsupported scheme: ", parsedURL.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
|
|
||||||
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
request.Header.Add("User-Agent", "curl/7.88.0")
|
|
||||||
response, err := httpClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
_, err = bufio.Copy(os.Stdout, response.Body)
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/quic-go/http3"
|
|
||||||
box "github.com/sagernet/sing-box"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initializeHTTP3Client(instance *box.Box) error {
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
http3Client = &http.Client{
|
|
||||||
Transport: &http3.Transport{
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
|
||||||
destination := M.ParseSocksaddr(addr)
|
|
||||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
|
||||||
if dErr != nil {
|
|
||||||
return nil, dErr
|
|
||||||
}
|
|
||||||
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
//go:build !with_quic
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
box "github.com/sagernet/sing-box"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initializeHTTP3Client(instance *box.Box) error {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchHTTP3(parsedURL *url.URL) error {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
108
cmd/sing-box/cmd_tools_install_ca.go
Normal file
108
cmd/sing-box/cmd_tools_install_ca.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/shell"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandInstallCACertificate = &cobra.Command{
|
||||||
|
Use: "install-ca <path to certificate>",
|
||||||
|
Short: "Install CA certificate to system",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := installCACertificate(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandTools.AddCommand(commandInstallCACertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func installCACertificate(path string) error {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return shell.Exec("powershell", "-Command", "Import-Certificate -FilePath \""+path+"\" -CertStoreLocation Cert:\\LocalMachine\\Root").Attach().Run()
|
||||||
|
case "darwin":
|
||||||
|
return shell.Exec("sudo", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", path).Attach().Run()
|
||||||
|
case "linux":
|
||||||
|
updateCertPath, updateCertPathNotFoundErr := exec.LookPath("update-ca-certificates")
|
||||||
|
if updateCertPathNotFoundErr == nil {
|
||||||
|
publicDer, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.MkdirAll("/usr/local/share/ca-certificates", 0o755)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrPermission) {
|
||||||
|
log.Info("Try running with sudo")
|
||||||
|
return shell.Exec("sudo", os.Args...).Attach().Run()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileName := filepath.Base(updateCertPath)
|
||||||
|
if !strings.HasSuffix(fileName, ".crt") {
|
||||||
|
fileName = fileName + ".crt"
|
||||||
|
}
|
||||||
|
filePath, _ := filepath.Abs(filepath.Join("/usr/local/share/ca-certificates", fileName))
|
||||||
|
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrPermission) {
|
||||||
|
log.Info("Try running with sudo")
|
||||||
|
return shell.Exec("sudo", os.Args...).Attach().Run()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("certificate written to " + filePath + "\n")
|
||||||
|
err = shell.Exec(updateCertPath).Attach().Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("certificate installed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updateTrustPath, updateTrustPathNotFoundErr := exec.LookPath("update-ca-trust")
|
||||||
|
if updateTrustPathNotFoundErr == nil {
|
||||||
|
publicDer, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileName := filepath.Base(updateTrustPath)
|
||||||
|
fileExt := filepath.Ext(path)
|
||||||
|
if fileExt != "" {
|
||||||
|
fileName = fileName[:len(fileName)-len(fileExt)]
|
||||||
|
}
|
||||||
|
filePath, _ := filepath.Abs(filepath.Join("/etc/pki/ca-trust/source/anchors/", fileName+".pem"))
|
||||||
|
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrPermission) {
|
||||||
|
log.Info("Try running with sudo")
|
||||||
|
return shell.Exec("sudo", os.Args...).Attach().Run()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("certificate written to " + filePath + "\n")
|
||||||
|
err = shell.Exec(updateTrustPath, "extract").Attach().Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("certificate installed")
|
||||||
|
}
|
||||||
|
return E.New("update-ca-certificates or update-ca-trust not found")
|
||||||
|
default:
|
||||||
|
return E.New("unsupported operating system: ", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -39,20 +40,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncTime() error {
|
func syncTime() error {
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
||||||
if serverAddress.Port == 0 {
|
if serverAddress.Port == 0 {
|
||||||
serverAddress.Port = 123
|
serverAddress.Port = 123
|
||||||
}
|
}
|
||||||
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
//go:build go1.21 && !without_badtls && with_ech
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
|
||||||
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
|
||||||
if !loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return true, func() error {
|
|
||||||
return echReadRecord(tlsConn)
|
|
||||||
}, func() error {
|
|
||||||
return echHandlePostHandshakeMessage(tlsConn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
|
||||||
func echReadRecord(c *tls.Conn) error
|
|
||||||
|
|
||||||
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
|
||||||
4394
common/certificate/mozilla.go
Normal file
4394
common/certificate/mozilla.go
Normal file
File diff suppressed because it is too large
Load Diff
216
common/certificate/store.go
Normal file
216
common/certificate/store.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package certificate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/fswatch"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"software.sslmate.com/src/go-pkcs12"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
systemPool *x509.CertPool
|
||||||
|
currentPool *x509.CertPool
|
||||||
|
certificate string
|
||||||
|
certificatePaths []string
|
||||||
|
certificateDirectoryPaths []string
|
||||||
|
watcher *fswatch.Watcher
|
||||||
|
tlsDecryptionEnabled bool
|
||||||
|
tlsDecryptionPrivateKey any
|
||||||
|
tlsDecryptionCertificate *x509.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||||
|
var systemPool *x509.CertPool
|
||||||
|
switch options.Store {
|
||||||
|
case C.CertificateStoreSystem, "":
|
||||||
|
systemPool = x509.NewCertPool()
|
||||||
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
|
var systemValid bool
|
||||||
|
if platformInterface != nil {
|
||||||
|
for _, cert := range platformInterface.SystemCertificates() {
|
||||||
|
if systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||||
|
systemValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !systemValid {
|
||||||
|
certPool, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
systemPool = certPool
|
||||||
|
}
|
||||||
|
case C.CertificateStoreMozilla:
|
||||||
|
systemPool = mozillaIncluded
|
||||||
|
case C.CertificateStoreNone:
|
||||||
|
systemPool = nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate store: ", options.Store)
|
||||||
|
}
|
||||||
|
store := &Store{
|
||||||
|
systemPool: systemPool,
|
||||||
|
certificate: strings.Join(options.Certificate, "\n"),
|
||||||
|
certificatePaths: options.CertificatePath,
|
||||||
|
certificateDirectoryPaths: options.CertificateDirectoryPath,
|
||||||
|
}
|
||||||
|
var watchPaths []string
|
||||||
|
for _, target := range options.CertificatePath {
|
||||||
|
watchPaths = append(watchPaths, target)
|
||||||
|
}
|
||||||
|
for _, target := range options.CertificateDirectoryPath {
|
||||||
|
watchPaths = append(watchPaths, target)
|
||||||
|
}
|
||||||
|
if len(watchPaths) > 0 {
|
||||||
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
|
Path: watchPaths,
|
||||||
|
Logger: logger,
|
||||||
|
Callback: func(_ string) {
|
||||||
|
err := store.update()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(E.Cause(err, "reload certificates"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
|
||||||
|
}
|
||||||
|
store.watcher = watcher
|
||||||
|
}
|
||||||
|
err := store.update()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initializing certificate store")
|
||||||
|
}
|
||||||
|
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
|
||||||
|
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode key pair base64 bytes")
|
||||||
|
}
|
||||||
|
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode key pair")
|
||||||
|
}
|
||||||
|
store.tlsDecryptionEnabled = true
|
||||||
|
store.tlsDecryptionPrivateKey = privateKey
|
||||||
|
store.tlsDecryptionCertificate = certificate
|
||||||
|
}
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Name() string {
|
||||||
|
return "certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.watcher != nil {
|
||||||
|
return s.watcher.Start()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Close() error {
|
||||||
|
if s.watcher != nil {
|
||||||
|
return s.watcher.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Pool() *x509.CertPool {
|
||||||
|
return s.currentPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) update() error {
|
||||||
|
var currentPool *x509.CertPool
|
||||||
|
if s.systemPool == nil {
|
||||||
|
currentPool = x509.NewCertPool()
|
||||||
|
} else {
|
||||||
|
currentPool = s.systemPool.Clone()
|
||||||
|
}
|
||||||
|
if s.certificate != "" {
|
||||||
|
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||||
|
return E.New("invalid certificate PEM strings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, path := range s.certificatePaths {
|
||||||
|
pemContent, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||||
|
return E.New("invalid certificate PEM file: ", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var firstErr error
|
||||||
|
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||||
|
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil && !os.IsNotExist(err) {
|
||||||
|
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, directoryEntry := range directoryEntries {
|
||||||
|
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||||
|
if err == nil {
|
||||||
|
currentPool.AppendCertsFromPEM(pemContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if firstErr != nil {
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
s.currentPool = currentPool
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uniq := files[:0]
|
||||||
|
for _, f := range files {
|
||||||
|
if !isSameDirSymlink(f, dir) {
|
||||||
|
uniq = append(uniq, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||||
|
if f.Type()&fs.ModeSymlink == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||||
|
return err == nil && !strings.Contains(target, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) TLSDecryptionEnabled() bool {
|
||||||
|
return s.tlsDecryptionEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
|
||||||
|
return s.tlsDecryptionCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) TLSDecryptionPrivateKey() any {
|
||||||
|
return s.tlsDecryptionPrivateKey
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/conntrack"
|
"github.com/sagernet/sing-box/common/conntrack"
|
||||||
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -35,7 +36,7 @@ type DefaultDialer struct {
|
|||||||
udpListener net.ListenConfig
|
udpListener net.ListenConfig
|
||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
isWireGuardListener bool
|
netns string
|
||||||
networkManager adapter.NetworkManager
|
networkManager adapter.NetworkManager
|
||||||
networkStrategy *C.NetworkStrategy
|
networkStrategy *C.NetworkStrategy
|
||||||
defaultNetworkStrategy bool
|
defaultNetworkStrategy bool
|
||||||
@@ -183,11 +184,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
}
|
}
|
||||||
setMultiPathTCP(&dialer4)
|
setMultiPathTCP(&dialer4)
|
||||||
}
|
}
|
||||||
if options.IsWireGuardListener {
|
|
||||||
for _, controlFn := range WgControlFns {
|
|
||||||
listener.Control = control.Append(listener.Control, controlFn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -204,7 +200,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
udpListener: listener,
|
udpListener: listener,
|
||||||
udpAddr4: udpAddr4,
|
udpAddr4: udpAddr4,
|
||||||
udpAddr6: udpAddr6,
|
udpAddr6: udpAddr6,
|
||||||
isWireGuardListener: options.IsWireGuardListener,
|
netns: options.NetNs,
|
||||||
networkManager: networkManager,
|
networkManager: networkManager,
|
||||||
networkStrategy: networkStrategy,
|
networkStrategy: networkStrategy,
|
||||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||||
@@ -217,21 +213,25 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
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() {
|
||||||
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
switch N.NetworkName(network) {
|
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||||
case N.NetworkUDP:
|
switch N.NetworkName(network) {
|
||||||
if !address.IsIPv6() {
|
case N.NetworkUDP:
|
||||||
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
|
if !address.IsIPv6() {
|
||||||
} else {
|
return d.udpDialer4.DialContext(ctx, network, address.String())
|
||||||
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
|
} else {
|
||||||
|
return d.udpDialer6.DialContext(ctx, network, address.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if !address.IsIPv6() {
|
||||||
if !address.IsIPv6() {
|
return DialSlowContext(&d.dialer4, ctx, network, address)
|
||||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
} else {
|
||||||
} else {
|
return DialSlowContext(&d.dialer6, ctx, network, address)
|
||||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
}
|
||||||
}
|
}))
|
||||||
} else {
|
} else {
|
||||||
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||||
}
|
}
|
||||||
@@ -287,13 +287,15 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
|
|
||||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
if destination.IsIPv6() {
|
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
if destination.IsIPv6() {
|
||||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
|
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
} else {
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
|
} else {
|
||||||
}
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
|
||||||
|
}
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,20 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DirectDialer interface {
|
||||||
|
IsEmpty() bool
|
||||||
|
}
|
||||||
|
|
||||||
type DetourDialer struct {
|
type DetourDialer struct {
|
||||||
outboundManager adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
detour string
|
detour string
|
||||||
|
directResolver bool
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
initErr error
|
initErr error
|
||||||
@@ -23,22 +29,34 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer
|
|||||||
return &DetourDialer{outboundManager: outboundManager, detour: detour}
|
return &DetourDialer{outboundManager: outboundManager, detour: detour}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) Start() error {
|
func InitializeDetour(dialer N.Dialer) error {
|
||||||
_, err := d.Dialer()
|
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||||
return err
|
if !isDetour {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return common.Error(detourDialer.Dialer())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||||
d.initOnce.Do(func() {
|
d.initOnce.Do(d.init)
|
||||||
var loaded bool
|
|
||||||
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
|
||||||
if !loaded {
|
|
||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return d.dialer, d.initErr
|
return d.dialer, d.initErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DetourDialer) init() {
|
||||||
|
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||||
|
if !loaded {
|
||||||
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||||
|
if directDialer.IsEmpty() {
|
||||||
|
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.dialer = dialer
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
dialer, err := d.Dialer()
|
dialer, err := d.Dialer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,68 +8,121 @@ import (
|
|||||||
|
|
||||||
"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/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
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"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
|
type Options struct {
|
||||||
if options.IsWireGuardListener {
|
Context context.Context
|
||||||
return NewDefault(ctx, options)
|
Options option.DialerOptions
|
||||||
}
|
RemoteIsDomain bool
|
||||||
|
DirectResolver bool
|
||||||
|
ResolverOnDetour bool
|
||||||
|
NewDialer bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: merge with NewWithOptions
|
||||||
|
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
|
||||||
|
return NewWithOptions(Options{
|
||||||
|
Context: ctx,
|
||||||
|
Options: options,
|
||||||
|
RemoteIsDomain: remoteIsDomain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithOptions(options Options) (N.Dialer, error) {
|
||||||
|
dialOptions := options.Options
|
||||||
var (
|
var (
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if options.Detour == "" {
|
if dialOptions.Detour != "" {
|
||||||
dialer, err = NewDefault(ctx, options)
|
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
|
|
||||||
if outboundManager == nil {
|
if outboundManager == nil {
|
||||||
return nil, E.New("missing outbound manager")
|
return nil, E.New("missing outbound manager")
|
||||||
}
|
}
|
||||||
dialer = NewDetour(outboundManager, options.Detour)
|
dialer = NewDetour(outboundManager, dialOptions.Detour)
|
||||||
}
|
} else {
|
||||||
if options.Detour == "" {
|
dialer, err = NewDefault(options.Context, dialOptions)
|
||||||
router := service.FromContext[adapter.Router](ctx)
|
if err != nil {
|
||||||
if router != nil {
|
return nil, err
|
||||||
dialer = NewResolveDialer(
|
|
||||||
router,
|
|
||||||
dialer,
|
|
||||||
options.Detour == "" && !options.TCPFastOpen,
|
|
||||||
dns.DomainStrategy(options.DomainStrategy),
|
|
||||||
time.Duration(options.FallbackDelay))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||||
|
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||||
|
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||||
|
var defaultOptions adapter.NetworkOptions
|
||||||
|
if networkManager != nil {
|
||||||
|
defaultOptions = networkManager.DefaultOptions()
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
server string
|
||||||
|
dnsQueryOptions adapter.DNSQueryOptions
|
||||||
|
resolveFallbackDelay time.Duration
|
||||||
|
)
|
||||||
|
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
|
||||||
|
var transport adapter.DNSTransport
|
||||||
|
if !options.DirectResolver {
|
||||||
|
var loaded bool
|
||||||
|
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var strategy C.DomainStrategy
|
||||||
|
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||||
|
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
|
||||||
|
} else if
|
||||||
|
//nolint:staticcheck
|
||||||
|
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||||
|
//nolint:staticcheck
|
||||||
|
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||||
|
}
|
||||||
|
server = dialOptions.DomainResolver.Server
|
||||||
|
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||||
|
Transport: transport,
|
||||||
|
Strategy: strategy,
|
||||||
|
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||||
|
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||||
|
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||||
|
}
|
||||||
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
|
} else if options.DirectResolver {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
|
} else if defaultOptions.DomainResolver != "" {
|
||||||
|
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||||
|
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||||
|
}
|
||||||
|
dnsQueryOptions.Transport = transport
|
||||||
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
|
} else if options.NewDialer {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
|
} else {
|
||||||
|
transports := dnsTransport.Transports()
|
||||||
|
if len(transports) < 2 {
|
||||||
|
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||||
|
} else {
|
||||||
|
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialer = NewResolveDialer(
|
||||||
|
options.Context,
|
||||||
|
dialer,
|
||||||
|
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
|
||||||
|
server,
|
||||||
|
dnsQueryOptions,
|
||||||
|
resolveFallbackDelay,
|
||||||
|
)
|
||||||
|
}
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) {
|
|
||||||
if options.Detour != "" {
|
|
||||||
return nil, E.New("`detour` is not supported in direct context")
|
|
||||||
}
|
|
||||||
if options.IsWireGuardListener {
|
|
||||||
return NewDefault(ctx, options)
|
|
||||||
}
|
|
||||||
dialer, err := NewDefault(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewResolveParallelInterfaceDialer(
|
|
||||||
service.FromContext[adapter.Router](ctx),
|
|
||||||
dialer,
|
|
||||||
true,
|
|
||||||
dns.DomainStrategy(options.DomainStrategy),
|
|
||||||
time.Duration(options.FallbackDelay),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParallelInterfaceDialer interface {
|
type ParallelInterfaceDialer interface {
|
||||||
N.Dialer
|
N.Dialer
|
||||||
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ package dialer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
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"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -20,21 +21,51 @@ var (
|
|||||||
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResolveDialer interface {
|
||||||
|
N.Dialer
|
||||||
|
QueryOptions() adapter.DNSQueryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParallelInterfaceResolveDialer interface {
|
||||||
|
ParallelInterfaceDialer
|
||||||
|
QueryOptions() adapter.DNSQueryOptions
|
||||||
|
}
|
||||||
|
|
||||||
type resolveDialer struct {
|
type resolveDialer struct {
|
||||||
|
transport adapter.DNSTransportManager
|
||||||
|
router adapter.DNSRouter
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
parallel bool
|
parallel bool
|
||||||
router adapter.Router
|
server string
|
||||||
strategy dns.DomainStrategy
|
initOnce sync.Once
|
||||||
|
initErr error
|
||||||
|
queryOptions adapter.DNSQueryOptions
|
||||||
fallbackDelay time.Duration
|
fallbackDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
|
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
|
||||||
|
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
|
||||||
|
return &resolveParallelNetworkDialer{
|
||||||
|
resolveDialer{
|
||||||
|
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||||
|
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||||
|
dialer: dialer,
|
||||||
|
parallel: parallel,
|
||||||
|
server: server,
|
||||||
|
queryOptions: queryOptions,
|
||||||
|
fallbackDelay: fallbackDelay,
|
||||||
|
},
|
||||||
|
parallelDialer,
|
||||||
|
}
|
||||||
|
}
|
||||||
return &resolveDialer{
|
return &resolveDialer{
|
||||||
dialer,
|
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||||
parallel,
|
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||||
router,
|
dialer: dialer,
|
||||||
strategy,
|
parallel: parallel,
|
||||||
fallbackDelay,
|
server: server,
|
||||||
|
queryOptions: queryOptions,
|
||||||
|
fallbackDelay: fallbackDelay,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,59 +74,53 @@ type resolveParallelNetworkDialer struct {
|
|||||||
dialer ParallelInterfaceDialer
|
dialer ParallelInterfaceDialer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolveParallelInterfaceDialer(router adapter.Router, dialer ParallelInterfaceDialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
|
func (d *resolveDialer) initialize() error {
|
||||||
return &resolveParallelNetworkDialer{
|
d.initOnce.Do(d.initServer)
|
||||||
resolveDialer{
|
return d.initErr
|
||||||
dialer,
|
}
|
||||||
parallel,
|
|
||||||
router,
|
func (d *resolveDialer) initServer() {
|
||||||
strategy,
|
if d.server == "" {
|
||||||
fallbackDelay,
|
return
|
||||||
},
|
|
||||||
dialer,
|
|
||||||
}
|
}
|
||||||
|
transport, loaded := d.transport.Transport(d.server)
|
||||||
|
if !loaded {
|
||||||
|
d.initErr = E.New("domain resolver not found: " + d.server)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.queryOptions.Transport = transport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
metadata.Destination = destination
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
metadata.Domain = ""
|
|
||||||
var addresses []netip.Addr
|
|
||||||
var err error
|
|
||||||
if d.strategy == dns.DomainStrategyAsIS {
|
|
||||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
|
||||||
} else {
|
|
||||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if d.parallel {
|
if d.parallel {
|
||||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
|
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
metadata.Destination = destination
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
metadata.Domain = ""
|
|
||||||
var addresses []netip.Addr
|
|
||||||
var err error
|
|
||||||
if d.strategy == dns.DomainStrategyAsIS {
|
|
||||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
|
||||||
} else {
|
|
||||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -106,21 +131,24 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||||
|
return d.queryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *resolveDialer) Upstream() any {
|
||||||
|
return d.dialer
|
||||||
|
}
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
metadata.Destination = destination
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
metadata.Domain = ""
|
|
||||||
var addresses []netip.Addr
|
|
||||||
var err error
|
|
||||||
if d.strategy == dns.DomainStrategyAsIS {
|
|
||||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
|
||||||
} else {
|
|
||||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -128,30 +156,28 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
|||||||
fallbackDelay = d.fallbackDelay
|
fallbackDelay = d.fallbackDelay
|
||||||
}
|
}
|
||||||
if d.parallel {
|
if d.parallel {
|
||||||
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
metadata.Destination = destination
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
metadata.Domain = ""
|
|
||||||
var addresses []netip.Addr
|
|
||||||
var err error
|
|
||||||
if d.strategy == dns.DomainStrategyAsIS {
|
|
||||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
|
||||||
} else {
|
|
||||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if fallbackDelay == 0 {
|
||||||
|
fallbackDelay = d.fallbackDelay
|
||||||
|
}
|
||||||
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -159,6 +185,10 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) Upstream() any {
|
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||||
|
return d.queryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *resolveParallelNetworkDialer) Upstream() any {
|
||||||
return d.dialer
|
return d.dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,24 +7,27 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
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"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DefaultOutboundDialer struct {
|
type DefaultOutboundDialer struct {
|
||||||
outboundManager adapter.OutboundManager
|
outbound adapter.OutboundManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer {
|
func NewDefaultOutbound(ctx context.Context) N.Dialer {
|
||||||
return &DefaultOutboundDialer{outboundManager: outboundManager}
|
return &DefaultOutboundDialer{
|
||||||
|
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
return d.outboundManager.Default().DialContext(ctx, network, destination)
|
return d.outbound.Default().DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
return d.outboundManager.Default().ListenPacket(ctx, destination)
|
return d.outbound.Default().ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultOutboundDialer) Upstream() any {
|
func (d *DefaultOutboundDialer) Upstream() any {
|
||||||
return d.outboundManager.Default()
|
return d.outbound.Default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -14,6 +16,8 @@ import (
|
|||||||
"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"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener struct {
|
type Listener struct {
|
||||||
@@ -135,3 +139,30 @@ func (l *Listener) UDPConn() *net.UDPConn {
|
|||||||
func (l *Listener) ListenOptions() option.ListenOptions {
|
func (l *Listener) ListenOptions() option.ListenOptions {
|
||||||
return l.listenOptions
|
return l.listenOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
||||||
|
if nameOrPath != "" {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
currentNs, err := netns.Get()
|
||||||
|
if err != nil {
|
||||||
|
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||||
|
}
|
||||||
|
defer netns.Set(currentNs)
|
||||||
|
var targetNs netns.NsHandle
|
||||||
|
if strings.HasPrefix(nameOrPath, "/") {
|
||||||
|
targetNs, err = netns.GetFromPath(nameOrPath)
|
||||||
|
} else {
|
||||||
|
targetNs, err = netns.GetFromName(nameOrPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
||||||
|
}
|
||||||
|
defer targetNs.Close()
|
||||||
|
err = netns.Set(targetNs)
|
||||||
|
if err != nil {
|
||||||
|
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return block()
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (l *Listener) ListenTCP() (net.Listener, error) {
|
func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||||
|
//nolint:staticcheck
|
||||||
|
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
||||||
|
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
|
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
|
||||||
var tcpListener net.Listener
|
|
||||||
var listenConfig net.ListenConfig
|
var listenConfig net.ListenConfig
|
||||||
if l.listenOptions.TCPKeepAlive >= 0 {
|
if l.listenOptions.TCPKeepAlive >= 0 {
|
||||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||||
@@ -37,20 +40,19 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
|||||||
}
|
}
|
||||||
setMultiPathTCP(&listenConfig)
|
setMultiPathTCP(&listenConfig)
|
||||||
}
|
}
|
||||||
if l.listenOptions.TCPFastOpen {
|
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
|
||||||
var tfoConfig tfo.ListenConfig
|
if l.listenOptions.TCPFastOpen {
|
||||||
tfoConfig.ListenConfig = listenConfig
|
var tfoConfig tfo.ListenConfig
|
||||||
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
tfoConfig.ListenConfig = listenConfig
|
||||||
} else {
|
return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||||
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
} else {
|
||||||
}
|
return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||||
if err == nil {
|
}
|
||||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
})
|
||||||
}
|
if err != nil {
|
||||||
//nolint:staticcheck
|
return nil, err
|
||||||
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
|
||||||
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
|
|
||||||
}
|
}
|
||||||
|
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
||||||
l.tcpListener = tcpListener
|
l.tcpListener = tcpListener
|
||||||
return tcpListener, err
|
return tcpListener, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package listener
|
package listener
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -24,7 +25,9 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
|||||||
if !udpFragment {
|
if !udpFragment {
|
||||||
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
|
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
|
||||||
}
|
}
|
||||||
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||||
|
return lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -34,6 +37,13 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
|||||||
return udpConn, err
|
return udpConn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Listener) ListenPacket(ctx context.Context, network string, address string) (net.PacketConn, error) {
|
||||||
|
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||||
|
var listenConfig net.ListenConfig
|
||||||
|
return listenConfig.ListenPacket(ctx, network, address)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Listener) UDPAddr() M.Socksaddr {
|
func (l *Listener) UDPAddr() M.Socksaddr {
|
||||||
return l.udpAddr
|
return l.udpAddr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
|
ProcessID uint32
|
||||||
ProcessPath string
|
ProcessPath string
|
||||||
PackageName string
|
PackageName string
|
||||||
User string
|
User string
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
"github.com/sagernet/sing/common/winiphlpapi"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
@@ -26,209 +23,39 @@ func NewSearcher(_ Config) (Searcher, error) {
|
|||||||
return &windowsSearcher{}, nil
|
return &windowsSearcher{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
|
||||||
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
|
|
||||||
procGetExtendedUdpTable = modiphlpapi.NewProc("GetExtendedUdpTable")
|
|
||||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
||||||
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
|
|
||||||
)
|
|
||||||
|
|
||||||
func initWin32API() error {
|
func initWin32API() error {
|
||||||
err := modiphlpapi.Load()
|
return winiphlpapi.LoadExtendedTable()
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "load iphlpapi.dll")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = procGetExtendedTcpTable.Find()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "load iphlpapi::GetExtendedTcpTable")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = procGetExtendedUdpTable.Find()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "load iphlpapi::GetExtendedUdpTable")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = modkernel32.Load()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "load kernel32.dll")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = procQueryFullProcessImageNameW.Find()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "load kernel32::QueryFullProcessImageNameW")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
pid, err := winiphlpapi.FindPid(network, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Info{ProcessPath: processName, UserId: -1}, nil
|
path, err := getProcessPath(pid)
|
||||||
}
|
|
||||||
|
|
||||||
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
|
|
||||||
family := windows.AF_INET
|
|
||||||
if ip.Is6() {
|
|
||||||
family = windows.AF_INET6
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
tcpTablePidConn = 4
|
|
||||||
udpTablePid = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
var class int
|
|
||||||
var fn uintptr
|
|
||||||
switch network {
|
|
||||||
case N.NetworkTCP:
|
|
||||||
fn = procGetExtendedTcpTable.Addr()
|
|
||||||
class = tcpTablePidConn
|
|
||||||
case N.NetworkUDP:
|
|
||||||
fn = procGetExtendedUdpTable.Addr()
|
|
||||||
class = udpTablePid
|
|
||||||
default:
|
|
||||||
return "", os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := getTransportTable(fn, family, class)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return &Info{ProcessID: pid, UserId: -1}, err
|
||||||
}
|
}
|
||||||
|
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
||||||
s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
|
|
||||||
|
|
||||||
pid, err := s.Search(buf, ip, uint16(srcPort))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return getExecPathFromPID(pid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type searcher struct {
|
func getProcessPath(pid uint32) (string, error) {
|
||||||
itemSize int
|
|
||||||
port int
|
|
||||||
ip int
|
|
||||||
ipSize int
|
|
||||||
pid int
|
|
||||||
tcpState int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
|
|
||||||
n := int(readNativeUint32(b[:4]))
|
|
||||||
itemSize := s.itemSize
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
|
||||||
|
|
||||||
if s.tcpState >= 0 {
|
|
||||||
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
|
|
||||||
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
|
|
||||||
if tcpState != 5 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
|
||||||
// this field can be illustrated as follows depends on different machine endianess:
|
|
||||||
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
|
||||||
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
|
|
||||||
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
|
|
||||||
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
|
|
||||||
if srcPort != port {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
|
|
||||||
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
|
||||||
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pid := readNativeUint32(row[s.pid : s.pid+4])
|
|
||||||
return pid, nil
|
|
||||||
}
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSearcher(isV4, isTCP bool) *searcher {
|
|
||||||
var itemSize, port, ip, ipSize, pid int
|
|
||||||
tcpState := -1
|
|
||||||
switch {
|
|
||||||
case isV4 && isTCP:
|
|
||||||
// struct MIB_TCPROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
|
|
||||||
case isV4 && !isTCP:
|
|
||||||
// struct MIB_UDPROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
|
|
||||||
case !isV4 && isTCP:
|
|
||||||
// struct MIB_TCP6ROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
|
|
||||||
case !isV4 && !isTCP:
|
|
||||||
// struct MIB_UDP6ROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
|
|
||||||
}
|
|
||||||
|
|
||||||
return &searcher{
|
|
||||||
itemSize: itemSize,
|
|
||||||
port: port,
|
|
||||||
ip: ip,
|
|
||||||
ipSize: ipSize,
|
|
||||||
pid: pid,
|
|
||||||
tcpState: tcpState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
|
|
||||||
for size, buf := uint32(8), make([]byte, 8); ; {
|
|
||||||
ptr := unsafe.Pointer(&buf[0])
|
|
||||||
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
|
|
||||||
|
|
||||||
switch err {
|
|
||||||
case 0:
|
|
||||||
return buf, nil
|
|
||||||
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
|
|
||||||
buf = make([]byte, size)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("syscall error: %d", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readNativeUint32(b []byte) uint32 {
|
|
||||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExecPathFromPID(pid uint32) (string, error) {
|
|
||||||
// kernel process starts with a colon in order to distinguish with normal processes
|
|
||||||
switch pid {
|
switch pid {
|
||||||
case 0:
|
case 0:
|
||||||
// reserved pid for system idle process
|
|
||||||
return ":System Idle Process", nil
|
return ":System Idle Process", nil
|
||||||
case 4:
|
case 4:
|
||||||
// reserved pid for windows kernel image
|
|
||||||
return ":System", nil
|
return ":System", nil
|
||||||
}
|
}
|
||||||
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer windows.CloseHandle(h)
|
defer windows.CloseHandle(handle)
|
||||||
|
size := uint32(syscall.MAX_LONG_PATH)
|
||||||
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
||||||
size := uint32(len(buf))
|
err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
|
||||||
r1, _, err := syscall.SyscallN(
|
if err != nil {
|
||||||
procQueryFullProcessImageNameW.Addr(),
|
|
||||||
uintptr(h),
|
|
||||||
uintptr(0),
|
|
||||||
uintptr(unsafe.Pointer(&buf[0])),
|
|
||||||
uintptr(unsafe.Pointer(&size)),
|
|
||||||
)
|
|
||||||
if r1 == 0 {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return syscall.UTF16ToString(buf[:size]), nil
|
return windows.UTF16ToString(buf[:size]), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
|
|||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolHTTP
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
|
metadata.HTTPRequest = request
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
58
common/sniff/ntp.go
Normal file
58
common/sniff/ntp.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package sniff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
|
// NTP packets must be at least 48 bytes long (standard NTP header size).
|
||||||
|
pLen := len(packet)
|
||||||
|
if pLen < 48 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
// Check the LI (Leap Indicator) and Version Number (VN) in the first byte.
|
||||||
|
// We'll primarily focus on ensuring the version is valid for NTP.
|
||||||
|
// Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations)
|
||||||
|
firstByte := packet[0]
|
||||||
|
li := (firstByte >> 6) & 0x03 // Extract LI
|
||||||
|
vn := (firstByte >> 3) & 0x07 // Extract VN
|
||||||
|
mode := firstByte & 0x07 // Extract Mode
|
||||||
|
|
||||||
|
// Leap Indicator should be a valid value (0-3).
|
||||||
|
if li > 3 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version Check (common NTP versions are 3 and 4)
|
||||||
|
if vn != 3 && vn != 4 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the Mode field for a client request (Mode 3). This validates it *is* a request.
|
||||||
|
if mode != 3 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Root Delay and Root Dispersion. While not strictly *required* for a request,
|
||||||
|
// we can check if they appear to be reasonable values (not excessively large).
|
||||||
|
rootDelay := binary.BigEndian.Uint32(packet[4:8])
|
||||||
|
rootDispersion := binary.BigEndian.Uint32(packet[8:12])
|
||||||
|
|
||||||
|
// Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds.
|
||||||
|
// Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds.
|
||||||
|
if float64(rootDelay)/65536.0 > 16.0 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
if float64(rootDispersion)/65536.0 > 16.0 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Protocol = C.ProtocolNTP
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
common/sniff/ntp_test.go
Normal file
33
common/sniff/ntp_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package sniff_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSniffNTP(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.NTP(context.Background(), &metadata, packet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolNTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffNTPFailed(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.NTP(context.Background(), &metadata, packet)
|
||||||
|
require.ErrorIs(t, err, os.ErrInvalid)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
|||||||
if clientHello != nil {
|
if clientHello != nil {
|
||||||
metadata.Protocol = C.ProtocolTLS
|
metadata.Protocol = C.ProtocolTLS
|
||||||
metadata.Domain = clientHello.ServerName
|
metadata.Domain = clientHello.ServerName
|
||||||
|
metadata.ClientHello = clientHello
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/libdns/alidns"
|
"github.com/libdns/alidns"
|
||||||
"github.com/libdns/cloudflare"
|
"github.com/libdns/cloudflare"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v3/acme"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,15 +29,12 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
|
|||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.Reality != nil && options.Reality.Enabled {
|
||||||
return NewECHClient(ctx, serverAddress, options)
|
|
||||||
} else if options.Reality != nil && options.Reality.Enabled {
|
|
||||||
return NewRealityClient(ctx, serverAddress, options)
|
return NewRealityClient(ctx, serverAddress, options)
|
||||||
} else if options.UTLS != nil && options.UTLS.Enabled {
|
} else if options.UTLS != nil && options.UTLS.Enabled {
|
||||||
return NewUTLSClient(ctx, serverAddress, options)
|
return NewUTLSClient(ctx, serverAddress, options)
|
||||||
} else {
|
|
||||||
return NewSTDClient(ctx, serverAddress, options)
|
|
||||||
}
|
}
|
||||||
|
return NewSTDClient(ctx, serverAddress, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||||
|
|||||||
174
common/tls/ech.go
Normal file
174
common/tls/ech.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
//go:build go1.24
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||||
|
var echConfig []byte
|
||||||
|
if len(options.ECH.Config) > 0 {
|
||||||
|
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
||||||
|
} else if options.ECH.ConfigPath != "" {
|
||||||
|
content, err := os.ReadFile(options.ECH.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read ECH config")
|
||||||
|
}
|
||||||
|
echConfig = content
|
||||||
|
}
|
||||||
|
//nolint:staticcheck
|
||||||
|
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||||
|
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
|
||||||
|
}
|
||||||
|
if len(echConfig) > 0 {
|
||||||
|
block, rest := pem.Decode(echConfig)
|
||||||
|
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
||||||
|
return nil, E.New("invalid ECH configs pem")
|
||||||
|
}
|
||||||
|
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
|
||||||
|
return &STDClientConfig{tlsConfig}, nil
|
||||||
|
} else {
|
||||||
|
return &STDECHClientConfig{STDClientConfig{tlsConfig}, service.FromContext[adapter.DNSRouter](ctx)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
|
||||||
|
var echKey []byte
|
||||||
|
if len(options.ECH.Key) > 0 {
|
||||||
|
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
|
||||||
|
} else if options.ECH.KeyPath != "" {
|
||||||
|
content, err := os.ReadFile(options.ECH.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read ECH keys")
|
||||||
|
}
|
||||||
|
echKey = content
|
||||||
|
*echKeyPath = options.ECH.KeyPath
|
||||||
|
} else {
|
||||||
|
return E.New("missing ECH keys")
|
||||||
|
}
|
||||||
|
block, rest := pem.Decode(echKey)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||||
|
return E.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse ECH keys")
|
||||||
|
}
|
||||||
|
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||||
|
//nolint:staticcheck
|
||||||
|
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||||
|
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||||
|
echKey, err := os.ReadFile(echKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "reload ECH keys from ", echKeyPath)
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode(echKey)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" {
|
||||||
|
return E.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse ECH keys")
|
||||||
|
}
|
||||||
|
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type STDECHClientConfig struct {
|
||||||
|
STDClientConfig
|
||||||
|
dnsRouter adapter.DNSRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||||
|
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
||||||
|
message := &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
RecursionDesired: true,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{
|
||||||
|
{
|
||||||
|
Name: mDNS.Fqdn(s.config.ServerName),
|
||||||
|
Qtype: mDNS.TypeHTTPS,
|
||||||
|
Qclass: mDNS.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := s.dnsRouter.Exchange(ctx, message, adapter.DNSQueryOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "fetch ECH config list")
|
||||||
|
}
|
||||||
|
if response.Rcode != mDNS.RcodeSuccess {
|
||||||
|
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
|
||||||
|
}
|
||||||
|
for _, rr := range response.Answer {
|
||||||
|
switch resource := rr.(type) {
|
||||||
|
case *mDNS.HTTPS:
|
||||||
|
for _, value := range resource.Value {
|
||||||
|
if value.Key().String() == "ech" {
|
||||||
|
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode ECH config")
|
||||||
|
}
|
||||||
|
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.New("no ECH config found in DNS records")
|
||||||
|
}
|
||||||
|
tlsConn, err := s.Client(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tlsConn.HandshakeContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *STDECHClientConfig) Clone() Config {
|
||||||
|
return &STDECHClientConfig{STDClientConfig{s.config.Clone()}, s.dnsRouter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
|
var keys []tls.EncryptedClientHelloKey
|
||||||
|
rawString := cryptobyte.String(raw)
|
||||||
|
for !rawString.Empty() {
|
||||||
|
var key tls.EncryptedClientHelloKey
|
||||||
|
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
|
||||||
|
return nil, E.New("error parsing private key")
|
||||||
|
}
|
||||||
|
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
|
||||||
|
return nil, E.New("error parsing config")
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil, E.New("empty ECH keys")
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
//go:build with_ech
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/pem"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
type echClientConfig struct {
|
|
||||||
config *cftls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) ServerName() string {
|
|
||||||
return c.config.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) SetServerName(serverName string) {
|
|
||||||
c.config.ServerName = serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) NextProtos() []string {
|
|
||||||
return c.config.NextProtos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) SetNextProtos(nextProto []string) {
|
|
||||||
c.config.NextProtos = nextProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) Config() (*STDConfig, error) {
|
|
||||||
return nil, E.New("unsupported usage for ECH")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
|
|
||||||
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) Clone() Config {
|
|
||||||
return &echClientConfig{
|
|
||||||
config: c.config.Clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type echConnWrapper struct {
|
|
||||||
*cftls.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
|
||||||
state := c.Conn.ConnectionState()
|
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
|
||||||
Version: state.Version,
|
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
|
||||||
DidResume: state.DidResume,
|
|
||||||
CipherSuite: state.CipherSuite,
|
|
||||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
|
||||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
|
||||||
ServerName: state.ServerName,
|
|
||||||
PeerCertificates: state.PeerCertificates,
|
|
||||||
VerifiedChains: state.VerifiedChains,
|
|
||||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
|
||||||
OCSPResponse: state.OCSPResponse,
|
|
||||||
TLSUnique: state.TLSUnique,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echConnWrapper) Upstream() any {
|
|
||||||
return c.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
|
||||||
var serverName string
|
|
||||||
if options.ServerName != "" {
|
|
||||||
serverName = options.ServerName
|
|
||||||
} else if serverAddress != "" {
|
|
||||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
|
||||||
serverName = serverAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if serverName == "" && !options.Insecure {
|
|
||||||
return nil, E.New("missing server_name or insecure=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
var tlsConfig cftls.Config
|
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
|
||||||
if options.DisableSNI {
|
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
|
||||||
} else {
|
|
||||||
tlsConfig.ServerName = serverName
|
|
||||||
}
|
|
||||||
if options.Insecure {
|
|
||||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
|
||||||
} else if options.DisableSNI {
|
|
||||||
tlsConfig.InsecureSkipVerify = true
|
|
||||||
tlsConfig.VerifyConnection = func(state cftls.ConnectionState) error {
|
|
||||||
verifyOptions := x509.VerifyOptions{
|
|
||||||
DNSName: serverName,
|
|
||||||
Intermediates: x509.NewCertPool(),
|
|
||||||
}
|
|
||||||
for _, cert := range state.PeerCertificates[1:] {
|
|
||||||
verifyOptions.Intermediates.AddCert(cert)
|
|
||||||
}
|
|
||||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(options.ALPN) > 0 {
|
|
||||||
tlsConfig.NextProtos = options.ALPN
|
|
||||||
}
|
|
||||||
if options.MinVersion != "" {
|
|
||||||
minVersion, err := ParseTLSVersion(options.MinVersion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse min_version")
|
|
||||||
}
|
|
||||||
tlsConfig.MinVersion = minVersion
|
|
||||||
}
|
|
||||||
if options.MaxVersion != "" {
|
|
||||||
maxVersion, err := ParseTLSVersion(options.MaxVersion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse max_version")
|
|
||||||
}
|
|
||||||
tlsConfig.MaxVersion = maxVersion
|
|
||||||
}
|
|
||||||
if options.CipherSuites != nil {
|
|
||||||
find:
|
|
||||||
for _, cipherSuite := range options.CipherSuites {
|
|
||||||
for _, tlsCipherSuite := range cftls.CipherSuites() {
|
|
||||||
if cipherSuite == tlsCipherSuite.Name {
|
|
||||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
|
||||||
continue find
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var certificate []byte
|
|
||||||
if len(options.Certificate) > 0 {
|
|
||||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
|
||||||
if len(certificate) > 0 {
|
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(certificate) {
|
|
||||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
|
||||||
}
|
|
||||||
tlsConfig.RootCAs = certPool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ECH Config
|
|
||||||
|
|
||||||
tlsConfig.ECHEnabled = true
|
|
||||||
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
|
||||||
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
|
||||||
|
|
||||||
var echConfig []byte
|
|
||||||
if len(options.ECH.Config) > 0 {
|
|
||||||
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
|
||||||
} else if options.ECH.ConfigPath != "" {
|
|
||||||
content, err := os.ReadFile(options.ECH.ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read ECH config")
|
|
||||||
}
|
|
||||||
echConfig = content
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(echConfig) > 0 {
|
|
||||||
block, rest := pem.Decode(echConfig)
|
|
||||||
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
|
||||||
return nil, E.New("invalid ECH configs pem")
|
|
||||||
}
|
|
||||||
echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse ECH configs")
|
|
||||||
}
|
|
||||||
tlsConfig.ClientECHConfigs = echConfigs
|
|
||||||
} else {
|
|
||||||
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
|
|
||||||
}
|
|
||||||
return &echClientConfig{&tlsConfig}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
|
||||||
return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
|
||||||
message := &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
RecursionDesired: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{
|
|
||||||
{
|
|
||||||
Name: serverName + ".",
|
|
||||||
Qtype: mDNS.TypeHTTPS,
|
|
||||||
Qclass: mDNS.ClassINET,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if response.Rcode != mDNS.RcodeSuccess {
|
|
||||||
return nil, dns.RCodeError(response.Rcode)
|
|
||||||
}
|
|
||||||
for _, rr := range response.Answer {
|
|
||||||
switch resource := rr.(type) {
|
|
||||||
case *mDNS.HTTPS:
|
|
||||||
for _, value := range resource.Value {
|
|
||||||
if value.Key().String() == "ech" {
|
|
||||||
echConfig, err := base64.StdEncoding.DecodeString(value.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode ECH config")
|
|
||||||
}
|
|
||||||
return cftls.UnmarshalECHConfigs(echConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, E.New("no ECH config found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build with_ech
|
|
||||||
|
|
||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,14 +5,13 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
"github.com/cloudflare/circl/hpke"
|
"github.com/cloudflare/circl/hpke"
|
||||||
"github.com/cloudflare/circl/kem"
|
"github.com/cloudflare/circl/kem"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
|
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
|
||||||
cipherSuites := []echCipherSuite{
|
cipherSuites := []echCipherSuite{
|
||||||
{
|
{
|
||||||
kdf: hpke.KDF_HKDF_SHA256,
|
kdf: hpke.KDF_HKDF_SHA256,
|
||||||
@@ -24,13 +21,9 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
|
|||||||
aead: hpke.AEAD_ChaCha20Poly1305,
|
aead: hpke.AEAD_ChaCha20Poly1305,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
keyConfig := []myECHKeyConfig{
|
keyConfig := []myECHKeyConfig{
|
||||||
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
|
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
|
||||||
}
|
}
|
||||||
if pqSignatureSchemesEnabled {
|
|
||||||
keyConfig = append(keyConfig, myECHKeyConfig{id: 1, kem: hpke.KEM_X25519_KYBER768_DRAFT00})
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
|
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,7 +52,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
|
|||||||
|
|
||||||
type echKeyConfigPair struct {
|
type echKeyConfigPair struct {
|
||||||
id uint8
|
id uint8
|
||||||
key cftls.EXP_ECHKey
|
|
||||||
rawKey []byte
|
rawKey []byte
|
||||||
conf myECHKeyConfig
|
conf myECHKeyConfig
|
||||||
rawConf []byte
|
rawConf []byte
|
||||||
@@ -155,15 +147,6 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
|
|||||||
sk = append(sk, secBuf...)
|
sk = append(sk, secBuf...)
|
||||||
sk = be.AppendUint16(sk, uint16(len(b)))
|
sk = be.AppendUint16(sk, uint16(len(b)))
|
||||||
sk = append(sk, b...)
|
sk = append(sk, b...)
|
||||||
|
|
||||||
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
|
|
||||||
}
|
|
||||||
if len(cfECHKeys) != 1 {
|
|
||||||
return nil, E.New("bug: unexpected server key count")
|
|
||||||
}
|
|
||||||
pair.key = cfECHKeys[0]
|
|
||||||
pair.rawKey = sk
|
pair.rawKey = sk
|
||||||
|
|
||||||
pairs = append(pairs, pair)
|
pairs = append(pairs, pair)
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
//go:build with_quic && with_ech
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/quic-go/ech"
|
|
||||||
"github.com/sagernet/quic-go/http3_ech"
|
|
||||||
"github.com/sagernet/sing-quic"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ qtls.Config = (*echClientConfig)(nil)
|
|
||||||
_ qtls.ServerConfig = (*echServerConfig)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
|
|
||||||
return quic.Dial(ctx, conn, addr, c.config, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
|
|
||||||
return quic.DialEarly(ctx, conn, addr, c.config, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
|
|
||||||
return &http3.Transport{
|
|
||||||
TLSClientConfig: c.config,
|
|
||||||
QUICConfig: quicConfig,
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
|
||||||
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
*quicConnPtr = quicConn
|
|
||||||
return quicConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.Listener, error) {
|
|
||||||
return quic.Listen(conn, c.config, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.EarlyListener, error) {
|
|
||||||
return quic.ListenEarly(conn, c.config, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) ConfigureHTTP3() {
|
|
||||||
http3.ConfigureTLSConfig(c.config)
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
//go:build with_ech
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/pem"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/fswatch"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type echServerConfig struct {
|
|
||||||
config *cftls.Config
|
|
||||||
logger log.Logger
|
|
||||||
certificate []byte
|
|
||||||
key []byte
|
|
||||||
certificatePath string
|
|
||||||
keyPath string
|
|
||||||
echKeyPath string
|
|
||||||
watcher *fswatch.Watcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) ServerName() string {
|
|
||||||
return c.config.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) SetServerName(serverName string) {
|
|
||||||
c.config.ServerName = serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) NextProtos() []string {
|
|
||||||
return c.config.NextProtos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) SetNextProtos(nextProto []string) {
|
|
||||||
c.config.NextProtos = nextProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Config() (*STDConfig, error) {
|
|
||||||
return nil, E.New("unsupported usage for ECH")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Client(conn net.Conn) (Conn, error) {
|
|
||||||
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Server(conn net.Conn) (Conn, error) {
|
|
||||||
return &echConnWrapper{cftls.Server(conn, c.config)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Clone() Config {
|
|
||||||
return &echServerConfig{
|
|
||||||
config: c.config.Clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Start() error {
|
|
||||||
err := c.startWatcher()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Warn("create credentials watcher: ", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) startWatcher() error {
|
|
||||||
var watchPath []string
|
|
||||||
if c.certificatePath != "" {
|
|
||||||
watchPath = append(watchPath, c.certificatePath)
|
|
||||||
}
|
|
||||||
if c.keyPath != "" {
|
|
||||||
watchPath = append(watchPath, c.keyPath)
|
|
||||||
}
|
|
||||||
if c.echKeyPath != "" {
|
|
||||||
watchPath = append(watchPath, c.echKeyPath)
|
|
||||||
}
|
|
||||||
if len(watchPath) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
|
||||||
Path: watchPath,
|
|
||||||
Callback: func(path string) {
|
|
||||||
err := c.credentialsUpdated(path)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Error(E.Cause(err, "reload credentials from ", path))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = watcher.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.watcher = watcher
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) credentialsUpdated(path string) error {
|
|
||||||
if path == c.certificatePath || path == c.keyPath {
|
|
||||||
if path == c.certificatePath {
|
|
||||||
certificate, err := os.ReadFile(c.certificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.certificate = certificate
|
|
||||||
} else {
|
|
||||||
key, err := os.ReadFile(c.keyPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.key = key
|
|
||||||
}
|
|
||||||
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "parse key pair")
|
|
||||||
}
|
|
||||||
c.config.Certificates = []cftls.Certificate{keyPair}
|
|
||||||
c.logger.Info("reloaded TLS certificate")
|
|
||||||
} else {
|
|
||||||
echKeyContent, err := os.ReadFile(c.echKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
block, rest := pem.Decode(echKeyContent)
|
|
||||||
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
|
||||||
return E.New("invalid ECH keys pem")
|
|
||||||
}
|
|
||||||
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "parse ECH keys")
|
|
||||||
}
|
|
||||||
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "create ECH key set")
|
|
||||||
}
|
|
||||||
c.config.ServerECHProvider = echKeySet
|
|
||||||
c.logger.Info("reloaded ECH keys")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *echServerConfig) Close() error {
|
|
||||||
var err error
|
|
||||||
if c.watcher != nil {
|
|
||||||
err = E.Append(err, c.watcher.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close credentials watcher")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var tlsConfig cftls.Config
|
|
||||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
|
||||||
return nil, E.New("acme is unavailable in ech")
|
|
||||||
}
|
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
|
||||||
if options.ServerName != "" {
|
|
||||||
tlsConfig.ServerName = options.ServerName
|
|
||||||
}
|
|
||||||
if len(options.ALPN) > 0 {
|
|
||||||
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
|
|
||||||
}
|
|
||||||
if options.MinVersion != "" {
|
|
||||||
minVersion, err := ParseTLSVersion(options.MinVersion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse min_version")
|
|
||||||
}
|
|
||||||
tlsConfig.MinVersion = minVersion
|
|
||||||
}
|
|
||||||
if options.MaxVersion != "" {
|
|
||||||
maxVersion, err := ParseTLSVersion(options.MaxVersion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse max_version")
|
|
||||||
}
|
|
||||||
tlsConfig.MaxVersion = maxVersion
|
|
||||||
}
|
|
||||||
if options.CipherSuites != nil {
|
|
||||||
find:
|
|
||||||
for _, cipherSuite := range options.CipherSuites {
|
|
||||||
for _, tlsCipherSuite := range tls.CipherSuites() {
|
|
||||||
if cipherSuite == tlsCipherSuite.Name {
|
|
||||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
|
||||||
continue find
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var certificate []byte
|
|
||||||
var key []byte
|
|
||||||
if len(options.Certificate) > 0 {
|
|
||||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
|
||||||
if len(options.Key) > 0 {
|
|
||||||
key = []byte(strings.Join(options.Key, "\n"))
|
|
||||||
} else if options.KeyPath != "" {
|
|
||||||
content, err := os.ReadFile(options.KeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read key")
|
|
||||||
}
|
|
||||||
key = content
|
|
||||||
}
|
|
||||||
|
|
||||||
if certificate == nil {
|
|
||||||
return nil, E.New("missing certificate")
|
|
||||||
} else if key == nil {
|
|
||||||
return nil, E.New("missing key")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPair, err := cftls.X509KeyPair(certificate, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse x509 key pair")
|
|
||||||
}
|
|
||||||
tlsConfig.Certificates = []cftls.Certificate{keyPair}
|
|
||||||
|
|
||||||
var echKey []byte
|
|
||||||
if len(options.ECH.Key) > 0 {
|
|
||||||
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
|
|
||||||
} else if options.ECH.KeyPath != "" {
|
|
||||||
content, err := os.ReadFile(options.ECH.KeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read ECH key")
|
|
||||||
}
|
|
||||||
echKey = content
|
|
||||||
} else {
|
|
||||||
return nil, E.New("missing ECH key")
|
|
||||||
}
|
|
||||||
|
|
||||||
block, rest := pem.Decode(echKey)
|
|
||||||
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
|
||||||
return nil, E.New("invalid ECH keys pem")
|
|
||||||
}
|
|
||||||
|
|
||||||
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "parse ECH keys")
|
|
||||||
}
|
|
||||||
|
|
||||||
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create ECH key set")
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig.ECHEnabled = true
|
|
||||||
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
|
||||||
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
|
||||||
tlsConfig.ServerECHProvider = echKeySet
|
|
||||||
|
|
||||||
return &echServerConfig{
|
|
||||||
config: &tlsConfig,
|
|
||||||
logger: logger,
|
|
||||||
certificate: certificate,
|
|
||||||
key: key,
|
|
||||||
certificatePath: options.CertificatePath,
|
|
||||||
keyPath: options.KeyPath,
|
|
||||||
echKeyPath: options.ECH.KeyPath,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
//go:build !with_ech
|
//go:build !go1.24
|
||||||
|
|
||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errECHNotIncluded = E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
|
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||||
|
return nil, E.New("ECH requires go1.24, please recompile your binary.")
|
||||||
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
|
||||||
return nil, errECHNotIncluded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
|
||||||
return nil, errECHNotIncluded
|
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ECHKeygenDefault(host string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
|
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||||
return "", "", errECHNotIncluded
|
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||||
|
if timeFunc == nil {
|
||||||
|
timeFunc = time.Now
|
||||||
|
}
|
||||||
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
|
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -24,9 +30,6 @@ func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
||||||
if timeFunc == nil {
|
|
||||||
timeFunc = time.Now
|
|
||||||
}
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -35,17 +38,30 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
template := &x509.Certificate{
|
var template *x509.Certificate
|
||||||
SerialNumber: serialNumber,
|
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
template = &x509.Certificate{
|
||||||
NotAfter: expire,
|
SerialNumber: serialNumber,
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
BasicConstraintsValid: true,
|
NotAfter: expire,
|
||||||
Subject: pkix.Name{
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
CommonName: serverName,
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
},
|
BasicConstraintsValid: true,
|
||||||
DNSNames: []string{serverName},
|
}
|
||||||
|
} else {
|
||||||
|
template = &x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
|
NotAfter: expire,
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: serverName,
|
||||||
|
},
|
||||||
|
DNSNames: []string{serverName},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
parent = template
|
parent = template
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common/debug"
|
"github.com/sagernet/sing/common/debug"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
utls "github.com/sagernet/utls"
|
utls "github.com/sagernet/utls"
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ import (
|
|||||||
var _ ConfigCompat = (*RealityClientConfig)(nil)
|
var _ ConfigCompat = (*RealityClientConfig)(nil)
|
||||||
|
|
||||||
type RealityClientConfig struct {
|
type RealityClientConfig struct {
|
||||||
|
ctx context.Context
|
||||||
uClient *UTLSClientConfig
|
uClient *UTLSClientConfig
|
||||||
publicKey []byte
|
publicKey []byte
|
||||||
shortID [8]byte
|
shortID [8]byte
|
||||||
@@ -70,7 +73,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
|
|||||||
if decodedLen > 8 {
|
if decodedLen > 8 {
|
||||||
return nil, E.New("invalid short_id")
|
return nil, E.New("invalid short_id")
|
||||||
}
|
}
|
||||||
return &RealityClientConfig{uClient, publicKey, shortID}, nil
|
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *RealityClientConfig) ServerName() string {
|
func (e *RealityClientConfig) ServerName() string {
|
||||||
@@ -180,20 +183,24 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !verifier.verified {
|
if !verifier.verified {
|
||||||
go realityClientFallback(uConn, e.uClient.ServerName(), e.uClient.id)
|
go realityClientFallback(e.ctx, uConn, e.uClient.ServerName(), e.uClient.id)
|
||||||
return nil, E.New("reality verification failed")
|
return nil, E.New("reality verification failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &realityClientConnWrapper{uConn}, nil
|
return &realityClientConnWrapper{uConn}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
func realityClientFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||||
defer uConn.Close()
|
defer uConn.Close()
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: &http2.Transport{
|
Transport: &http2.Transport{
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
|
DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
|
||||||
return uConn, nil
|
return uConn, nil
|
||||||
},
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(ctx),
|
||||||
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
request, _ := http.NewRequest("GET", "https://"+serverName, nil)
|
request, _ := http.NewRequest("GET", "https://"+serverName, nil)
|
||||||
@@ -213,6 +220,7 @@ func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello [
|
|||||||
|
|
||||||
func (e *RealityClientConfig) Clone() Config {
|
func (e *RealityClientConfig) Clone() Config {
|
||||||
return &RealityClientConfig{
|
return &RealityClientConfig{
|
||||||
|
e.ctx,
|
||||||
e.uClient.Clone().(*UTLSClientConfig),
|
e.uClient.Clone().(*UTLSClientConfig),
|
||||||
e.publicKey,
|
e.publicKey,
|
||||||
e.shortID,
|
e.shortID,
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
|
|||||||
tlsConfig.ShortIds[shortID] = true
|
tlsConfig.ShortIds[shortID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions)
|
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,10 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
|
|||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.Reality != nil && options.Reality.Enabled {
|
||||||
return NewECHServer(ctx, logger, options)
|
|
||||||
} else if options.Reality != nil && options.Reality.Enabled {
|
|
||||||
return NewRealityServer(ctx, logger, options)
|
return NewRealityServer(ctx, logger, options)
|
||||||
} else {
|
|
||||||
return NewSTDServer(ctx, logger, options)
|
|
||||||
}
|
}
|
||||||
|
return NewSTDServer(ctx, logger, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
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"
|
||||||
@@ -51,9 +51,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
} else if serverAddress != "" {
|
} else if serverAddress != "" {
|
||||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
serverName = serverAddress
|
||||||
serverName = serverAddress
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if serverName == "" && !options.Insecure {
|
if serverName == "" && !options.Insecure {
|
||||||
return nil, E.New("missing server_name or insecure=true")
|
return nil, E.New("missing server_name or insecure=true")
|
||||||
@@ -61,6 +59,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
|
|
||||||
var tlsConfig tls.Config
|
var tlsConfig tls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
|
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
@@ -128,5 +127,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
}
|
}
|
||||||
tlsConfig.RootCAs = certPool
|
tlsConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
|
return parseECHClientConfig(ctx, options, &tlsConfig)
|
||||||
|
}
|
||||||
return &STDClientConfig{&tlsConfig}, nil
|
return &STDClientConfig{&tlsConfig}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type STDServerConfig struct {
|
|||||||
key []byte
|
key []byte
|
||||||
certificatePath string
|
certificatePath string
|
||||||
keyPath string
|
keyPath string
|
||||||
|
echKeyPath string
|
||||||
watcher *fswatch.Watcher
|
watcher *fswatch.Watcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +95,15 @@ func (c *STDServerConfig) startWatcher() error {
|
|||||||
if c.keyPath != "" {
|
if c.keyPath != "" {
|
||||||
watchPath = append(watchPath, c.keyPath)
|
watchPath = append(watchPath, c.keyPath)
|
||||||
}
|
}
|
||||||
|
if c.echKeyPath != "" {
|
||||||
|
watchPath = append(watchPath, c.echKeyPath)
|
||||||
|
}
|
||||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
Path: watchPath,
|
Path: watchPath,
|
||||||
Callback: func(path string) {
|
Callback: func(path string) {
|
||||||
err := c.certificateUpdated(path)
|
err := c.certificateUpdated(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error(err)
|
c.logger.Error(E.Cause(err, "reload certificate"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -115,25 +119,33 @@ func (c *STDServerConfig) startWatcher() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) certificateUpdated(path string) error {
|
func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||||
if path == c.certificatePath {
|
if path == c.certificatePath || path == c.keyPath {
|
||||||
certificate, err := os.ReadFile(c.certificatePath)
|
if path == c.certificatePath {
|
||||||
if err != nil {
|
certificate, err := os.ReadFile(c.certificatePath)
|
||||||
return E.Cause(err, "reload certificate from ", c.certificatePath)
|
if err != nil {
|
||||||
|
return E.Cause(err, "reload certificate from ", c.certificatePath)
|
||||||
|
}
|
||||||
|
c.certificate = certificate
|
||||||
|
} else if path == c.keyPath {
|
||||||
|
key, err := os.ReadFile(c.keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "reload key from ", c.keyPath)
|
||||||
|
}
|
||||||
|
c.key = key
|
||||||
}
|
}
|
||||||
c.certificate = certificate
|
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
|
||||||
} else if path == c.keyPath {
|
|
||||||
key, err := os.ReadFile(c.keyPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "reload key from ", c.keyPath)
|
return E.Cause(err, "reload key pair")
|
||||||
}
|
}
|
||||||
c.key = key
|
c.config.Certificates = []tls.Certificate{keyPair}
|
||||||
|
c.logger.Info("reloaded TLS certificate")
|
||||||
|
} else if path == c.echKeyPath {
|
||||||
|
err := reloadECHKeys(c.echKeyPath, c.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.logger.Info("reloaded ECH keys")
|
||||||
}
|
}
|
||||||
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "reload key pair")
|
|
||||||
}
|
|
||||||
c.config.Certificates = []tls.Certificate{keyPair}
|
|
||||||
c.logger.Info("reloaded TLS certificate")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +250,13 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var echKeyPath string
|
||||||
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
|
err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return &STDServerConfig{
|
return &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -246,5 +265,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
key: key,
|
key: key,
|
||||||
certificatePath: options.CertificatePath,
|
certificatePath: options.CertificatePath,
|
||||||
keyPath: options.KeyPath,
|
keyPath: options.KeyPath,
|
||||||
|
echKeyPath: echKeyPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
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"
|
||||||
@@ -130,6 +131,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
|||||||
|
|
||||||
var tlsConfig utls.Config
|
var tlsConfig utls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
|
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
107
common/tlsfragment/conn.go
Normal file
107
common/tlsfragment/conn.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
tcpConn *net.TCPConn
|
||||||
|
ctx context.Context
|
||||||
|
firstPacketWritten bool
|
||||||
|
fallbackDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
|
||||||
|
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
|
||||||
|
return &Conn{
|
||||||
|
Conn: conn,
|
||||||
|
tcpConn: tcpConn,
|
||||||
|
ctx: ctx,
|
||||||
|
fallbackDelay: fallbackDelay,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||||
|
if !c.firstPacketWritten {
|
||||||
|
defer func() {
|
||||||
|
c.firstPacketWritten = true
|
||||||
|
}()
|
||||||
|
serverName := indexTLSServerName(b)
|
||||||
|
if serverName != nil {
|
||||||
|
if c.tcpConn != nil {
|
||||||
|
err = c.tcpConn.SetNoDelay(true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
splits := strings.Split(serverName.ServerName, ".")
|
||||||
|
currentIndex := serverName.Index
|
||||||
|
if publicSuffix := publicsuffix.List.PublicSuffix(serverName.ServerName); publicSuffix != "" {
|
||||||
|
splits = splits[:len(splits)-strings.Count(serverName.ServerName, ".")]
|
||||||
|
}
|
||||||
|
if len(splits) > 1 && splits[0] == "..." {
|
||||||
|
currentIndex += len(splits[0]) + 1
|
||||||
|
splits = splits[1:]
|
||||||
|
}
|
||||||
|
var splitIndexes []int
|
||||||
|
for i, split := range splits {
|
||||||
|
splitAt := rand.Intn(len(split))
|
||||||
|
splitIndexes = append(splitIndexes, currentIndex+splitAt)
|
||||||
|
currentIndex += len(split)
|
||||||
|
if i != len(splits)-1 {
|
||||||
|
currentIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i <= len(splitIndexes); i++ {
|
||||||
|
var payload []byte
|
||||||
|
if i == 0 {
|
||||||
|
payload = b[:splitIndexes[i]]
|
||||||
|
} else if i == len(splitIndexes) {
|
||||||
|
payload = b[splitIndexes[i-1]:]
|
||||||
|
} else {
|
||||||
|
payload = b[splitIndexes[i-1]:splitIndexes[i]]
|
||||||
|
}
|
||||||
|
if c.tcpConn != nil && i != len(splitIndexes) {
|
||||||
|
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = c.Conn.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.tcpConn != nil {
|
||||||
|
err = c.tcpConn.SetNoDelay(false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.Conn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriterReplaceable() bool {
|
||||||
|
return c.firstPacketWritten
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Upstream() any {
|
||||||
|
return c.Conn
|
||||||
|
}
|
||||||
131
common/tlsfragment/index.go
Normal file
131
common/tlsfragment/index.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
recordLayerHeaderLen int = 5
|
||||||
|
handshakeHeaderLen int = 6
|
||||||
|
randomDataLen int = 32
|
||||||
|
sessionIDHeaderLen int = 1
|
||||||
|
cipherSuiteHeaderLen int = 2
|
||||||
|
compressMethodHeaderLen int = 1
|
||||||
|
extensionsHeaderLen int = 2
|
||||||
|
extensionHeaderLen int = 4
|
||||||
|
sniExtensionHeaderLen int = 5
|
||||||
|
contentType uint8 = 22
|
||||||
|
handshakeType uint8 = 1
|
||||||
|
sniExtensionType uint16 = 0
|
||||||
|
sniNameDNSHostnameType uint8 = 0
|
||||||
|
tlsVersionBitmask uint16 = 0xFFFC
|
||||||
|
tls13 uint16 = 0x0304
|
||||||
|
)
|
||||||
|
|
||||||
|
type myServerName struct {
|
||||||
|
Index int
|
||||||
|
Length int
|
||||||
|
ServerName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexTLSServerName(payload []byte) *myServerName {
|
||||||
|
if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
segmentLen := binary.BigEndian.Uint16(payload[3:5])
|
||||||
|
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
|
||||||
|
if serverName == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
serverName.Length += recordLayerHeaderLen
|
||||||
|
return serverName
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
|
||||||
|
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hs[0] != handshakeType {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
|
||||||
|
if len(hs[4:]) != int(handshakeLen) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
|
||||||
|
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sessionIDLen := hs[38]
|
||||||
|
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
|
||||||
|
if len(cs) < cipherSuiteHeaderLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
csLen := uint16(cs[0])<<8 | uint16(cs[1])
|
||||||
|
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
|
||||||
|
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
|
||||||
|
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
|
||||||
|
if serverName == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
serverName.Index += currentIndex
|
||||||
|
return serverName
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
|
||||||
|
if len(exs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(exs) < extensionsHeaderLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
|
||||||
|
exs = exs[extensionsHeaderLen:]
|
||||||
|
if len(exs) < int(exsLen) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for currentIndex := extensionsHeaderLen; len(exs) > 0; {
|
||||||
|
if len(exs) < extensionHeaderLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
exType := uint16(exs[0])<<8 | uint16(exs[1])
|
||||||
|
exLen := uint16(exs[2])<<8 | uint16(exs[3])
|
||||||
|
if len(exs) < extensionHeaderLen+int(exLen) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
|
||||||
|
|
||||||
|
switch exType {
|
||||||
|
case sniExtensionType:
|
||||||
|
if len(sex) < sniExtensionHeaderLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sniType := sex[2]
|
||||||
|
if sniType != sniNameDNSHostnameType {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
|
||||||
|
sex = sex[sniExtensionHeaderLen:]
|
||||||
|
return &myServerName{
|
||||||
|
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
|
||||||
|
Length: int(sniLen),
|
||||||
|
ServerName: string(sex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exs = exs[4+exLen:]
|
||||||
|
currentIndex += 4 + int(exLen)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
93
common/tlsfragment/wait_darwin.go
Normal file
93
common/tlsfragment/wait_darwin.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/control"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
const tcpMaxNotifyAck = 10
|
||||||
|
|
||||||
|
type tcpNotifyAckID uint32
|
||||||
|
|
||||||
|
type tcpNotifyAckComplete struct {
|
||||||
|
NotifyPending uint32
|
||||||
|
NotifyCompleteCount uint32
|
||||||
|
NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{}))
|
||||||
|
|
||||||
|
func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) {
|
||||||
|
var value tcpNotifyAckComplete
|
||||||
|
vallen := uint32(sizeOfTCPNotifyAckComplete)
|
||||||
|
err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen)
|
||||||
|
return &value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname getsockopt golang.org/x/sys/unix.getsockopt
|
||||||
|
func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error
|
||||||
|
|
||||||
|
func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error {
|
||||||
|
const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212
|
||||||
|
return control.Conn(conn, func(fd uintptr) error {
|
||||||
|
err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, unix.EINVAL) {
|
||||||
|
return waitAckFallback(ctx, conn, 0)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
var ackComplete *tcpNotifyAckComplete
|
||||||
|
ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ackComplete.NotifyPending == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||||
|
_, err := conn.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return control.Conn(conn, func(fd uintptr) error {
|
||||||
|
start := time.Now()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if unacked == 0 {
|
||||||
|
if time.Since(start) <= 20*time.Millisecond {
|
||||||
|
// under transparent proxy
|
||||||
|
time.Sleep(fallbackDelay)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
40
common/tlsfragment/wait_linux.go
Normal file
40
common/tlsfragment/wait_linux.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/control"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||||
|
_, err := conn.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return control.Conn(conn, func(fd uintptr) error {
|
||||||
|
start := time.Now()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tcpInfo.Unacked == 0 {
|
||||||
|
if time.Since(start) <= 20*time.Millisecond {
|
||||||
|
// under transparent proxy
|
||||||
|
time.Sleep(fallbackDelay)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
14
common/tlsfragment/wait_stub.go
Normal file
14
common/tlsfragment/wait_stub.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !(linux || darwin || windows)
|
||||||
|
|
||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||||
|
time.Sleep(fallbackDelay)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
common/tlsfragment/wait_windows.go
Normal file
28
common/tlsfragment/wait_windows.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/winiphlpapi"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||||
|
start := time.Now()
|
||||||
|
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||||
|
time.Sleep(fallbackDelay)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if time.Since(start) <= 20*time.Millisecond {
|
||||||
|
time.Sleep(fallbackDelay)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,32 +2,32 @@ package urltest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
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"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type History struct {
|
var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
|
||||||
Time time.Time `json:"time"`
|
|
||||||
Delay uint16 `json:"delay"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HistoryStorage struct {
|
type HistoryStorage struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
delayHistory map[string]*History
|
delayHistory map[string]*adapter.URLTestHistory
|
||||||
updateHook chan<- struct{}
|
updateHook chan<- struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHistoryStorage() *HistoryStorage {
|
func NewHistoryStorage() *HistoryStorage {
|
||||||
return &HistoryStorage{
|
return &HistoryStorage{
|
||||||
delayHistory: make(map[string]*History),
|
delayHistory: make(map[string]*adapter.URLTestHistory),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
|
|||||||
s.updateHook = hook
|
s.updateHook = hook
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
|
func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
|||||||
s.notifyUpdated()
|
s.notifyUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
|
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
s.delayHistory[tag] = history
|
s.delayHistory[tag] = history
|
||||||
s.access.Unlock()
|
s.access.Unlock()
|
||||||
@@ -110,6 +110,10 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
|
|||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return instance, nil
|
return instance, nil
|
||||||
},
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(ctx),
|
||||||
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
|
|||||||
7
constant/certificate.go
Normal file
7
constant/certificate.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
CertificateStoreSystem = "system"
|
||||||
|
CertificateStoreMozilla = "mozilla"
|
||||||
|
CertificateStoreNone = "none"
|
||||||
|
)
|
||||||
@@ -1,5 +1,35 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultDNSTTL = 600
|
||||||
|
)
|
||||||
|
|
||||||
|
type DomainStrategy = uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
DomainStrategyAsIS DomainStrategy = iota
|
||||||
|
DomainStrategyPreferIPv4
|
||||||
|
DomainStrategyPreferIPv6
|
||||||
|
DomainStrategyIPv4Only
|
||||||
|
DomainStrategyIPv6Only
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DNSTypeLegacy = "legacy"
|
||||||
|
DNSTypeLegacyRcode = "legacy_rcode"
|
||||||
|
DNSTypeUDP = "udp"
|
||||||
|
DNSTypeTCP = "tcp"
|
||||||
|
DNSTypeTLS = "tls"
|
||||||
|
DNSTypeHTTPS = "https"
|
||||||
|
DNSTypeQUIC = "quic"
|
||||||
|
DNSTypeHTTP3 = "h3"
|
||||||
|
DNSTypeLocal = "local"
|
||||||
|
DNSTypeHosts = "hosts"
|
||||||
|
DNSTypeFakeIP = "fakeip"
|
||||||
|
DNSTypeDHCP = "dhcp"
|
||||||
|
DNSTypeTailscale = "tailscale"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DNSProviderAliDNS = "alidns"
|
DNSProviderAliDNS = "alidns"
|
||||||
DNSProviderCloudflare = "cloudflare"
|
DNSProviderCloudflare = "cloudflare"
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ const (
|
|||||||
TypeTor = "tor"
|
TypeTor = "tor"
|
||||||
TypeSSH = "ssh"
|
TypeSSH = "ssh"
|
||||||
TypeShadowTLS = "shadowtls"
|
TypeShadowTLS = "shadowtls"
|
||||||
|
TypeAnyTLS = "anytls"
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeVLESS = "vless"
|
TypeVLESS = "vless"
|
||||||
TypeTUIC = "tuic"
|
TypeTUIC = "tuic"
|
||||||
TypeHysteria2 = "hysteria2"
|
TypeHysteria2 = "hysteria2"
|
||||||
|
TypeTailscale = "tailscale"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -76,6 +78,8 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "TUIC"
|
return "TUIC"
|
||||||
case TypeHysteria2:
|
case TypeHysteria2:
|
||||||
return "Hysteria2"
|
return "Hysteria2"
|
||||||
|
case TypeAnyTLS:
|
||||||
|
return "AnyTLS"
|
||||||
case TypeSelector:
|
case TypeSelector:
|
||||||
return "Selector"
|
return "Selector"
|
||||||
case TypeURLTest:
|
case TypeURLTest:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const (
|
|||||||
RuleActionTypeHijackDNS = "hijack-dns"
|
RuleActionTypeHijackDNS = "hijack-dns"
|
||||||
RuleActionTypeSniff = "sniff"
|
RuleActionTypeSniff = "sniff"
|
||||||
RuleActionTypeResolve = "resolve"
|
RuleActionTypeResolve = "resolve"
|
||||||
|
RuleActionTypePredefined = "predefined"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
7
constant/script.go
Normal file
7
constant/script.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScriptTypeSurge = "surge"
|
||||||
|
ScriptSourceTypeLocal = "local"
|
||||||
|
ScriptSourceTypeRemote = "remote"
|
||||||
|
)
|
||||||
@@ -16,6 +16,7 @@ const (
|
|||||||
StopTimeout = 5 * time.Second
|
StopTimeout = 5 * time.Second
|
||||||
FatalStopTimeout = 10 * time.Second
|
FatalStopTimeout = 10 * time.Second
|
||||||
FakeIPMetadataSaveInterval = 10 * time.Second
|
FakeIPMetadataSaveInterval = 10 * time.Second
|
||||||
|
TLSFragmentFallbackDelay = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
var PortProtocols = map[uint16]string{
|
var PortProtocols = map[uint16]string{
|
||||||
|
|||||||
633
dns/client.go
Normal file
633
dns/client.go
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
|
|
||||||
|
dns "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoRawSupport = E.New("no raw query support by current transport")
|
||||||
|
ErrNotCached = E.New("not cached")
|
||||||
|
ErrResponseRejected = E.New("response rejected")
|
||||||
|
ErrResponseRejectedCached = E.Extend(ErrResponseRejected, "cached")
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.DNSClient = (*Client)(nil)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
timeout time.Duration
|
||||||
|
disableCache bool
|
||||||
|
disableExpire bool
|
||||||
|
independentCache bool
|
||||||
|
rdrc adapter.RDRCStore
|
||||||
|
initRDRCFunc func() adapter.RDRCStore
|
||||||
|
logger logger.ContextLogger
|
||||||
|
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||||
|
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientOptions struct {
|
||||||
|
Timeout time.Duration
|
||||||
|
DisableCache bool
|
||||||
|
DisableExpire bool
|
||||||
|
IndependentCache bool
|
||||||
|
CacheCapacity uint32
|
||||||
|
RDRC func() adapter.RDRCStore
|
||||||
|
Logger logger.ContextLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(options ClientOptions) *Client {
|
||||||
|
client := &Client{
|
||||||
|
timeout: options.Timeout,
|
||||||
|
disableCache: options.DisableCache,
|
||||||
|
disableExpire: options.DisableExpire,
|
||||||
|
independentCache: options.IndependentCache,
|
||||||
|
initRDRCFunc: options.RDRC,
|
||||||
|
logger: options.Logger,
|
||||||
|
}
|
||||||
|
if client.timeout == 0 {
|
||||||
|
client.timeout = C.DNSTimeout
|
||||||
|
}
|
||||||
|
cacheCapacity := options.CacheCapacity
|
||||||
|
if cacheCapacity < 1024 {
|
||||||
|
cacheCapacity = 1024
|
||||||
|
}
|
||||||
|
if !client.disableCache {
|
||||||
|
if !client.independentCache {
|
||||||
|
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
||||||
|
} else {
|
||||||
|
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
type transportCacheKey struct {
|
||||||
|
dns.Question
|
||||||
|
transportTag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Start() {
|
||||||
|
if c.initRDRCFunc != nil {
|
||||||
|
c.rdrc = c.initRDRCFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if len(message.Question) == 0 {
|
||||||
|
if c.logger != nil {
|
||||||
|
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||||
|
}
|
||||||
|
responseMessage := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Response: true,
|
||||||
|
Rcode: dns.RcodeFormatError,
|
||||||
|
},
|
||||||
|
Question: message.Question,
|
||||||
|
}
|
||||||
|
return &responseMessage, nil
|
||||||
|
}
|
||||||
|
question := message.Question[0]
|
||||||
|
if options.ClientSubnet.IsValid() {
|
||||||
|
message = SetClientSubnet(message, options.ClientSubnet, true)
|
||||||
|
}
|
||||||
|
isSimpleRequest := len(message.Question) == 1 &&
|
||||||
|
len(message.Ns) == 0 &&
|
||||||
|
len(message.Extra) == 0 &&
|
||||||
|
!options.ClientSubnet.IsValid()
|
||||||
|
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||||
|
if !disableCache {
|
||||||
|
response, ttl := c.loadResponse(question, transport)
|
||||||
|
if response != nil {
|
||||||
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
|
response.Id = message.Id
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
||||||
|
responseMessage := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Response: true,
|
||||||
|
Rcode: dns.RcodeSuccess,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
}
|
||||||
|
if c.logger != nil {
|
||||||
|
c.logger.DebugContext(ctx, "strategy rejected")
|
||||||
|
}
|
||||||
|
return &responseMessage, nil
|
||||||
|
}
|
||||||
|
messageId := message.Id
|
||||||
|
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
||||||
|
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
||||||
|
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
||||||
|
}
|
||||||
|
ctx = contextWithTransportTag(ctx, transport.Tag())
|
||||||
|
if responseChecker != nil && c.rdrc != nil {
|
||||||
|
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
||||||
|
if rejected {
|
||||||
|
return nil, ErrResponseRejectedCached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
|
response, err := transport.Exchange(ctx, message)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
if responseChecker != nil {
|
||||||
|
addr, addrErr := MessageToAddresses(response)
|
||||||
|
if addrErr != nil || !responseChecker(addr) {
|
||||||
|
if c.rdrc != nil {
|
||||||
|
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||||
|
}
|
||||||
|
logRejectedResponse(c.logger, ctx, response)
|
||||||
|
return response, ErrResponseRejected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if question.Qtype == dns.TypeHTTPS {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.Id = messageId
|
||||||
|
if !disableCache {
|
||||||
|
c.storeCache(transport, question, response, timeToLive)
|
||||||
|
}
|
||||||
|
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||||
|
domain = FqdnToDomain(domain)
|
||||||
|
dnsName := dns.Fqdn(domain)
|
||||||
|
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||||
|
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||||
|
} else if options.Strategy == C.DomainStrategyIPv6Only {
|
||||||
|
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||||
|
}
|
||||||
|
var response4 []netip.Addr
|
||||||
|
var response6 []netip.Addr
|
||||||
|
var group task.Group
|
||||||
|
group.Append("exchange4", func(ctx context.Context) error {
|
||||||
|
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response4 = response
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
group.Append("exchange6", func(ctx context.Context) error {
|
||||||
|
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response6 = response
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
err := group.Run(ctx)
|
||||||
|
if len(response4) == 0 && len(response6) == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sortAddresses(response4, response6, options.Strategy), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ClearCache() {
|
||||||
|
if c.cache != nil {
|
||||||
|
c.cache.Purge()
|
||||||
|
}
|
||||||
|
if c.transportCache != nil {
|
||||||
|
c.transportCache.Purge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
|
||||||
|
if c.disableCache || c.independentCache {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if dns.IsFqdn(domain) {
|
||||||
|
domain = domain[:len(domain)-1]
|
||||||
|
}
|
||||||
|
dnsName := dns.Fqdn(domain)
|
||||||
|
if strategy == C.DomainStrategyIPv4Only {
|
||||||
|
response, err := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
if err != ErrNotCached {
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
} else if strategy == C.DomainStrategyIPv6Only {
|
||||||
|
response, err := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
if err != ErrNotCached {
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response4, _ := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
response6, _ := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
if len(response4) > 0 || len(response6) > 0 {
|
||||||
|
return sortAddresses(response4, response6, strategy), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
||||||
|
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
question := message.Question[0]
|
||||||
|
response, ttl := c.loadResponse(question, nil)
|
||||||
|
if response == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
|
response.Id = message.Id
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
||||||
|
if strategy == C.DomainStrategyPreferIPv6 {
|
||||||
|
return append(response6, response4...)
|
||||||
|
} else {
|
||||||
|
return append(response4, response6...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, timeToLive uint32) {
|
||||||
|
if timeToLive == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.disableExpire {
|
||||||
|
if !c.independentCache {
|
||||||
|
c.cache.Add(question, message)
|
||||||
|
} else {
|
||||||
|
c.transportCache.Add(transportCacheKey{
|
||||||
|
Question: question,
|
||||||
|
transportTag: transport.Tag(),
|
||||||
|
}, message)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.independentCache {
|
||||||
|
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) {
|
||||||
|
question := dns.Question{
|
||||||
|
Name: name,
|
||||||
|
Qtype: qType,
|
||||||
|
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{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
RecursionDesired: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
}
|
||||||
|
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return MessageToAddresses(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
||||||
|
response, _ := c.loadResponse(question, transport)
|
||||||
|
if response == nil {
|
||||||
|
return nil, ErrNotCached
|
||||||
|
}
|
||||||
|
return MessageToAddresses(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
||||||
|
var (
|
||||||
|
response *dns.Msg
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
|
||||||
|
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||||
|
return nil, RcodeError(response.Rcode)
|
||||||
|
}
|
||||||
|
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, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapError(err error) error {
|
||||||
|
switch dnsErr := err.(type) {
|
||||||
|
case *net.DNSError:
|
||||||
|
if dnsErr.IsNotFound {
|
||||||
|
return RcodeNameError
|
||||||
|
}
|
||||||
|
case *net.AddrError:
|
||||||
|
return RcodeNameError
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type transportKey struct{}
|
||||||
|
|
||||||
|
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||||
|
return context.WithValue(ctx, transportKey{}, transportTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||||
|
value, loaded := ctx.Value(transportKey{}).(string)
|
||||||
|
return value, loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
|
||||||
|
response := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: id,
|
||||||
|
Rcode: dns.RcodeSuccess,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
}
|
||||||
|
for _, address := range addresses {
|
||||||
|
if address.Is4() && question.Qtype == dns.TypeA {
|
||||||
|
response.Answer = append(response.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: question.Name,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: timeToLive,
|
||||||
|
},
|
||||||
|
A: address.AsSlice(),
|
||||||
|
})
|
||||||
|
} else if address.Is6() && question.Qtype == dns.TypeAAAA {
|
||||||
|
response.Answer = append(response.Answer, &dns.AAAA{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: question.Name,
|
||||||
|
Rrtype: dns.TypeAAAA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: timeToLive,
|
||||||
|
},
|
||||||
|
AAAA: address.AsSlice(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
|
||||||
|
response := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: id,
|
||||||
|
Rcode: dns.RcodeSuccess,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
Answer: []dns.RR{
|
||||||
|
&dns.CNAME{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: question.Name,
|
||||||
|
Rrtype: dns.TypeCNAME,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: timeToLive,
|
||||||
|
},
|
||||||
|
Target: record,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
|
||||||
|
response := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: id,
|
||||||
|
Rcode: dns.RcodeSuccess,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
Answer: []dns.RR{
|
||||||
|
&dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: question.Name,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: timeToLive,
|
||||||
|
},
|
||||||
|
Txt: records,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
|
||||||
|
response := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: id,
|
||||||
|
Rcode: dns.RcodeSuccess,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
}
|
||||||
|
for _, record := range records {
|
||||||
|
response.Answer = append(response.Answer, &dns.MX{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: question.Name,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: timeToLive,
|
||||||
|
},
|
||||||
|
Preference: record.Pref,
|
||||||
|
Mx: record.Host,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
69
dns/client_log.go
Normal file
69
dns/client_log.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func logCachedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl int) {
|
||||||
|
if logger == nil || len(response.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
domain := FqdnToDomain(response.Question[0].Name)
|
||||||
|
logger.DebugContext(ctx, "cached ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
logger.InfoContext(ctx, "cached ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
||||||
|
if logger == nil || len(response.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
domain := FqdnToDomain(response.Question[0].Name)
|
||||||
|
logger.DebugContext(ctx, "exchanged ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
logger.InfoContext(ctx, "exchanged ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logRejectedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
||||||
|
if logger == nil || len(response.Question) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
logger.InfoContext(ctx, "rejected ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FqdnToDomain(fqdn string) string {
|
||||||
|
if dns.IsFqdn(fqdn) {
|
||||||
|
return fqdn[:len(fqdn)-1]
|
||||||
|
}
|
||||||
|
return fqdn
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatQuestion(string string) string {
|
||||||
|
for strings.HasPrefix(string, ";") {
|
||||||
|
string = string[1:]
|
||||||
|
}
|
||||||
|
string = strings.ReplaceAll(string, "\t", " ")
|
||||||
|
string = strings.ReplaceAll(string, "\n", " ")
|
||||||
|
string = strings.ReplaceAll(string, ";; ", " ")
|
||||||
|
string = strings.ReplaceAll(string, "; ", " ")
|
||||||
|
|
||||||
|
for strings.Contains(string, " ") {
|
||||||
|
string = strings.ReplaceAll(string, " ", " ")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string)
|
||||||
|
}
|
||||||
29
dns/client_truncate.go
Normal file
29
dns/client_truncate.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf.Buffer, error) {
|
||||||
|
maxLen := 512
|
||||||
|
if edns0Option := request.IsEdns0(); edns0Option != nil {
|
||||||
|
if udpSize := int(edns0Option.UDPSize()); udpSize > 512 {
|
||||||
|
maxLen = udpSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseLen := response.Len()
|
||||||
|
if responseLen > maxLen {
|
||||||
|
response.Truncate(maxLen)
|
||||||
|
}
|
||||||
|
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||||
|
buffer.Resize(headroom, 0)
|
||||||
|
rawMessage, err := response.PackBuffer(buffer.FreeBytes())
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buffer.Truncate(len(rawMessage))
|
||||||
|
return buffer, nil
|
||||||
|
}
|
||||||
56
dns/extension_edns0_subnet.go
Normal file
56
dns/extension_edns0_subnet.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) *dns.Msg {
|
||||||
|
var (
|
||||||
|
optRecord *dns.OPT
|
||||||
|
subnetOption *dns.EDNS0_SUBNET
|
||||||
|
)
|
||||||
|
findExists:
|
||||||
|
for _, record := range message.Extra {
|
||||||
|
var isOPTRecord bool
|
||||||
|
if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord {
|
||||||
|
for _, option := range optRecord.Option {
|
||||||
|
var isEDNS0Subnet bool
|
||||||
|
subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET)
|
||||||
|
if isEDNS0Subnet {
|
||||||
|
if !override {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
break findExists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if optRecord == nil {
|
||||||
|
exMessage := *message
|
||||||
|
message = &exMessage
|
||||||
|
optRecord = &dns.OPT{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: ".",
|
||||||
|
Rrtype: dns.TypeOPT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
message.Extra = append(message.Extra, optRecord)
|
||||||
|
} else {
|
||||||
|
message = message.Copy()
|
||||||
|
}
|
||||||
|
if subnetOption == nil {
|
||||||
|
subnetOption = new(dns.EDNS0_SUBNET)
|
||||||
|
optRecord.Option = append(optRecord.Option, subnetOption)
|
||||||
|
}
|
||||||
|
subnetOption.Code = dns.EDNS0SUBNET
|
||||||
|
if clientSubnet.Addr().Is4() {
|
||||||
|
subnetOption.Family = 1
|
||||||
|
} else {
|
||||||
|
subnetOption.Family = 2
|
||||||
|
}
|
||||||
|
subnetOption.SourceNetmask = uint8(clientSubnet.Bits())
|
||||||
|
subnetOption.Address = clientSubnet.Addr().AsSlice()
|
||||||
|
return message
|
||||||
|
}
|
||||||
17
dns/rcode.go
Normal file
17
dns/rcode.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||||
|
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||||
|
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||||
|
)
|
||||||
|
|
||||||
|
type RcodeError int
|
||||||
|
|
||||||
|
func (e RcodeError) Error() string {
|
||||||
|
return mDNS.RcodeToString[int(e)]
|
||||||
|
}
|
||||||
454
dns/router.go
Normal file
454
dns/router.go
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
R "github.com/sagernet/sing-box/route/rule"
|
||||||
|
"github.com/sagernet/sing-tun"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.DNSRouter = (*Router)(nil)
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.ContextLogger
|
||||||
|
transport adapter.DNSTransportManager
|
||||||
|
outbound adapter.OutboundManager
|
||||||
|
client adapter.DNSClient
|
||||||
|
rules []adapter.DNSRule
|
||||||
|
defaultDomainStrategy C.DomainStrategy
|
||||||
|
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
||||||
|
platformInterface platform.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
||||||
|
router := &Router{
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logFactory.NewLogger("dns"),
|
||||||
|
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||||
|
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||||
|
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
||||||
|
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
||||||
|
}
|
||||||
|
router.client = NewClient(ClientOptions{
|
||||||
|
DisableCache: options.DNSClientOptions.DisableCache,
|
||||||
|
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||||
|
IndependentCache: options.DNSClientOptions.IndependentCache,
|
||||||
|
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||||
|
RDRC: func() adapter.RDRCStore {
|
||||||
|
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||||
|
if cacheFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !cacheFile.StoreRDRC() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cacheFile
|
||||||
|
},
|
||||||
|
Logger: router.logger,
|
||||||
|
})
|
||||||
|
if options.ReverseMapping {
|
||||||
|
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
|
||||||
|
}
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||||
|
for i, ruleOptions := range rules {
|
||||||
|
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse dns rule[", i, "]")
|
||||||
|
}
|
||||||
|
r.rules = append(r.rules, dnsRule)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Start(stage adapter.StartStage) error {
|
||||||
|
monitor := taskmonitor.New(r.logger, C.StartTimeout)
|
||||||
|
switch stage {
|
||||||
|
case adapter.StartStateStart:
|
||||||
|
monitor.Start("initialize DNS client")
|
||||||
|
r.client.Start()
|
||||||
|
monitor.Finish()
|
||||||
|
|
||||||
|
for i, rule := range r.rules {
|
||||||
|
monitor.Start("initialize DNS rule[", i, "]")
|
||||||
|
err := rule.Start()
|
||||||
|
monitor.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "initialize DNS rule[", i, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Close() error {
|
||||||
|
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||||
|
var err error
|
||||||
|
for i, rule := range r.rules {
|
||||||
|
monitor.Start("close dns rule[", i, "]")
|
||||||
|
err = E.Append(err, rule.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close dns rule[", i, "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||||
|
metadata := adapter.ContextFrom(ctx)
|
||||||
|
if metadata == nil {
|
||||||
|
panic("no context")
|
||||||
|
}
|
||||||
|
var currentRuleIndex int
|
||||||
|
if ruleIndex != -1 {
|
||||||
|
currentRuleIndex = ruleIndex + 1
|
||||||
|
}
|
||||||
|
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
|
||||||
|
currentRule := r.rules[currentRuleIndex]
|
||||||
|
if currentRule.WithAddressLimit() && !isAddressQuery {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metadata.ResetRuleCache()
|
||||||
|
if currentRule.Match(metadata) {
|
||||||
|
displayRuleIndex := currentRuleIndex
|
||||||
|
if displayRuleIndex != -1 {
|
||||||
|
displayRuleIndex += displayRuleIndex + 1
|
||||||
|
}
|
||||||
|
ruleDescription := currentRule.String()
|
||||||
|
if ruleDescription != "" {
|
||||||
|
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
||||||
|
} else {
|
||||||
|
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||||
|
}
|
||||||
|
switch action := currentRule.Action().(type) {
|
||||||
|
case *R.RuleActionDNSRoute:
|
||||||
|
transport, loaded := r.transport.Transport(action.Server)
|
||||||
|
if !loaded {
|
||||||
|
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isFakeIP := transport.Type() == C.DNSTypeFakeIP
|
||||||
|
if isFakeIP && !allowFakeIP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if action.Strategy != C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = action.Strategy
|
||||||
|
}
|
||||||
|
if isFakeIP || action.DisableCache {
|
||||||
|
options.DisableCache = true
|
||||||
|
}
|
||||||
|
if action.RewriteTTL != nil {
|
||||||
|
options.RewriteTTL = action.RewriteTTL
|
||||||
|
}
|
||||||
|
if action.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = action.ClientSubnet
|
||||||
|
}
|
||||||
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
|
}
|
||||||
|
if !options.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transport, currentRule, currentRuleIndex
|
||||||
|
case *R.RuleActionDNSRouteOptions:
|
||||||
|
if action.Strategy != C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = action.Strategy
|
||||||
|
}
|
||||||
|
if action.DisableCache {
|
||||||
|
options.DisableCache = true
|
||||||
|
}
|
||||||
|
if action.RewriteTTL != nil {
|
||||||
|
options.RewriteTTL = action.RewriteTTL
|
||||||
|
}
|
||||||
|
if action.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = action.ClientSubnet
|
||||||
|
}
|
||||||
|
case *R.RuleActionReject:
|
||||||
|
return nil, currentRule, currentRuleIndex
|
||||||
|
case *R.RuleActionPredefined:
|
||||||
|
return nil, currentRule, currentRuleIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.transport.Default(), nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||||
|
if len(message.Question) != 1 {
|
||||||
|
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||||
|
responseMessage := mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Response: true,
|
||||||
|
Rcode: mDNS.RcodeFormatError,
|
||||||
|
},
|
||||||
|
Question: message.Question,
|
||||||
|
}
|
||||||
|
return &responseMessage, nil
|
||||||
|
}
|
||||||
|
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||||
|
var (
|
||||||
|
transport adapter.DNSTransport
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
response, cached := r.client.ExchangeCache(ctx, message)
|
||||||
|
if !cached {
|
||||||
|
var metadata *adapter.InboundContext
|
||||||
|
ctx, metadata = adapter.ExtendContext(ctx)
|
||||||
|
metadata.Destination = M.Socksaddr{}
|
||||||
|
metadata.QueryType = message.Question[0].Qtype
|
||||||
|
switch metadata.QueryType {
|
||||||
|
case mDNS.TypeA:
|
||||||
|
metadata.IPVersion = 4
|
||||||
|
case mDNS.TypeAAAA:
|
||||||
|
metadata.IPVersion = 6
|
||||||
|
}
|
||||||
|
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||||
|
if options.Transport != nil {
|
||||||
|
transport = options.Transport
|
||||||
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
|
}
|
||||||
|
if !options.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||||
|
} else {
|
||||||
|
var (
|
||||||
|
rule adapter.DNSRule
|
||||||
|
ruleIndex int
|
||||||
|
)
|
||||||
|
ruleIndex = -1
|
||||||
|
for {
|
||||||
|
dnsCtx := adapter.OverrideContext(ctx)
|
||||||
|
dnsOptions := options
|
||||||
|
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||||
|
if rule != nil {
|
||||||
|
switch action := rule.Action().(type) {
|
||||||
|
case *R.RuleActionReject:
|
||||||
|
switch action.Method {
|
||||||
|
case C.RuleActionRejectMethodDefault:
|
||||||
|
return FixedResponse(message.Id, message.Question[0], nil, 0), nil
|
||||||
|
case C.RuleActionRejectMethodDrop:
|
||||||
|
return nil, tun.ErrDrop
|
||||||
|
}
|
||||||
|
case *R.RuleActionPredefined:
|
||||||
|
return action.Response(message), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||||
|
if rule != nil && rule.WithAddressLimit() {
|
||||||
|
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||||
|
metadata.DestinationAddresses = responseAddrs
|
||||||
|
return rule.MatchAddressLimit(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||||
|
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
||||||
|
var rejected bool
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrResponseRejectedCached) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
||||||
|
} else if errors.Is(err, ErrResponseRejected) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else if len(message.Question) > 0 {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if responseCheck != nil && rejected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
|
||||||
|
if transport == nil || transport.Type() != C.DNSTypeFakeIP {
|
||||||
|
for _, answer := range response.Answer {
|
||||||
|
switch record := answer.(type) {
|
||||||
|
case *mDNS.A:
|
||||||
|
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
||||||
|
case *mDNS.AAAA:
|
||||||
|
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||||
|
var (
|
||||||
|
responseAddrs []netip.Addr
|
||||||
|
cached bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
printResult := func() {
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrResponseRejectedCached) {
|
||||||
|
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
|
||||||
|
} else if errors.Is(err, ErrResponseRejected) {
|
||||||
|
r.logger.DebugContext(ctx, "response rejected for ", domain)
|
||||||
|
} else {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
|
||||||
|
}
|
||||||
|
} else if len(responseAddrs) == 0 {
|
||||||
|
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
|
||||||
|
err = RcodeNameError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
||||||
|
if cached {
|
||||||
|
if len(responseAddrs) == 0 {
|
||||||
|
return nil, RcodeNameError
|
||||||
|
}
|
||||||
|
return responseAddrs, nil
|
||||||
|
}
|
||||||
|
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
|
metadata.Destination = M.Socksaddr{}
|
||||||
|
metadata.Domain = FqdnToDomain(domain)
|
||||||
|
if options.Transport != nil {
|
||||||
|
transport := options.Transport
|
||||||
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
if !options.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
||||||
|
} else {
|
||||||
|
var (
|
||||||
|
transport adapter.DNSTransport
|
||||||
|
rule adapter.DNSRule
|
||||||
|
ruleIndex int
|
||||||
|
)
|
||||||
|
ruleIndex = -1
|
||||||
|
for {
|
||||||
|
dnsCtx := adapter.OverrideContext(ctx)
|
||||||
|
dnsOptions := options
|
||||||
|
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
|
||||||
|
if rule != nil {
|
||||||
|
switch action := rule.Action().(type) {
|
||||||
|
case *R.RuleActionReject:
|
||||||
|
switch action.Method {
|
||||||
|
case C.RuleActionRejectMethodDefault:
|
||||||
|
return nil, nil
|
||||||
|
case C.RuleActionRejectMethodDrop:
|
||||||
|
return nil, tun.ErrDrop
|
||||||
|
}
|
||||||
|
case *R.RuleActionPredefined:
|
||||||
|
if action.Rcode != mDNS.RcodeSuccess {
|
||||||
|
err = RcodeError(action.Rcode)
|
||||||
|
} else {
|
||||||
|
for _, answer := range action.Answer {
|
||||||
|
switch record := answer.(type) {
|
||||||
|
case *mDNS.A:
|
||||||
|
responseAddrs = append(responseAddrs, M.AddrFromIP(record.A))
|
||||||
|
case *mDNS.AAAA:
|
||||||
|
responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goto response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||||
|
if rule != nil && rule.WithAddressLimit() {
|
||||||
|
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||||
|
metadata.DestinationAddresses = responseAddrs
|
||||||
|
return rule.MatchAddressLimit(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||||
|
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck)
|
||||||
|
if responseCheck == nil || err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
printResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response:
|
||||||
|
printResult()
|
||||||
|
if len(responseAddrs) > 0 {
|
||||||
|
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
|
||||||
|
}
|
||||||
|
return responseAddrs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAddressQuery(message *mDNS.Msg) bool {
|
||||||
|
for _, question := range message.Question {
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) ClearCache() {
|
||||||
|
r.client.ClearCache()
|
||||||
|
if r.platformInterface != nil {
|
||||||
|
r.platformInterface.ClearDNSCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
|
||||||
|
if r.dnsReverseMapping == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
domain, loaded := r.dnsReverseMapping.Get(ip)
|
||||||
|
return domain, loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) ResetNetwork() {
|
||||||
|
r.ClearCache()
|
||||||
|
for _, transport := range r.transport.Transports() {
|
||||||
|
transport.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,6 @@ package dhcp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,13 +11,18 @@ import (
|
|||||||
"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/dialer"
|
||||||
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/transport"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/task"
|
"github.com/sagernet/sing/common/task"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
@@ -29,62 +31,54 @@ import (
|
|||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||||
dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) {
|
dns.RegisterTransport[option.DHCPDNSServerOptions](registry, C.DNSTypeDHCP, NewTransport)
|
||||||
return NewTransport(options)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||||
|
|
||||||
type Transport struct {
|
type Transport struct {
|
||||||
options dns.TransportOptions
|
dns.TransportAdapter
|
||||||
router adapter.Router
|
ctx context.Context
|
||||||
|
dialer N.Dialer
|
||||||
|
logger logger.ContextLogger
|
||||||
networkManager adapter.NetworkManager
|
networkManager adapter.NetworkManager
|
||||||
interfaceName string
|
interfaceName string
|
||||||
autoInterface bool
|
|
||||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||||
transports []dns.Transport
|
transports []adapter.DNSTransport
|
||||||
updateAccess sync.Mutex
|
updateAccess sync.Mutex
|
||||||
updatedAt time.Time
|
updatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransport(options dns.TransportOptions) (*Transport, error) {
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
linkURL, err := url.Parse(options.Address)
|
transportDialer, err := dns.NewLocalDialer(ctx, options.LocalDNSServerOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if linkURL.Host == "" {
|
return &Transport{
|
||||||
return nil, E.New("missing interface name for DHCP")
|
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions),
|
||||||
}
|
ctx: ctx,
|
||||||
transport := &Transport{
|
dialer: transportDialer,
|
||||||
options: options,
|
logger: logger,
|
||||||
networkManager: service.FromContext[adapter.NetworkManager](options.Context),
|
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
||||||
interfaceName: linkURL.Host,
|
interfaceName: options.Interface,
|
||||||
autoInterface: linkURL.Host == "auto",
|
}, nil
|
||||||
}
|
|
||||||
return transport, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Name() string {
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
return t.options.Name
|
if stage != adapter.StartStateStart {
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
func (t *Transport) Start() error {
|
|
||||||
err := t.fetchServers()
|
err := t.fetchServers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if t.autoInterface {
|
if t.interfaceName == "" {
|
||||||
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
|
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Reset() {
|
|
||||||
for _, transport := range t.transports {
|
|
||||||
transport.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Close() error {
|
func (t *Transport) Close() error {
|
||||||
for _, transport := range t.transports {
|
for _, transport := range t.transports {
|
||||||
transport.Close()
|
transport.Close()
|
||||||
@@ -95,10 +89,6 @@ func (t *Transport) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Raw() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
err := t.fetchServers()
|
err := t.fetchServers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,7 +110,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) fetchInterface() (*control.Interface, error) {
|
func (t *Transport) fetchInterface() (*control.Interface, error) {
|
||||||
if t.autoInterface {
|
if t.interfaceName == "" {
|
||||||
if t.networkManager.InterfaceMonitor() == nil {
|
if t.networkManager.InterfaceMonitor() == nil {
|
||||||
return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface")
|
return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface")
|
||||||
}
|
}
|
||||||
@@ -152,8 +142,8 @@ func (t *Transport) updateServers() error {
|
|||||||
return E.Cause(err, "dhcp: prepare interface")
|
return E.Cause(err, "dhcp: prepare interface")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.options.Logger.Info("dhcp: query DNS servers on ", iface.Name)
|
t.logger.Info("dhcp: query DNS servers on ", iface.Name)
|
||||||
fetchCtx, cancel := context.WithTimeout(t.options.Context, C.DHCPTimeout)
|
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
|
||||||
err = t.fetchServers0(fetchCtx, iface)
|
err = t.fetchServers0(fetchCtx, iface)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,7 +159,7 @@ func (t *Transport) updateServers() error {
|
|||||||
func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) {
|
func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) {
|
||||||
err := t.updateServers()
|
err := t.updateServers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.options.Logger.Error("update servers: ", err)
|
t.logger.Error("update servers: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +171,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
|
|||||||
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
listenAddr = "255.255.255.255:68"
|
listenAddr = "255.255.255.255:68"
|
||||||
}
|
}
|
||||||
packetConn, err := listener.ListenPacket(t.options.Context, "udp4", listenAddr)
|
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -219,17 +209,17 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
|||||||
|
|
||||||
dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes())
|
dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.options.Logger.Trace("dhcp: parse DHCP response: ", err)
|
t.logger.Trace("dhcp: parse DHCP response: ", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer {
|
if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer {
|
||||||
t.options.Logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
|
t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if dhcpPacket.TransactionID != transactionID {
|
if dhcpPacket.TransactionID != transactionID {
|
||||||
t.options.Logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
|
t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,36 +227,23 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
|||||||
if len(dns) == 0 {
|
if len(dns) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return t.recreateServers(iface, common.Map(dns, func(it net.IP) M.Socksaddr {
|
||||||
var addrs []netip.Addr
|
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||||
for _, ip := range dns {
|
}))
|
||||||
addr, _ := netip.AddrFromSlice(ip)
|
|
||||||
addrs = append(addrs, addr.Unmap())
|
|
||||||
}
|
|
||||||
return t.recreateServers(iface, addrs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []netip.Addr) error {
|
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.Socksaddr) error {
|
||||||
if len(serverAddrs) > 0 {
|
if len(serverAddrs) > 0 {
|
||||||
t.options.Logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string {
|
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "]")
|
||||||
return it.String()
|
|
||||||
}), ","), "]")
|
|
||||||
}
|
}
|
||||||
serverDialer := common.Must1(dialer.NewDefault(t.options.Context, option.DialerOptions{
|
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
|
||||||
BindInterface: iface.Name,
|
BindInterface: iface.Name,
|
||||||
UDPFragmentDefault: true,
|
UDPFragmentDefault: true,
|
||||||
}))
|
}))
|
||||||
var transports []dns.Transport
|
var transports []adapter.DNSTransport
|
||||||
for _, serverAddr := range serverAddrs {
|
for _, serverAddr := range serverAddrs {
|
||||||
newOptions := t.options
|
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
|
||||||
newOptions.Address = serverAddr.String()
|
|
||||||
newOptions.Dialer = serverDialer
|
|
||||||
serverTransport, err := dns.NewUDPTransport(newOptions)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "create UDP transport from DHCP result: ", serverAddr)
|
|
||||||
}
|
|
||||||
transports = append(transports, serverTransport)
|
|
||||||
}
|
}
|
||||||
for _, transport := range t.transports {
|
for _, transport := range t.transports {
|
||||||
transport.Close()
|
transport.Close()
|
||||||
@@ -274,7 +251,3 @@ func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []neti
|
|||||||
t.transports = transports
|
t.transports = transports
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
67
dns/transport/fakeip/fakeip.go
Normal file
67
dns/transport/fakeip/fakeip.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package fakeip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.FakeIPDNSServerOptions](registry, C.DNSTypeFakeIP, NewTransport)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.FakeIPTransport = (*Transport)(nil)
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
dns.TransportAdapter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
store adapter.FakeIPStore
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}))
|
||||||
|
return &Transport{
|
||||||
|
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
||||||
|
logger: logger,
|
||||||
|
store: store,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.store.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Close() error {
|
||||||
|
return t.store.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Reset() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
question := message.Question[0]
|
||||||
|
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
||||||
|
return nil, E.New("only IP queries are supported by fakeip")
|
||||||
|
}
|
||||||
|
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dns.FixedResponse(message.Id, question, []netip.Addr{address}, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Store() adapter.FakeIPStore {
|
||||||
|
return t.store
|
||||||
|
}
|
||||||
84
dns/transport/hosts/hosts.go
Normal file
84
dns/transport/hosts/hosts.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package hosts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/service/filemanager"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
dns.TransportAdapter
|
||||||
|
files []*File
|
||||||
|
predefined map[string][]netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
|
var (
|
||||||
|
files []*File
|
||||||
|
predefined = make(map[string][]netip.Addr)
|
||||||
|
)
|
||||||
|
if len(options.Path) == 0 {
|
||||||
|
files = append(files, NewFile(DefaultPath))
|
||||||
|
} else {
|
||||||
|
for _, path := range options.Path {
|
||||||
|
files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Predefined != nil {
|
||||||
|
for _, entry := range options.Predefined.Entries() {
|
||||||
|
predefined[mDNS.CanonicalName(entry.Key)] = entry.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Transport{
|
||||||
|
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil),
|
||||||
|
files: files,
|
||||||
|
predefined: predefined,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
question := message.Question[0]
|
||||||
|
domain := mDNS.CanonicalName(question.Name)
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
if addresses, ok := t.predefined[domain]; ok {
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
for _, file := range t.files {
|
||||||
|
addresses := file.Lookup(domain)
|
||||||
|
if len(addresses) > 0 {
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Rcode: mDNS.RcodeNameError,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{question},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
102
dns/transport/hosts/hosts_file.go
Normal file
102
dns/transport/hosts/hosts_file.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package hosts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cacheMaxAge = 5 * time.Second
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
path string
|
||||||
|
access sync.Mutex
|
||||||
|
byName map[string][]netip.Addr
|
||||||
|
expire time.Time
|
||||||
|
modTime time.Time
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFile(path string) *File {
|
||||||
|
return &File{
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Lookup(name string) []netip.Addr {
|
||||||
|
f.access.Lock()
|
||||||
|
defer f.access.Unlock()
|
||||||
|
f.update()
|
||||||
|
return f.byName[dns.CanonicalName(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) update() {
|
||||||
|
now := time.Now()
|
||||||
|
if now.Before(f.expire) && len(f.byName) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stat, err := os.Stat(f.path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.modTime.Equal(stat.ModTime()) && f.size == stat.Size() {
|
||||||
|
f.expire = now.Add(cacheMaxAge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
byName := make(map[string][]netip.Addr)
|
||||||
|
file, err := os.Open(f.path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
var (
|
||||||
|
prefix []byte
|
||||||
|
line []byte
|
||||||
|
isPrefix bool
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
line, isPrefix, err = reader.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isPrefix {
|
||||||
|
prefix = append(prefix, line...)
|
||||||
|
continue
|
||||||
|
} else if len(prefix) > 0 {
|
||||||
|
line = append(prefix, line...)
|
||||||
|
prefix = nil
|
||||||
|
}
|
||||||
|
commentIndex := strings.IndexRune(string(line), '#')
|
||||||
|
if commentIndex != -1 {
|
||||||
|
line = line[:commentIndex]
|
||||||
|
}
|
||||||
|
fields := strings.Fields(string(line))
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var addr netip.Addr
|
||||||
|
addr, err = netip.ParseAddr(fields[0])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for index := 1; index < len(fields); index++ {
|
||||||
|
canonicalName := dns.CanonicalName(fields[index])
|
||||||
|
byName[canonicalName] = append(byName[canonicalName], addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.expire = now.Add(cacheMaxAge)
|
||||||
|
f.modTime = stat.ModTime()
|
||||||
|
f.size = stat.Size()
|
||||||
|
f.byName = byName
|
||||||
|
}
|
||||||
16
dns/transport/hosts/hosts_test.go
Normal file
16
dns/transport/hosts/hosts_test.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package hosts_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHosts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost"))
|
||||||
|
require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost"))
|
||||||
|
}
|
||||||
5
dns/transport/hosts/hosts_unix.go
Normal file
5
dns/transport/hosts/hosts_unix.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package hosts
|
||||||
|
|
||||||
|
var DefaultPath = "/etc/hosts"
|
||||||
17
dns/transport/hosts/hosts_windows.go
Normal file
17
dns/transport/hosts/hosts_windows.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package hosts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultPath string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
systemDirectory, err := windows.GetSystemDirectory()
|
||||||
|
if err != nil {
|
||||||
|
systemDirectory = "C:\\Windows\\System32"
|
||||||
|
}
|
||||||
|
DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts")
|
||||||
|
}
|
||||||
2
dns/transport/hosts/testdata/hosts
vendored
Normal file
2
dns/transport/hosts/testdata/hosts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
127.0.0.1 localhost
|
||||||
|
::1 localhost
|
||||||
213
dns/transport/https.go
Normal file
213
dns/transport/https.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MimeType = "application/dns-message"
|
||||||
|
|
||||||
|
var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
|
||||||
|
|
||||||
|
func RegisterHTTPS(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPSTransport struct {
|
||||||
|
dns.TransportAdapter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
dialer N.Dialer
|
||||||
|
destination *url.URL
|
||||||
|
headers http.Header
|
||||||
|
transport *http.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
|
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||||
|
tlsOptions.Enabled = true
|
||||||
|
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||||
|
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
|
||||||
|
}
|
||||||
|
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
|
||||||
|
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
|
||||||
|
}
|
||||||
|
headers := options.Headers.Build()
|
||||||
|
host := headers.Get("Host")
|
||||||
|
if host != "" {
|
||||||
|
headers.Del("Host")
|
||||||
|
} else {
|
||||||
|
if tlsConfig.ServerName() != "" {
|
||||||
|
host = tlsConfig.ServerName()
|
||||||
|
} else {
|
||||||
|
host = options.Server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destinationURL := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
if destinationURL.Host == "" {
|
||||||
|
destinationURL.Host = options.Server
|
||||||
|
}
|
||||||
|
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||||
|
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||||
|
}
|
||||||
|
path := options.Path
|
||||||
|
if path == "" {
|
||||||
|
path = "/dns-query"
|
||||||
|
}
|
||||||
|
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serverAddr := options.DNSServerAddressOptions.Build()
|
||||||
|
if serverAddr.Port == 0 {
|
||||||
|
serverAddr.Port = 443
|
||||||
|
}
|
||||||
|
return NewHTTPSRaw(
|
||||||
|
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||||
|
logger,
|
||||||
|
transportDialer,
|
||||||
|
&destinationURL,
|
||||||
|
headers,
|
||||||
|
serverAddr,
|
||||||
|
tlsConfig,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSRaw(
|
||||||
|
adapter dns.TransportAdapter,
|
||||||
|
logger log.ContextLogger,
|
||||||
|
dialer N.Dialer,
|
||||||
|
destination *url.URL,
|
||||||
|
headers http.Header,
|
||||||
|
serverAddr M.Socksaddr,
|
||||||
|
tlsConfig tls.Config,
|
||||||
|
) *HTTPSTransport {
|
||||||
|
var transport *http.Transport
|
||||||
|
if tlsConfig != nil {
|
||||||
|
transport = &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
|
||||||
|
if hErr != nil {
|
||||||
|
return nil, hErr
|
||||||
|
}
|
||||||
|
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
|
||||||
|
if hErr != nil {
|
||||||
|
tcpConn.Close()
|
||||||
|
return nil, hErr
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transport = &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, serverAddr)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &HTTPSTransport{
|
||||||
|
TransportAdapter: adapter,
|
||||||
|
logger: logger,
|
||||||
|
dialer: dialer,
|
||||||
|
destination: destination,
|
||||||
|
headers: headers,
|
||||||
|
transport: transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dialer.InitializeDetour(t.dialer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HTTPSTransport) Close() error {
|
||||||
|
t.transport.CloseIdleConnections()
|
||||||
|
t.transport = t.transport.Clone()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
exMessage := *message
|
||||||
|
exMessage.Id = 0
|
||||||
|
exMessage.Compress = true
|
||||||
|
requestBuffer := buf.NewSize(1 + message.Len())
|
||||||
|
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
|
||||||
|
if err != nil {
|
||||||
|
requestBuffer.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||||
|
if err != nil {
|
||||||
|
requestBuffer.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header = t.headers.Clone()
|
||||||
|
request.Header.Set("Content-Type", MimeType)
|
||||||
|
request.Header.Set("Accept", MimeType)
|
||||||
|
response, err := t.transport.RoundTrip(request)
|
||||||
|
requestBuffer.Release()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return nil, E.New("unexpected status: ", response.Status)
|
||||||
|
}
|
||||||
|
var responseMessage mDNS.Msg
|
||||||
|
if response.ContentLength > 0 {
|
||||||
|
responseBuffer := buf.NewSize(int(response.ContentLength))
|
||||||
|
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = responseMessage.Unpack(responseBuffer.Bytes())
|
||||||
|
responseBuffer.Release()
|
||||||
|
} else {
|
||||||
|
rawMessage, err = io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = responseMessage.Unpack(rawMessage)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &responseMessage, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user