Compare commits

...

106 Commits

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

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

- Skip OPT records in all TTL loops (Exchange + loadResponse)
- Use message.Copy() in storeCache() to isolate cache from mutations
2026-04-10 14:08:24 +08:00
TargetLocked
7c3d8cf8db Fix disable tcp keep alive 2026-04-10 13:29:15 +08:00
世界
813b634d08 Bump version 2026-04-06 23:09:11 +08:00
hdrover
d9b435fb62 Fix naive inbound padding bytes 2026-04-06 22:33:11 +08:00
世界
354b4b040e sing: Fix vectorised readv iovec length calculation
This does not seem to affect any actual paths in the sing-box.
2026-04-01 16:16:58 +08:00
世界
7ffdc48b49 Bump version 2026-03-30 23:03:43 +08:00
世界
e15bdf11eb sing: Minor fixes 2026-03-30 22:58:11 +08:00
世界
e3bcb06c3e platform: Add HTTPResponse.WriteToWithProgress 2026-03-30 22:42:36 +08:00
世界
84d2280960 quic: Fix protocol client close & Sync hysteria bbr fix 2026-03-30 22:42:36 +08:00
世界
4fd2532b0a Fix naive quic error message 2026-03-30 22:42:36 +08:00
Zhengchao Ding
02ccde6c71 fix(rpm): add vendor field to fpm config to avoid (none) vendor
Co-authored-by: Hyper <hypar@disroot.org>
2026-03-30 22:09:54 +08:00
世界
e98b4ad449 Fix WireGuard shutdown race crashing
Stop peer goroutines before closing the TUN device to prevent
RoutineSequentialReceiver from calling Write on a nil dispatcher.
2026-03-26 16:33:21 +08:00
世界
d09182614c Bump version 2026-03-26 13:28:33 +08:00
世界
6381de7bab route: Fix query_type never matching in rule_set headless rules 2026-03-26 13:26:18 +08:00
世界
b0c6762bc1 route: merge rule_set branches into outer rules
Treat rule_set items as merged branches instead of standalone boolean
sub-items.

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

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

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

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

- SourceAddressMatch
- SourcePortMatch
- DestinationAddressMatch
- DestinationPortMatch
- DidMatch

That leak had two opposite effects.

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

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

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

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

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

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

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

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

In other words, rule-sets now behave as "OR branches whose successful
group matches merge into the outer rule", which matches the documented
intent without reintroducing cross-branch cache leakage.
2026-03-24 15:03:43 +08:00
世界
a3623eb41a tun: Fix system stack rewriting TUN subnet destinations to loopback 2026-03-23 19:38:55 +08:00
世界
72bc4c1f87 Fix DNS transport returning error for empty AAAA response
Closes #3925
2026-03-23 19:21:55 +08:00
世界
9ac1e2ff32 Match package_name in process_path rule on Android 2026-03-23 18:57:35 +08:00
世界
0045103d14 Fix package_name shared uid matching 2026-03-23 18:57:35 +08:00
世界
d2a933784c Optimize Darwin process finder 2026-03-23 18:57:35 +08:00
世界
3f05a37f65 Optimize Linux process finder 2026-03-23 18:57:35 +08:00
世界
b8e5a71450 Add process information cache to avoid duplicate lookups
PreMatch and full match phases each created a fresh InboundContext,
causing process search (expensive OS syscalls) to run twice per
connection. Use a freelru ShardedLRU cache with 200ms TTL to serve
the second lookup from cache.
2026-03-23 14:26:45 +08:00
世界
c13faa8e3c tailscale: Only set ProcessLocalIPs/ProcessSubnets for fake TUN 2026-03-23 14:16:40 +08:00
世界
7623bcd19e Fix DialerForICMPDestination 2026-03-23 13:58:55 +08:00
世界
795d1c2892 Fix nested rule-set match cache isolation 2026-03-23 12:26:19 +08:00
世界
6913b11e0a Reject removed legacy inbound fields instead of silently ignoring 2026-03-21 17:16:10 +08:00
世界
1e57c06295 daemon: Allow StartOrReloadService to recover from FATAL state 2026-03-21 13:37:14 +08:00
世界
ea464cef8d daemon: Fix CloseService leaving instance non-nil on close error 2026-03-21 13:23:57 +08:00
Andrew Novikov
a8e3cd3256 tun: Fix nfqueue not working in prerouting 2026-03-17 11:05:40 +08:00
世界
686cf1f304 documentation: Fix Chinese link anchors 2026-03-16 12:24:10 +08:00
世界
9fbfb87723 documentation: Fix unicode heading anchors 2026-03-16 12:10:32 +08:00
世界
d2fa21d07b Deprecate Socksaddr.IsFqdn: do not reject potentially valid domain names 2026-03-16 09:37:59 +08:00
世界
d3768cca36 Bump version 2026-03-15 17:56:37 +08:00
世界
0889ddd001 Fix connector canceled dial cleanup 2026-03-15 17:56:37 +08:00
深鸣
f46fbf188a documentation: Minor fixes 2026-03-15 17:56:37 +08:00
世界
f2d15139f5 tun: Fix nftables single include_uid not working 2026-03-15 16:58:34 +08:00
世界
041646b728 Fix kTLS crash 2026-03-14 21:38:38 +08:00
世界
b990de2e12 tun: Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" 2026-03-14 21:38:38 +08:00
世界
fe585157d2 Bump version 2026-03-14 21:38:38 +08:00
世界
eed6a36e5d tun:Fix auto_redirect dropping SO_BINDTODEVICE traffic 2026-03-14 21:38:38 +08:00
世界
eb0f38544c tailscale: Fix system interface rules 2026-03-14 21:38:38 +08:00
世界
54468a1a2a platform: Add f-droid update helpers 2026-03-11 20:41:29 +08:00
世界
8289bbd846 Add Alpine APK packaging to CI build
Add fpm-based Alpine APK packaging alongside existing DEB/RPM/Pacman
packages. Alpine APKs use `linux` in the filename to distinguish from
OpenWrt APKs which use the `openwrt` prefix.
2026-03-11 20:41:29 +08:00
世界
49c450d942 ccm/ocm: Fix missing metering for 1M context and /fast mode
CCM: Fix 1M context detection - use prefix match for versioned
beta strings (e.g. "context-1m-2025-08-07") and include cache
tokens in the 200K threshold check per Anthropic billing docs.

OCM: Add GPT-5.4 family pricing (standard/priority/flex) with
extended context (>272K) premium pricing support. Add context
window tracking to usage combinations, mirroring CCM's pattern.
Update normalizeGPT5Model defaults to latest known models.
2026-03-11 20:41:29 +08:00
世界
a7ee943216 Fix tailscale connections 2026-03-11 00:27:15 +08:00
世界
8bb4c4dd32 documentation: Update ocm/ccm examples 2026-03-10 22:04:12 +08:00
世界
67621ee6ba Fix OCM websocket proxy lifecycle and headers 2026-03-10 22:04:11 +08:00
世界
a09ffe6a0f ccm/ocm: Add by_user_and_week cost summary 2026-03-10 22:04:11 +08:00
世界
e0be8743f6 ocm: Add Responses WebSocket API proxy and fix client config docs
Support the OpenAI Responses WebSocket API (`wss://.../v1/responses`)
for bidirectional frame proxying with usage tracking.
Fix Codex CLI client config examples to use profiles and correct flags.

Update openai-go v3.24.0 → v3.26.0.
2026-03-10 22:04:11 +08:00
世界
0b04528803 tailscaile: Fix using TUN auto redirect with tailscale system interface 2026-03-10 22:04:11 +08:00
世界
65875e6dac tailscale: Use system dialer for system interface
* Revert "Fix netstack TCP connections with system interface
2026-03-10 19:50:16 +08:00
世界
4d6fb1d38d Fix legacy DNS client_subnet options not working 2026-03-09 20:18:47 +08:00
世界
305b930d90 release: Fix default config 2026-03-09 20:18:43 +08:00
世界
bc3884ca91 release: Add openwrt apk build 2026-03-09 20:18:40 +08:00
世界
df0bf927e4 Fix missing with_gvisor build tag for tailscale 2026-03-09 20:18:28 +08:00
世界
efe20ea51c release: Backport Go 1.25 to macOS 10.13 2026-03-09 20:13:36 +08:00
世界
e21a72fcd1 Fix websocket connection and goroutine leaks in Clash API
Co-authored-by: traitman <112139837+traitman@users.noreply.github.com>
2026-03-09 20:06:34 +08:00
世界
e1477bd065 documentation: Update cronet-go descriptions 2026-03-09 20:06:34 +08:00
世界
aa495fce38 Fix local DNS transport CNAME chain broken with systemd-resolved
Replace D-Bus ResolveRecord API with direct raw DNS queries to upstream
servers obtained from systemd-resolved's per-interface link properties.
2026-03-09 20:06:34 +08:00
世界
9cd60c28c0 tailscale: Fix inbound UDP packet connection 2026-03-09 20:06:34 +08:00
Heng lu
2ba896c5ac Fix netns fd leak in ListenNetworkNamespace 2026-03-09 20:06:34 +08:00
Oleg Artyomov
1d388547ee service/ccm: strip Accept-Encoding before forwarding to avoid untracked usage
When clients (e.g. Node.js Anthropic SDK) explicitly set Accept-Encoding: gzip,
Go's http.Transport does not transparently decompress the response body, because
it only does so when it added the header itself. This causes CCM's json.Unmarshal
to receive raw gzip bytes, silently failing to parse usage data and leaving the
usage counter unchanged.

Fix: remove Accept-Encoding from the outgoing proxy request. Transport adds it
automatically and transparently decompresses response.Body before CCM reads it.

Wire compression (CCM→Anthropic) is preserved — Transport still negotiates gzip.
Only CCM→localhost path is affected; compression on loopback has no practical
benefit.
2026-03-09 20:06:34 +08:00
世界
e343cec4d5 Fix legacy DNS defaults on final transport 2026-03-09 20:06:34 +08:00
世界
d58efc5d01 cronet-go: Fix library search path 2026-03-09 20:06:34 +08:00
世界
4b26ab16fb Bump version 2026-03-07 16:13:23 +08:00
世界
0e27312eda Update Go to 1.25.8 2026-03-07 16:13:23 +08:00
401 changed files with 28920 additions and 3707 deletions

View File

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

View File

@@ -1 +1 @@
cba7b9ac0399055aa49fbdc57c03c374f58e1597
335e5bef5d88fc4474c9a70b865561f45a67de83

81
.github/build_alpine_apk.sh vendored Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -e -o pipefail
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
# 1.13.0 -> 1.13.0-r0
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
APK_VERSION="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
trap 'rm -rf "$ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box"
# Service files
install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service"
install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/conf.d/sing-box
/etc/init.d/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later with name use or association addition" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

80
.github/build_openwrt_apk.sh vendored Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -e -o pipefail
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
# 1.13.0 -> 1.13.0-r0
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
APK_VERSION="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
trap 'rm -rf "$ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box"
install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/config/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
--info "provider-priority:100" \
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

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

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

45
.github/setup_go_for_macos1013.sh vendored Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.25.8"
PATCH_COMMITS=(
"afe69d3cec1c6dcf0f1797b20546795730850070"
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz"
tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
#cp -a go go_bootstrap
mv go go_osx
cd go_osx
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
# revert:
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done
# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external
# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the
# stdlib (crypto/x509) is compiled from patched src automatically.
#cd src
#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash
#cd ../..
#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz"

View File

@@ -1,16 +1,35 @@
#!/usr/bin/env bash
VERSION="1.25.7"
set -euo pipefail
mkdir -p $HOME/go
cd $HOME/go
VERSION="1.25.8"
PATCH_COMMITS=(
"466f6c7a29bc098b0d4c987b803c779222894a11"
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
"a90777dcf692dd2168577853ba743b4338721b06"
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_win7
cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.25.x
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert:
@@ -18,10 +37,10 @@ cd go_win7
# 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"
# fixes:
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done

View File

@@ -47,7 +47,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -72,27 +72,27 @@ jobs:
include:
- { os: linux, arch: amd64, variant: purego, naive: true }
- { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true }
- { os: linux, arch: arm64, variant: glibc, naive: true }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: "386", go386: sse2 }
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" }
- { os: linux, arch: arm, goarm: "7" }
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
- { os: linux, arch: riscv64, naive: true, variant: glibc }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, naive: true, variant: glibc }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
@@ -121,15 +121,10 @@ jobs:
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
if: ${{ ! matrix.legacy_win7 }}
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.10
go-version: ~1.25.8
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
@@ -137,9 +132,11 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1255
key: go_win7_1258
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |-
.github/setup_go_for_windows7.sh
- name: Setup Go for Windows 7
@@ -399,6 +396,30 @@ jobs:
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done
rm "dist/openwrt.deb"
- name: Install apk-tools
if: matrix.openwrt != '' || matrix.alpine != ''
run: |-
docker run --rm -v /usr/local/bin:/mnt alpine:edge sh -c "apk add --no-cache apk-tools-static && cp /sbin/apk.static /mnt/apk && chmod +x /mnt/apk"
- name: Package OpenWrt APK
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
for architecture in ${{ matrix.openwrt }}; do
.github/build_openwrt_apk.sh \
"$architecture" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.apk"
done
- name: Package Alpine APK
if: matrix.alpine != ''
run: |-
set -xeuo pipefail
.github/build_alpine_apk.sh \
"${{ matrix.alpine }}" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.alpine }}.apk"
- name: Archive
run: |
set -xeuo pipefail
@@ -434,22 +455,36 @@ jobs:
include:
- { arch: amd64 }
- { arch: arm64 }
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! matrix.legacy_go124 }}
if: ${{ ! matrix.legacy_osx }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
- name: Cache Go for macOS 10.13
if: matrix.legacy_osx
id: cache-go-for-macos1013
uses: actions/cache@v4
with:
go-version: ~1.24.6
path: |
~/go/go_osx
key: go_osx_1258
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |-
.github/setup_go_for_macos1013.sh
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx
run: |-
echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -457,7 +492,7 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
@@ -477,6 +512,7 @@ jobs:
CGO_ENABLED: "1"
GOOS: darwin
GOARCH: ${{ matrix.arch }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name
run: |-
@@ -605,7 +641,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -695,7 +731,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -794,7 +830,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Set tag
if: matrix.if
run: |-

View File

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

View File

@@ -11,11 +11,6 @@ on:
description: "Version name"
required: true
type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release:
types:
- published
@@ -23,7 +18,6 @@ on:
jobs:
calculate_version:
name: Calculate version
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
@@ -35,7 +29,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -78,7 +72,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -168,14 +162,8 @@ jobs:
- name: Set mtime
run: |-
TZ=UTC touch -t '197001010000' dist/sing-box
- name: Set name
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Detect track
run: bash .github/detect_track.sh
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package adapter
import (
"context"
"net/netip"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
@@ -25,18 +26,19 @@ type DNSRouter interface {
type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
ClearCache()
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
DisableOptimisticCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
@@ -49,11 +51,12 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
DisableOptimisticCache: options.DisableOptimisticCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
@@ -63,6 +66,13 @@ type RDRCStore interface {
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
}
type DNSCacheStore interface {
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
ClearDNSCache() error
}
type DNSTransport interface {
Lifecycle
Type() string
@@ -72,11 +82,6 @@ type DNSTransport interface {
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}
type LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}
type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)

View File

@@ -47,6 +47,12 @@ type CacheFile interface {
StoreRDRC() bool
RDRCStore
StoreDNS() bool
DNSCacheStore
SetDisableExpire(disableExpire bool)
SetOptimisticTimeout(timeout time.Duration)
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string

13
adapter/http.go Normal file
View File

@@ -0,0 +1,13 @@
package adapter
import (
"net/http"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/logger"
)
type HTTPClientManager interface {
ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error)
DefaultTransport() http.RoundTripper
}

View File

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

45
adapter/inbound_test.go Normal file
View File

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

23
adapter/neighbor.go Normal file
View File

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

View File

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

View File

@@ -2,17 +2,11 @@ package adapter
import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
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/x/list"
"go4.org/netipx"
@@ -26,6 +20,8 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}
@@ -49,7 +45,7 @@ type ConnectionRouterEx interface {
type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
StartContext(ctx context.Context) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
@@ -64,51 +60,14 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
type DNSRuleSetUpdateValidator interface {
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
}
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
ctx context.Context
access sync.Mutex
httpClientCache map[string]*http.Client
}
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
return &HTTPStartContext{
ctx: ctx,
httpClientCache: make(map[string]*http.Client),
}
}
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
c.access.Lock()
defer c.access.Unlock()
if httpClient, loaded := c.httpClientCache[detour]; loaded {
return httpClient
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(c.ctx),
RootCAs: RootPoolFromContext(c.ctx),
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *HTTPStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsDNSQueryTypeRule bool
}

View File

@@ -2,6 +2,8 @@ package adapter
import (
C "github.com/sagernet/sing-box/constant"
"github.com/miekg/dns"
)
type HeadlessRule interface {
@@ -18,8 +20,9 @@ type Rule interface {
type DNSRule interface {
Rule
LegacyPreMatch(metadata *InboundContext) bool
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
}
type RuleAction interface {
@@ -29,7 +32,7 @@ type RuleAction interface {
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
return false
default:
return true

49
adapter/tailscale.go Normal file
View File

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

175
box.go
View File

@@ -9,19 +9,21 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/httpclient"
"github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
@@ -37,20 +39,22 @@ import (
var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct {
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
certificateProvider *boxCertificate.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
httpClientService adapter.LifecycleService
internalService []adapter.LifecycleService
done chan struct{}
}
type Options struct {
@@ -66,6 +70,7 @@ func Context(
endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
certificateProviderRegistry adapter.CertificateProviderRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -90,6 +95,10 @@ func Context(
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
}
return ctx
}
@@ -106,6 +115,7 @@ func New(options Options) (*Box, error) {
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
@@ -122,6 +132,9 @@ func New(options Options) (*Box, error) {
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
if certificateProviderRegistry == nil {
return nil, E.New("missing certificate provider registry in context")
}
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -159,6 +172,10 @@ func New(options Options) (*Box, error) {
}
var internalServices []adapter.LifecycleService
routeOptions := common.PtrValueOrDefault(options.Route)
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
httpClientService := adapter.LifecycleService(httpClientManager)
certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 ||
@@ -171,21 +188,25 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
internalServices = append(internalServices, certificateStore)
}
routeOptions := common.PtrValueOrDefault(options.Route)
dnsOptions := common.PtrValueOrDefault(options.DNS)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize DNS router")
}
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
@@ -272,6 +293,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize inbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
for i, outboundOptions := range options.Outbounds {
var tag string
if outboundOptions.Tag != "" {
@@ -298,22 +337,22 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
for i, certificateProviderOptions := range options.CertificateProviders {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
if certificateProviderOptions.Tag != "" {
tag = certificateProviderOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
err = certificateProviderManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
certificateProviderOptions.Type,
certificateProviderOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
}
}
outboundManager.Initialize(func() (adapter.Outbound, error) {
@@ -326,13 +365,20 @@ func New(options Options) (*Box, error) {
)
})
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
return local.NewTransport(
return dnsTransportRegistry.CreateDNSTransport(
ctx,
logFactory.NewLogger("dns/local"),
"local",
option.LocalDNSServerOptions{},
C.DNSTypeLocal,
&option.LocalDNSServerOptions{},
)
})
httpClientManager.Initialize(func() (*httpclient.Client, error) {
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
var httpClientOptions option.HTTPClientOptions
httpClientOptions.DefaultOutbound = true
return httpclient.NewClient(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
})
if platformInterface != nil {
err = platformInterface.Initialize(networkManager)
if err != nil {
@@ -340,7 +386,7 @@ func New(options Options) (*Box, error) {
}
}
if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
internalServices = append(internalServices, cacheFile)
}
@@ -383,20 +429,22 @@ func New(options Options) (*Box, error) {
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
return &Box{
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
certificateProvider: certificateProviderManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
httpClientService: httpClientService,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
}, nil
}
@@ -450,11 +498,19 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
if err != nil {
return err
}
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
if err != nil {
return err
}
@@ -470,11 +526,19 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
@@ -482,7 +546,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
@@ -506,8 +570,9 @@ func (s *Box) Close() error {
service adapter.Lifecycle
}{
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"certificate-provider", s.certificateProvider},
{"endpoint", s.endpoint},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
@@ -522,6 +587,14 @@ func (s *Box) Close() error {
})
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
if s.httpClientService != nil {
s.logger.Trace("close ", s.httpClientService.Name())
startTime := time.Now()
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
return E.Cause(err, "close ", s.httpClientService.Name())
})
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
for _, lifecycleService := range s.internalService {
s.logger.Trace("close ", lifecycleService.Name())
startTime := time.Now()
@@ -555,6 +628,10 @@ func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}
func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ type DirectDialer interface {
type DetourDialer struct {
outboundManager adapter.OutboundManager
detour string
defaultOutbound bool
legacyDNSDialer bool
dialer N.Dialer
initOnce sync.Once
@@ -33,6 +34,13 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
}
}
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
return &DetourDialer{
outboundManager: outboundManager,
defaultOutbound: true,
}
}
func InitializeDetour(dialer N.Dialer) error {
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
if !isDetour {
@@ -47,12 +55,18 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
}
func (d *DetourDialer) init() {
dialer, loaded := d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
var dialer adapter.Outbound
if d.detour != "" {
var loaded bool
dialer, loaded = d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
}
} else {
dialer = d.outboundManager.Default()
}
if !d.legacyDNSDialer {
if !d.defaultOutbound && !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")

View File

@@ -25,6 +25,7 @@ type Options struct {
NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
DefaultOutbound bool
}
// TODO: merge with NewWithOptions
@@ -42,19 +43,26 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dialer N.Dialer
err error
)
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
if dialOptions.Detour != "" {
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
} else if options.DefaultOutbound {
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDefaultOutboundDetour(outboundManager)
} else {
dialer, err = NewDefault(options.Context, dialOptions)
if err != nil {
return nil, err
}
}
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
if options.RemoteIsDomain && (!hasDetour || 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
@@ -87,11 +95,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
}
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{}),
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
}
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver {

View File

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

154
common/httpclient/client.go Normal file
View File

@@ -0,0 +1,154 @@
package httpclient
import (
"context"
"io"
"net/http"
"time"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
type httpTransport interface {
http.RoundTripper
CloseIdleConnections()
Clone() httpTransport
}
type Client struct {
transport httpTransport
headers http.Header
host string
tag string
}
func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Client, error) {
rawDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: true,
ResolverOnDetour: options.ResolveOnDetour,
NewDialer: options.ResolveOnDetour,
DefaultOutbound: options.DefaultOutbound,
})
if err != nil {
return nil, err
}
tlsOptions := common.PtrValueOrDefault(options.TLS)
tlsOptions.Enabled = true
baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{
Context: ctx,
Logger: logger,
Options: tlsOptions,
AllowEmptyServerName: true,
})
if err != nil {
return nil, err
}
return NewClientWithDialer(rawDialer, baseTLSConfig, tag, options)
}
func NewClientWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Client, error) {
headers := options.Headers.Build()
host := headers.Get("Host")
headers.Del("Host")
transport, err := newTransport(rawDialer, baseTLSConfig, options)
if err != nil {
return nil, err
}
return &Client{
transport: transport,
headers: headers,
host: host,
tag: tag,
}, nil
}
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (httpTransport, error) {
version := options.Version
if version == 0 {
version = 2
}
fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay)
if fallbackDelay == 0 {
fallbackDelay = 300 * time.Millisecond
}
var transport httpTransport
var err error
switch version {
case 1:
transport = newHTTP1Transport(rawDialer, baseTLSConfig)
case 2:
if options.DisableVersionFallback {
transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options)
} else {
transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
}
case 3:
if baseTLSConfig != nil {
_, err = baseTLSConfig.STDConfig()
if err != nil {
return nil, err
}
}
if options.DisableVersionFallback {
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
} else {
var h2Fallback httpTransport
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
if err != nil {
return nil, err
}
transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay)
}
default:
return nil, E.New("unknown HTTP version: ", version)
}
if err != nil {
return nil, err
}
return transport, nil
}
func (c *Client) RoundTrip(request *http.Request) (*http.Response, error) {
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
return c.transport.RoundTrip(request)
}
if c.tag != "" {
if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == c.tag {
return nil, E.New("HTTP request loopback in transport[", c.tag, "]")
}
request = request.Clone(contextWithTransportTag(request.Context(), c.tag))
} else {
request = request.Clone(request.Context())
}
applyHeaders(request, c.headers, c.host)
return c.transport.RoundTrip(request)
}
func (c *Client) CloseIdleConnections() {
c.transport.CloseIdleConnections()
}
func (c *Client) Clone() *Client {
return &Client{
transport: c.transport.Clone(),
headers: c.headers.Clone(),
host: c.host,
tag: c.tag,
}
}
func (c *Client) Close() error {
c.CloseIdleConnections()
if closer, isCloser := c.transport.(io.Closer); isCloser {
return closer.Close()
}
return nil
}

View File

@@ -0,0 +1,14 @@
package httpclient
import "context"
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
}

View File

@@ -0,0 +1,86 @@
package httpclient
import (
"context"
stdTLS "crypto/tls"
"io"
"net"
"net/http"
"strings"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
if baseTLSConfig == nil {
return nil, E.New("TLS transport unavailable")
}
tlsConfig := baseTLSConfig.Clone()
if tlsConfig.ServerName() == "" && destination.IsValid() {
tlsConfig.SetServerName(destination.AddrString())
}
tlsConfig.SetNextProtos(nextProtos)
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil {
return nil, err
}
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
if err != nil {
conn.Close()
return nil, err
}
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
tlsConn.Close()
return nil, errHTTP2Fallback
}
return tlsConn, nil
}
func applyHeaders(request *http.Request, headers http.Header, host string) {
for header, values := range headers {
request.Header[header] = append([]string(nil), values...)
}
if host != "" {
request.Host = host
}
}
func requestRequiresHTTP1(request *http.Request) bool {
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
}
func requestReplayable(request *http.Request) bool {
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
}
func cloneRequestForRetry(request *http.Request) *http.Request {
cloned := request.Clone(request.Context())
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
cloned.Body = mustGetBody(request)
}
return cloned
}
func mustGetBody(request *http.Request) io.ReadCloser {
body, err := request.GetBody()
if err != nil {
panic(err)
}
return body
}
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
if baseTLSConfig == nil {
return nil, nil
}
tlsConfig := baseTLSConfig.Clone()
if tlsConfig.ServerName() == "" && destination.IsValid() {
tlsConfig.SetServerName(destination.AddrString())
}
tlsConfig.SetNextProtos(nextProtos)
return tlsConfig.STDConfig()
}

View File

@@ -0,0 +1,41 @@
package httpclient
import (
"context"
"net"
"net/http"
"github.com/sagernet/sing-box/common/tls"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type http1Transport struct {
transport *http.Transport
}
func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport {
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
}
if baseTLSConfig != nil {
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "")
}
}
return &http1Transport{transport: transport}
}
func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) {
return t.transport.RoundTrip(request)
}
func (t *http1Transport) CloseIdleConnections() {
t.transport.CloseIdleConnections()
}
func (t *http1Transport) Clone() httpTransport {
return &http1Transport{transport: t.transport.Clone()}
}

View File

@@ -0,0 +1,42 @@
package httpclient
import (
stdTLS "crypto/tls"
"net/http"
"time"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/net/http2"
)
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
return &http2.Transport{
ReadIdleTimeout: transport.ReadIdleTimeout,
PingTimeout: transport.PingTimeout,
DialTLSContext: transport.DialTLSContext,
}
}
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
stdTransport := &http.Transport{
TLSClientConfig: &stdTLS.Config{},
HTTP2: &http.HTTP2Config{
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
MaxConcurrentStreams: options.MaxConcurrentStreams,
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
PingTimeout: time.Duration(options.IdleTimeout),
},
}
h2Transport, err := http2.ConfigureTransports(stdTransport)
if err != nil {
return nil, E.Cause(err, "configure HTTP/2 transport")
}
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
h2Transport.ConnPool = nil
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
return h2Transport, nil
}

View File

@@ -0,0 +1,87 @@
package httpclient
import (
"context"
stdTLS "crypto/tls"
"errors"
"net"
"net/http"
"sync/atomic"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
var errHTTP2Fallback = E.New("fallback to HTTP/1.1")
type http2FallbackTransport struct {
h2Transport *http2.Transport
h1Transport *http1Transport
h2Fallback *atomic.Bool
}
func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) {
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
var fallback atomic.Bool
h2Transport, err := ConfigureHTTP2Transport(options)
if err != nil {
return nil, err
}
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS)
if dialErr != nil {
if errors.Is(dialErr, errHTTP2Fallback) {
fallback.Store(true)
}
return nil, dialErr
}
return conn, nil
}
return &http2FallbackTransport{
h2Transport: h2Transport,
h1Transport: h1,
h2Fallback: &fallback,
}, nil
}
func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
return t.roundTrip(request, true)
}
func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) {
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
return t.h1Transport.RoundTrip(request)
}
if t.h2Fallback.Load() {
if !allowHTTP1Fallback {
return nil, errHTTP2Fallback
}
return t.h1Transport.RoundTrip(request)
}
response, err := t.h2Transport.RoundTrip(request)
if err == nil {
return response, nil
}
if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback {
return nil, err
}
return t.h1Transport.RoundTrip(cloneRequestForRetry(request))
}
func (t *http2FallbackTransport) CloseIdleConnections() {
t.h1Transport.CloseIdleConnections()
t.h2Transport.CloseIdleConnections()
}
func (t *http2FallbackTransport) Clone() httpTransport {
return &http2FallbackTransport{
h2Transport: CloneHTTP2Transport(t.h2Transport),
h1Transport: t.h1Transport.Clone().(*http1Transport),
h2Fallback: t.h2Fallback,
}
}

View File

@@ -0,0 +1,54 @@
package httpclient
import (
"context"
stdTLS "crypto/tls"
"net"
"net/http"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
type http2Transport struct {
h2Transport *http2.Transport
h1Transport *http1Transport
}
func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) {
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
h2Transport, err := ConfigureHTTP2Transport(options)
if err != nil {
return nil, err
}
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS)
}
return &http2Transport{
h2Transport: h2Transport,
h1Transport: h1,
}, nil
}
func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) {
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
return t.h1Transport.RoundTrip(request)
}
return t.h2Transport.RoundTrip(request)
}
func (t *http2Transport) CloseIdleConnections() {
t.h1Transport.CloseIdleConnections()
t.h2Transport.CloseIdleConnections()
}
func (t *http2Transport) Clone() httpTransport {
return &http2Transport{
h2Transport: CloneHTTP2Transport(t.h2Transport),
h1Transport: t.h1Transport.Clone().(*http1Transport),
}
}

View File

@@ -0,0 +1,311 @@
//go:build with_quic
package httpclient
import (
"context"
stdTLS "crypto/tls"
"errors"
"net/http"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
"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"
)
type http3Transport struct {
h3Transport *http3.Transport
}
type http3FallbackTransport struct {
h3Transport *http3.Transport
h2Fallback httpTransport
fallbackDelay time.Duration
brokenAccess sync.Mutex
brokenUntil time.Time
brokenBackoff time.Duration
}
func newHTTP3RoundTripper(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
options option.QUICOptions,
) *http3.Transport {
var handshakeTimeout time.Duration
if baseTLSConfig != nil {
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
MaxIdleTimeout: time.Duration(options.IdleTimeout),
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
}
if options.InitialPacketSize > 0 {
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
}
if options.MaxConcurrentStreams > 0 {
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
}
if handshakeTimeout > 0 {
quicConfig.HandshakeIdleTimeout = handshakeTimeout
}
h3Transport := &http3.Transport{
TLSClientConfig: &stdTLS.Config{},
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) {
if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 {
quicConfig = quicConfig.Clone()
quicConfig.HandshakeIdleTimeout = handshakeTimeout
}
if baseTLSConfig != nil {
var err error
tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3})
if err != nil {
return nil, err
}
} else {
tlsConfig = tlsConfig.Clone()
tlsConfig.NextProtos = []string{http3.NextProtoH3}
}
conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
if err != nil {
return nil, err
}
quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig)
if err != nil {
conn.Close()
return nil, err
}
return quicConn, nil
},
}
return h3Transport
}
func newHTTP3Transport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
options option.QUICOptions,
) (httpTransport, error) {
return &http3Transport{
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
}, nil
}
func newHTTP3FallbackTransport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
h2Fallback httpTransport,
options option.QUICOptions,
fallbackDelay time.Duration,
) (httpTransport, error) {
return &http3FallbackTransport{
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
h2Fallback: h2Fallback,
fallbackDelay: fallbackDelay,
}, nil
}
func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) {
return t.h3Transport.RoundTrip(request)
}
func (t *http3Transport) CloseIdleConnections() {
t.h3Transport.CloseIdleConnections()
}
func (t *http3Transport) Close() error {
t.CloseIdleConnections()
return t.h3Transport.Close()
}
func (t *http3Transport) Clone() httpTransport {
return &http3Transport{
h3Transport: t.h3Transport,
}
}
func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
return t.h2Fallback.RoundTrip(request)
}
return t.roundTripHTTP3(request)
}
func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) {
if t.h3Broken() {
return t.h2FallbackRoundTrip(request)
}
response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true})
if err == nil {
t.clearH3Broken()
return response, nil
}
if !errors.Is(err, http3.ErrNoCachedConn) {
t.markH3Broken()
return t.h2FallbackRoundTrip(cloneRequestForRetry(request))
}
if !requestReplayable(request) {
response, err = t.h3Transport.RoundTrip(request)
if err == nil {
t.clearH3Broken()
return response, nil
}
t.markH3Broken()
return nil, err
}
return t.roundTripHTTP3Race(request)
}
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) {
ctx, cancel := context.WithCancel(request.Context())
defer cancel()
type result struct {
response *http.Response
err error
h3 bool
}
results := make(chan result, 2)
startRoundTrip := func(request *http.Request, useH3 bool) {
request = request.WithContext(ctx)
var (
response *http.Response
err error
)
if useH3 {
response, err = t.h3Transport.RoundTrip(request)
} else {
response, err = t.h2FallbackRoundTrip(request)
}
results <- result{response: response, err: err, h3: useH3}
}
goroutines := 1
received := 0
drainRemaining := func() {
cancel()
for range goroutines - received {
go func() {
loser := <-results
if loser.response != nil && loser.response.Body != nil {
loser.response.Body.Close()
}
}()
}
}
go startRoundTrip(cloneRequestForRetry(request), true)
timer := time.NewTimer(t.fallbackDelay)
defer timer.Stop()
var (
h3Err error
fallbackErr error
)
for {
select {
case <-timer.C:
if goroutines == 1 {
goroutines++
go startRoundTrip(cloneRequestForRetry(request), false)
}
case raceResult := <-results:
received++
if raceResult.err == nil {
if raceResult.h3 {
t.clearH3Broken()
}
drainRemaining()
return raceResult.response, nil
}
if raceResult.h3 {
t.markH3Broken()
h3Err = raceResult.err
if goroutines == 1 {
goroutines++
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
go startRoundTrip(cloneRequestForRetry(request), false)
}
} else {
fallbackErr = raceResult.err
}
if received < goroutines {
continue
}
drainRemaining()
switch {
case h3Err != nil && fallbackErr != nil:
return nil, E.Errors(h3Err, fallbackErr)
case fallbackErr != nil:
return nil, fallbackErr
default:
return nil, h3Err
}
}
}
}
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
return fallback.roundTrip(request, true)
}
return t.h2Fallback.RoundTrip(request)
}
func (t *http3FallbackTransport) CloseIdleConnections() {
t.h3Transport.CloseIdleConnections()
t.h2Fallback.CloseIdleConnections()
}
func (t *http3FallbackTransport) Close() error {
t.CloseIdleConnections()
return t.h3Transport.Close()
}
func (t *http3FallbackTransport) Clone() httpTransport {
return &http3FallbackTransport{
h3Transport: t.h3Transport,
h2Fallback: t.h2Fallback.Clone(),
fallbackDelay: t.fallbackDelay,
}
}
func (t *http3FallbackTransport) h3Broken() bool {
t.brokenAccess.Lock()
defer t.brokenAccess.Unlock()
return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil)
}
func (t *http3FallbackTransport) clearH3Broken() {
t.brokenAccess.Lock()
t.brokenUntil = time.Time{}
t.brokenBackoff = 0
t.brokenAccess.Unlock()
}
func (t *http3FallbackTransport) markH3Broken() {
t.brokenAccess.Lock()
defer t.brokenAccess.Unlock()
if t.brokenBackoff == 0 {
t.brokenBackoff = 5 * time.Minute
} else {
t.brokenBackoff *= 2
if t.brokenBackoff > 48*time.Hour {
t.brokenBackoff = 48 * time.Hour
}
}
t.brokenUntil = time.Now().Add(t.brokenBackoff)
}

View File

@@ -0,0 +1,30 @@
//go:build !with_quic
package httpclient
import (
"time"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
)
func newHTTP3FallbackTransport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
h2Fallback httpTransport,
options option.QUICOptions,
fallbackDelay time.Duration,
) (httpTransport, error) {
return nil, E.New("HTTP/3 requires building with the with_quic tag")
}
func newHTTP3Transport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
options option.QUICOptions,
) (httpTransport, error) {
return nil, E.New("HTTP/3 requires building with the with_quic tag")
}

View File

@@ -0,0 +1,136 @@
package httpclient
import (
"context"
"net/http"
"sync"
"github.com/sagernet/sing-box/adapter"
"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"
)
var (
_ adapter.HTTPClientManager = (*Manager)(nil)
_ adapter.LifecycleService = (*Manager)(nil)
)
type Manager struct {
ctx context.Context
logger log.ContextLogger
access sync.Mutex
defines map[string]option.HTTPClient
clients map[string]*Client
defaultTag string
defaultTransport http.RoundTripper
defaultTransportFallback func() (*Client, error)
fallbackClient *Client
}
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
defines := make(map[string]option.HTTPClient, len(clients))
for _, client := range clients {
defines[client.Tag] = client
}
defaultTag := defaultHTTPClient
if defaultTag == "" && len(clients) > 0 {
defaultTag = clients[0].Tag
}
return &Manager{
ctx: ctx,
logger: logger,
defines: defines,
clients: make(map[string]*Client),
defaultTag: defaultTag,
}
}
func (m *Manager) Initialize(defaultTransportFallback func() (*Client, error)) {
m.defaultTransportFallback = defaultTransportFallback
}
func (m *Manager) Name() string {
return "http-client"
}
func (m *Manager) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if m.defaultTag != "" {
transport, err := m.resolveShared(m.defaultTag)
if err != nil {
return E.Cause(err, "resolve default http client")
}
m.defaultTransport = transport
} else if m.defaultTransportFallback != nil {
client, err := m.defaultTransportFallback()
if err != nil {
return E.Cause(err, "create default http client")
}
m.defaultTransport = client
m.fallbackClient = client
}
return nil
}
func (m *Manager) DefaultTransport() http.RoundTripper {
return m.defaultTransport
}
func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error) {
if options.Tag != "" {
if options.ResolveOnDetour {
define, loaded := m.defines[options.Tag]
if !loaded {
return nil, E.New("http_client not found: ", options.Tag)
}
resolvedOptions := define.Options()
resolvedOptions.ResolveOnDetour = true
return NewClient(m.ctx, logger, options.Tag, resolvedOptions)
}
return m.resolveShared(options.Tag)
}
return NewClient(m.ctx, logger, "", options)
}
func (m *Manager) resolveShared(tag string) (http.RoundTripper, error) {
m.access.Lock()
defer m.access.Unlock()
if client, loaded := m.clients[tag]; loaded {
return client, nil
}
define, loaded := m.defines[tag]
if !loaded {
return nil, E.New("http_client not found: ", tag)
}
client, err := NewClient(m.ctx, m.logger, tag, define.Options())
if err != nil {
return nil, E.Cause(err, "create shared http_client[", tag, "]")
}
m.clients[tag] = client
return client, nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if m.clients == nil {
return nil
}
var err error
for _, client := range m.clients {
err = E.Append(err, client.Close(), func(err error) error {
return E.Cause(err, "close http client")
})
}
if m.fallbackClient != nil {
err = E.Append(err, m.fallbackClient.Close(), func(err error) error {
return E.Cause(err, "close default http client")
})
}
m.clients = nil
return err
}

View File

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

View File

@@ -151,6 +151,7 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get current netns")
}
defer currentNs.Close()
defer netns.Set(currentNs)
var targetNs netns.NsHandle
if strings.HasPrefix(nameOrPath, "/") {

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

@@ -8,14 +8,16 @@ import (
"os"
"github.com/sagernet/sing-box/common/badtls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
)
var errMissingServerName = E.New("missing server_name or insecure=true")
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
if !options.Enabled {
return dialer, nil
@@ -42,11 +44,12 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
}
type ClientOptions struct {
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
KTLSCompatible bool
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
AllowEmptyServerName bool
KTLSCompatible bool
}
func NewClientWithOptions(options ClientOptions) (Config, error) {
@@ -62,16 +65,14 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
}
if options.Options.Reality != nil && options.Options.Reality.Enabled {
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
}
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
}
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
if err != nil {
return nil, err

View File

@@ -52,11 +52,15 @@ type RealityClientConfig struct {
}
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newRealityClient(ctx, logger, serverAddress, options, false)
}
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
if options.UTLS == nil || !options.UTLS.Enabled {
return nil, E.New("uTLS is required by reality client")
}
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
if err != nil {
return nil, err
}
@@ -108,6 +112,14 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
e.uClient.SetNextProtos(nextProto)
}
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
return e.uClient.HandshakeTimeout()
}
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
e.uClient.SetHandshakeTimeout(timeout)
}
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
return nil, E.New("unsupported usage for reality")
}

View File

@@ -26,12 +26,17 @@ import (
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
type RealityServerConfig struct {
config *utls.RealityConfig
config *utls.RealityConfig
handshakeTimeout time.Duration
}
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
var tlsConfig utls.RealityConfig
if options.CertificateProvider != nil {
return nil, E.New("certificate_provider is unavailable in reality")
}
//nolint:staticcheck
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality")
}
@@ -126,7 +131,16 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
if options.ECH != nil && options.ECH.Enabled {
return nil, E.New("Reality is conflict with ECH")
}
var config ServerConfig = &RealityServerConfig{&tlsConfig}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
var config ServerConfig = &RealityServerConfig{
config: &tlsConfig,
handshakeTimeout: handshakeTimeout,
}
if options.KernelTx || options.KernelRx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
@@ -157,6 +171,14 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
return nil, E.New("unsupported usage for reality")
}
@@ -187,7 +209,8 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
func (c *RealityServerConfig) Clone() Config {
return &RealityServerConfig{
config: c.config.Clone(),
config: c.config.Clone(),
handshakeTimeout: c.handshakeTimeout,
}
}

View File

@@ -46,8 +46,11 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
}
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
if config.HandshakeTimeout() == 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
}
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
if err != nil {
return nil, err

View File

@@ -24,16 +24,30 @@ import (
type STDClientConfig struct {
ctx context.Context
config *tls.Config
serverName string
disableSNI bool
verifyServerName bool
handshakeTimeout time.Duration
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
}
func (c *STDClientConfig) ServerName() string {
return c.config.ServerName
return c.serverName
}
func (c *STDClientConfig) SetServerName(serverName string) {
c.serverName = serverName
if c.disableSNI {
c.config.ServerName = ""
if c.verifyServerName {
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
} else {
c.config.VerifyConnection = nil
}
return
}
c.config.ServerName = serverName
}
@@ -45,6 +59,14 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
return c.config, nil
}
@@ -57,13 +79,19 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
}
func (c *STDClientConfig) Clone() Config {
return &STDClientConfig{
cloned := &STDClientConfig{
ctx: c.ctx,
config: c.config.Clone(),
serverName: c.serverName,
disableSNI: c.disableSNI,
verifyServerName: c.verifyServerName,
handshakeTimeout: c.handshakeTimeout,
fragment: c.fragment,
fragmentFallbackDelay: c.fragmentFallbackDelay,
recordFragment: c.recordFragment,
}
cloned.SetServerName(cloned.serverName)
return cloned
}
func (c *STDClientConfig) ECHConfigList() []byte {
@@ -75,41 +103,27 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
}
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newSTDClient(ctx, logger, serverAddress, options, false)
}
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
serverName = serverAddress
}
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
if serverName == "" && !options.Insecure && !allowEmptyServerName {
return nil, errMissingServerName
}
var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI {
tlsConfig.ServerName = serverName
}
if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI {
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if tlsConfig.Time != nil {
verifyOptions.CurrentTime = tlsConfig.Time()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
}
if len(options.CertificatePublicKeySHA256) > 0 {
if len(options.Certificate) > 0 || options.CertificatePath != "" {
@@ -198,7 +212,24 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
var config Config = &STDClientConfig{
ctx: ctx,
config: &tlsConfig,
serverName: serverName,
disableSNI: options.DisableSNI,
verifyServerName: options.DisableSNI && !options.Insecure,
handshakeTimeout: handshakeTimeout,
fragment: options.Fragment,
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
recordFragment: options.RecordFragment,
}
config.SetServerName(serverName)
if options.ECH != nil && options.ECH.Enabled {
var err error
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
@@ -220,6 +251,27 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
return config, nil
}
func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverName string) func(state tls.ConnectionState) error {
return func(state tls.ConnectionState) error {
if serverName == "" {
return errMissingServerName
}
verifyOptions := x509.VerifyOptions{
Roots: rootCAs,
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if timeFunc != nil {
verifyOptions.CurrentTime = timeFunc()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
}
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
if err != nil {

View File

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

View File

@@ -28,6 +28,10 @@ import (
type UTLSClientConfig struct {
ctx context.Context
config *utls.Config
serverName string
disableSNI bool
verifyServerName bool
handshakeTimeout time.Duration
id utls.ClientHelloID
fragment bool
fragmentFallbackDelay time.Duration
@@ -35,10 +39,20 @@ type UTLSClientConfig struct {
}
func (c *UTLSClientConfig) ServerName() string {
return c.config.ServerName
return c.serverName
}
func (c *UTLSClientConfig) SetServerName(serverName string) {
c.serverName = serverName
if c.disableSNI {
c.config.ServerName = ""
if c.verifyServerName {
c.config.InsecureServerNameToVerify = serverName
} else {
c.config.InsecureServerNameToVerify = ""
}
return
}
c.config.ServerName = serverName
}
@@ -53,6 +67,14 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS")
}
@@ -69,9 +91,20 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
}
func (c *UTLSClientConfig) Clone() Config {
return &UTLSClientConfig{
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
cloned := &UTLSClientConfig{
ctx: c.ctx,
config: c.config.Clone(),
serverName: c.serverName,
disableSNI: c.disableSNI,
verifyServerName: c.verifyServerName,
handshakeTimeout: c.handshakeTimeout,
id: c.id,
fragment: c.fragment,
fragmentFallbackDelay: c.fragmentFallbackDelay,
recordFragment: c.recordFragment,
}
cloned.SetServerName(cloned.serverName)
return cloned
}
func (c *UTLSClientConfig) ECHConfigList() []byte {
@@ -143,29 +176,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
}
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newUTLSClient(ctx, logger, serverAddress, options, false)
}
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
serverName = serverAddress
}
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
if serverName == "" && !options.Insecure && !allowEmptyServerName {
return nil, errMissingServerName
}
var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI {
tlsConfig.ServerName = serverName
}
if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("disable_sni is unsupported in reality")
}
tlsConfig.InsecureServerNameToVerify = serverName
}
if len(options.CertificatePublicKeySHA256) > 0 {
if len(options.Certificate) > 0 || options.CertificatePath != "" {
@@ -251,11 +284,29 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
if err != nil {
return nil, err
}
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
var config Config = &UTLSClientConfig{
ctx: ctx,
config: &tlsConfig,
serverName: serverName,
disableSNI: options.DisableSNI,
verifyServerName: options.DisableSNI && !options.Insecure,
handshakeTimeout: handshakeTimeout,
id: id,
fragment: options.Fragment,
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
recordFragment: options.RecordFragment,
}
config.SetServerName(serverName)
if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH")

View File

@@ -12,10 +12,18 @@ import (
)
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newUTLSClient(ctx, logger, serverAddress, options, false)
}
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
}
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newRealityClient(ctx, logger, serverAddress, options, false)
}
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import (
"errors"
"net"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -14,7 +13,6 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
@@ -32,59 +30,63 @@ var (
var _ adapter.DNSClient = (*Client)(nil)
type Client struct {
timeout time.Duration
disableCache bool
disableExpire bool
independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger
cache freelru.Cache[dns.Question, *dns.Msg]
cacheLock compatible.Map[dns.Question, chan struct{}]
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
transportCacheLock compatible.Map[dns.Question, chan struct{}]
ctx context.Context
timeout time.Duration
disableCache bool
disableExpire bool
optimisticTimeout time.Duration
cacheCapacity uint32
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
dnsCache adapter.DNSCacheStore
initDNSCacheFunc func() adapter.DNSCacheStore
logger logger.ContextLogger
cache freelru.Cache[dnsCacheKey, *dns.Msg]
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
}
type ClientOptions struct {
Timeout time.Duration
DisableCache bool
DisableExpire bool
IndependentCache bool
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
Logger logger.ContextLogger
Context context.Context
Timeout time.Duration
DisableCache bool
DisableExpire bool
OptimisticTimeout time.Duration
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
DNSCache func() adapter.DNSCacheStore
Logger logger.ContextLogger
}
func NewClient(options ClientOptions) *Client {
client := &Client{
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
}
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))
}
client := &Client{
ctx: options.Context,
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
optimisticTimeout: options.OptimisticTimeout,
cacheCapacity: cacheCapacity,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
initDNSCacheFunc: options.DNSCache,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
if !client.disableCache && client.initDNSCacheFunc == nil {
client.initializeMemoryCache()
}
return client
}
type transportCacheKey struct {
type dnsCacheKey struct {
dns.Question
transportTag string
}
@@ -93,6 +95,19 @@ func (c *Client) Start() {
if c.initRDRCFunc != nil {
c.rdrc = c.initRDRCFunc()
}
if c.initDNSCacheFunc != nil {
c.dnsCache = c.initDNSCacheFunc()
}
if c.dnsCache == nil {
c.initializeMemoryCache()
}
}
func (c *Client) initializeMemoryCache() {
if c.disableCache || c.cache != nil {
return
}
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
}
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
@@ -109,7 +124,38 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
func computeTimeToLive(response *dns.Msg) uint32 {
var timeToLive uint32
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
return soaTTL
}
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
return timeToLive
}
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
if len(message.Question) == 0 {
if c.logger != nil {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
@@ -123,13 +169,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
return FixedResponseStatus(message, dns.RcodeSuccess), nil
}
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
message = c.prepareExchangeMessage(message, options)
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
@@ -141,40 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
!options.ClientSubnet.IsValid()
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
if !disableCache {
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.cacheLock.Delete(question)
close(cond)
}()
}
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.transportCacheLock.Delete(question)
close(cond)
}()
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.cacheLock.Delete(cacheKey)
close(cond)
}()
}
response, ttl := c.loadResponse(question, transport)
response, ttl, isStale := c.loadResponse(question, transport)
if response != nil {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
if isStale && !options.DisableOptimisticCache {
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
response.Id = message.Id
return response, nil
} else if !isStale {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
}
}
}
@@ -190,62 +222,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return nil, ErrResponseRejectedCached
}
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
response, err := transport.Exchange(ctx, message)
cancel()
response, err := c.exchangeToTransport(ctx, transport, message)
if err != nil {
var rcodeError RcodeError
if errors.As(err, &rcodeError) {
response = FixedResponseStatus(message, int(rcodeError))
} else {
return nil, err
}
return nil, err
}
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
validResponse := response
loop:
for {
var (
addresses int
queryCNAME string
)
for _, rawRR := range validResponse.Answer {
switch rr := rawRR.(type) {
case *dns.A:
break loop
case *dns.AAAA:
break loop
case *dns.CNAME:
queryCNAME = rr.Target
}
}
if queryCNAME == "" {
break
}
exMessage := *message
exMessage.Question = []dns.Question{{
Name: queryCNAME,
Qtype: question.Qtype,
}}
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
if err != nil {
return nil, err
}
}
if validResponse != response {
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
} else if len(response.Answer) == 0 {
rejected = !responseChecker(nil)
} else {
rejected = !responseChecker(MessageToAddresses(response))
rejected = !responseChecker(response)
}
if rejected {
if !disableCache && c.rdrc != nil {
@@ -255,48 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
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
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
timeToLive = soaTTL
}
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
}
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
record.Header().Ttl = timeToLive
}
}
timeToLive := applyResponseOptions(question, response, options)
if !disableCache {
c.storeCache(transport, question, response, timeToLive)
}
@@ -315,7 +261,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, nil
}
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
var strategy C.DomainStrategy
@@ -324,16 +270,20 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
} else {
strategy = options.Strategy
}
lookupOptions := options
if options.LookupStrategy != C.DomainStrategyAsIS {
lookupOptions.Strategy = strategy
}
if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
} else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, 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)
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
if err != nil {
return err
}
@@ -341,7 +291,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
return nil
})
group.Append("exchange6", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
if err != nil {
return err
}
@@ -358,8 +308,12 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
func (c *Client) ClearCache() {
if c.cache != nil {
c.cache.Purge()
} else if c.transportCache != nil {
c.transportCache.Purge()
}
if c.dnsCache != nil {
err := c.dnsCache.ClearDNSCache()
if err != nil && c.logger != nil {
c.logger.Warn("clear DNS cache: ", err)
}
}
}
@@ -375,46 +329,44 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
if timeToLive == 0 {
return
}
if c.dnsCache != nil {
packed, err := message.Pack()
if err == nil {
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
}
return
}
if c.cache == nil {
return
}
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
if c.disableExpire {
if !c.independentCache {
c.cache.Add(question, message)
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message)
}
c.cache.Add(key, message.Copy())
} else {
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))
}
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
}
}
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := dns.Question{
Name: name,
Qtype: qType,
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},
}
disableCache := c.disableCache || options.DisableCache
if !disableCache {
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
if err != ErrNotCached {
return cachedAddresses, err
}
}
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
if err != nil {
return nil, err
@@ -425,111 +377,181 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
return MessageToAddresses(response), nil
}
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
response, _ := c.loadResponse(question, transport)
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := message.Question[0]
response, _, isStale := c.loadResponse(question, transport)
if response == nil {
return nil, ErrNotCached
}
if isStale {
if options.DisableOptimisticCache {
return nil, ErrNotCached
}
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
}
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
return MessageToAddresses(response), nil
}
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 (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
if c.dnsCache != nil {
return c.loadPersistentResponse(question, transport)
}
if c.cache == nil {
return nil, 0, false
}
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
if c.disableExpire {
response, loaded := c.cache.Get(key)
if !loaded {
return nil, 0, false
}
return response.Copy(), 0, false
}
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
if !loaded {
return nil, 0, false
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
response = response.Copy()
normalizeTTL(response, 1)
return response, 0, true
}
c.cache.Remove(key)
return nil, 0, false
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
response = response.Copy()
normalizeTTL(response, uint32(nowTTL))
return response, nowTTL, false
}
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
if !loaded {
return nil, 0, false
}
response := new(dns.Msg)
err := response.Unpack(rawMessage)
if err != nil {
return nil, 0, false
}
if c.disableExpire {
return response, 0, false
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
normalizeTTL(response, 1)
return response, 0, true
}
return nil, 0, false
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
normalizeTTL(response, uint32(nowTTL))
return response, nowTTL, false
}
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
for _, rr := range response.Answer {
https, isHTTPS := rr.(*dns.HTTPS)
if !isHTTPS {
continue
}
content := https.SVCB
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
if options.Strategy == C.DomainStrategyIPv4Only {
return it.Key() != dns.SVCB_IPV6HINT
}
return it.Key() != dns.SVCB_IPV4HINT
})
https.SVCB = content
}
}
timeToLive := computeTimeToLive(response)
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
normalizeTTL(response, timeToLive)
return timeToLive
}
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
if loaded {
return
}
go func() {
defer c.backgroundRefresh.Delete(key)
ctx := contextWithTransportTag(c.ctx, transport.Tag())
response, err := c.exchangeToTransport(ctx, transport, message)
if err != nil {
if c.logger != nil {
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
}
return
}
if responseChecker != nil {
var rejected bool
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
} else {
rejected = !responseChecker(response)
}
if rejected {
if c.rdrc != nil {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
return
}
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
return
}
timeToLive := applyResponseOptions(question, response, options)
c.storeCache(transport, question, response, timeToLive)
}()
}
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
return message
}
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
response, err := transport.Exchange(ctx, message)
if err == nil {
return response, nil
}
var rcodeError RcodeError
if errors.As(err, &rcodeError) {
return FixedResponseStatus(message, int(rcodeError)), nil
}
return nil, err
}
func MessageToAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) {
case *dns.A:
addresses = append(addresses, M.AddrFromIP(answer.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
case *dns.HTTPS:
for _, value := range answer.SVCB.Value {
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
}
}
}
}
return addresses
return adapter.DNSResponseAddresses(response)
}
func wrapError(err error) error {

View File

@@ -22,6 +22,19 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
}
}
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
if logger == nil || len(response.Question) == 0 {
return
}
domain := FqdnToDomain(response.Question[0].Name)
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
}
}
}
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
if logger == nil || len(response.Question) == 0 {
return

View File

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

111
dns/repro_test.go Normal file
View File

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

File diff suppressed because it is too large Load Diff

2547
dns/router_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,18 @@ package transport
import (
"bytes"
"context"
"encoding/base64"
"errors"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/httpclient"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
@@ -44,14 +45,20 @@ type HTTPSTransport struct {
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
method string
host string
queryHeaders http.Header
transportAccess sync.Mutex
transport *HTTPSTransportWrapper
transport *httpclient.Client
transportResetAt time.Time
}
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
remoteOptions := option.RemoteDNSServerOptions{
DNSServerAddressOptions: options.DNSServerAddressOptions,
}
remoteOptions.DialerOptions = options.DialerOptions
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
if err != nil {
return nil, err
}
@@ -62,28 +69,21 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, err
}
if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
}
headers := options.Headers.Build()
host := headers.Get("Host")
if host != "" {
headers.Del("Host")
} else {
if tlsConfig.ServerName() != "" {
host = tlsConfig.ServerName()
} else {
host = options.Server
}
serverAddr := options.DNSServerAddressOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
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)))
Host: doHURLHost(serverAddr, 443),
}
path := options.Path
if path == "" {
@@ -93,41 +93,67 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil {
return nil, err
}
serverAddr := options.DNSServerAddressOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
method := strings.ToUpper(options.Method)
if method == "" {
method = http.MethodPost
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
switch method {
case http.MethodGet, http.MethodPost:
default:
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
}
return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
httpClientOptions := options.HTTPClientOptions
return NewHTTPRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
logger,
transportDialer,
&destinationURL,
headers,
serverAddr,
tlsConfig,
), nil
httpClientOptions,
method,
)
}
func NewHTTPSRaw(
func NewHTTPRaw(
adapter dns.TransportAdapter,
logger log.ContextLogger,
logger logger.ContextLogger,
dialer N.Dialer,
destination *url.URL,
headers http.Header,
serverAddr M.Socksaddr,
tlsConfig tls.Config,
) *HTTPSTransport {
httpClientOptions option.HTTPClientOptions,
method string,
) (*HTTPSTransport, error) {
if destination.Scheme == "https" && tlsConfig == nil {
return nil, E.New("TLS transport unavailable")
}
queryHeaders := headers.Clone()
if queryHeaders == nil {
queryHeaders = make(http.Header)
}
host := queryHeaders.Get("Host")
queryHeaders.Del("Host")
queryHeaders.Set("Accept", MimeType)
if method == http.MethodPost {
queryHeaders.Set("Content-Type", MimeType)
}
httpClientOptions.Tag = ""
httpClientOptions.Headers = nil
currentTransport, err := httpclient.NewClientWithDialer(dialer, tlsConfig, "", httpClientOptions)
if err != nil {
return nil, err
}
return &HTTPSTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
destination: destination,
headers: headers,
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
}
method: method,
host: host,
queryHeaders: queryHeaders,
transport: currentTransport,
}, nil
}
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
@@ -181,14 +207,25 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
requestBuffer.Release()
return nil, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
requestURL := *t.destination
var request *http.Request
switch t.method {
case http.MethodGet:
query := requestURL.Query()
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
requestURL.RawQuery = query.Encode()
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
default:
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.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)
request.Header = t.queryHeaders.Clone()
if t.host != "" {
request.Host = t.host
}
t.transportAccess.Lock()
currentTransport := t.transport
t.transportAccess.Unlock()
@@ -222,3 +259,13 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
}
return &responseMessage, nil
}
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
if serverAddr.Port != defaultPort {
return serverAddr.String()
}
if serverAddr.IsIPv6() {
return "[" + serverAddr.AddrString() + "]"
}
return serverAddr.AddrString()
}

View File

@@ -1,80 +0,0 @@
package transport
import (
"context"
"errors"
"net"
"net/http"
"sync/atomic"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/net/http2"
)
var errFallback = E.New("fallback to HTTP/1.1")
type HTTPSTransportWrapper struct {
http2Transport *http2.Transport
httpTransport *http.Transport
fallback *atomic.Bool
}
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
var fallback atomic.Bool
return &HTTPSTransportWrapper{
http2Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
if err != nil {
return nil, err
}
state := tlsConn.ConnectionState()
if state.NegotiatedProtocol == http2.NextProtoTLS {
return tlsConn, nil
}
tlsConn.Close()
fallback.Store(true)
return nil, errFallback
},
},
httpTransport: &http.Transport{
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialTLSContext(ctx, serverAddr)
},
},
fallback: &fallback,
}
}
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
if h.fallback.Load() {
return h.httpTransport.RoundTrip(request)
} else {
response, err := h.http2Transport.RoundTrip(request)
if err != nil {
if errors.Is(err, errFallback) {
return h.httpTransport.RoundTrip(request)
}
return nil, err
}
return response, nil
}
}
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
h.http2Transport.CloseIdleConnections()
h.httpTransport.CloseIdleConnections()
}
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
return &HTTPSTransportWrapper{
httpTransport: h.httpTransport,
http2Transport: &http2.Transport{
DialTLSContext: h.http2Transport.DialTLSContext,
},
fallback: h.fallback,
}
}

View File

@@ -81,10 +81,7 @@ func (t *Transport) Reset() {
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil {
resolverObject := t.resolved.Object()
if resolverObject != nil {
return t.resolved.Exchange(resolverObject, ctx, message)
}
return t.resolved.Exchange(ctx, message)
}
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

View File

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

View File

@@ -0,0 +1,249 @@
//go:build darwin
package local
/*
#include <stdlib.h>
#include <dns.h>
#include <resolv.h>
static void *cgo_dns_open_super() {
return (void *)dns_open(NULL);
}
static void cgo_dns_close(void *opaque) {
if (opaque != NULL) dns_free((dns_handle_t)opaque);
}
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
unsigned char *answer, int anslen) {
dns_handle_t handle = (dns_handle_t)opaque;
struct sockaddr_storage from;
uint32_t fromlen = sizeof(from);
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
}
static void *cgo_res_init() {
res_state state = calloc(1, sizeof(struct __res_state));
if (state == NULL) return NULL;
if (res_ninit(state) != 0) {
free(state);
return NULL;
}
return state;
}
static void cgo_res_destroy(void *opaque) {
res_state state = (res_state)opaque;
res_ndestroy(state);
free(state);
}
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
unsigned char *answer, int anslen,
int timeout_seconds,
int *out_h_errno) {
res_state state = (res_state)opaque;
state->retrans = timeout_seconds;
state->retry = 1;
int n = res_nsearch(state, dname, class, type, answer, anslen);
if (n < 0) {
*out_h_errno = state->res_h_errno;
}
return n;
}
*/
import "C"
import (
"context"
"errors"
"time"
"unsafe"
boxC "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
const (
darwinResolverHostNotFound = 1
darwinResolverTryAgain = 2
darwinResolverNoRecovery = 3
darwinResolverNoData = 4
darwinResolverMaxPacketSize = 65535
)
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
response, err := darwinSearchWithSystemRouting(name, class, qtype)
if err == nil {
return response, nil
}
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
if fallbackErr == nil || fallbackResponse != nil {
return fallbackResponse, fallbackErr
}
return nil, E.Errors(
E.Cause(err, "dns_search"),
E.Cause(fallbackErr, "res_nsearch"),
)
}
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
handle := C.cgo_dns_open_super()
if handle == nil {
return nil, E.New("dns_open failed")
}
defer C.cgo_dns_close(handle)
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
if n <= 0 {
return nil, E.New("dns_search failed for ", name)
}
if int(n) > bufSize {
bufSize = int(n)
continue
}
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
}
}
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
state := C.cgo_res_init()
if state == nil {
return nil, E.New("res_ninit failed")
}
defer C.cgo_res_destroy(state)
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
var hErrno C.int
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
C.int(timeoutSeconds),
&hErrno)
if n >= 0 {
if int(n) > bufSize {
bufSize = int(n)
continue
}
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
}
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
if err == nil {
return response, nil
}
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
bufSize *= 2
if bufSize > darwinResolverMaxPacketSize {
bufSize = darwinResolverMaxPacketSize
}
continue
}
return nil, err
}
}
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
var response mDNS.Msg
err := response.Unpack(packet)
if err != nil {
return nil, E.Cause(err, "unpack ", source, " response")
}
return &response, nil
}
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
if err == nil && response.Response {
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
return nil, errDarwinNeedLargerBuffer
}
return response, nil
}
return nil, darwinResolverHErrno(name, hErrno)
}
func darwinResolverHErrno(name string, hErrno int) error {
switch hErrno {
case darwinResolverHostNotFound:
return dns.RcodeNameError
case darwinResolverTryAgain:
return dns.RcodeServerFailure
case darwinResolverNoRecovery:
return dns.RcodeServerFailure
case darwinResolverNoData:
return dns.RcodeSuccess
default:
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
}
}
if t.fallback && t.dhcpTransport != nil {
dhcpServers := t.dhcpTransport.Fetch()
if len(dhcpServers) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
}
}
name := question.Name
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
remaining := time.Until(deadline)
if remaining <= 0 {
return nil, context.DeadlineExceeded
}
seconds := int(remaining.Seconds())
if seconds < 1 {
seconds = 1
}
timeoutSeconds = seconds
}
type resolvResult struct {
response *mDNS.Msg
err error
}
resultCh := make(chan resolvResult, 1)
go func() {
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
resultCh <- resolvResult{response, err}
}()
var result resolvResult
select {
case <-ctx.Done():
return nil, ctx.Err()
case result = <-resultCh:
}
if result.err != nil {
var rcodeError dns.RcodeError
if errors.As(result.err, &rcodeError) {
return dns.FixedResponseStatus(message, int(rcodeError)), nil
}
return nil, result.err
}
result.response.Id = message.Id
return result.response, nil
}

View File

@@ -9,6 +9,5 @@ import (
type ResolvedResolver interface {
Start() error
Close() error
Object() any
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
}

View File

@@ -4,19 +4,26 @@ import (
"bufio"
"context"
"errors"
"net/netip"
"os"
"strings"
"sync"
"sync/atomic"
"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"
dnsTransport "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
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/x/list"
"github.com/sagernet/sing/service"
@@ -49,13 +56,23 @@ type DBusResolvedResolver struct {
interfaceMonitor tun.DefaultInterfaceMonitor
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
systemBus *dbus.Conn
resoledObject atomic.Pointer[ResolvedObject]
savedServerSet atomic.Pointer[resolvedServerSet]
closeOnce sync.Once
}
type ResolvedObject struct {
dbus.BusObject
InterfaceIndex int32
type resolvedServerSet struct {
servers []resolvedServer
}
type resolvedServer struct {
primaryTransport adapter.DNSTransport
fallbackTransport adapter.DNSTransport
}
type resolvedServerSpecification struct {
address netip.Addr
port uint16
serverName string
}
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
@@ -82,17 +99,31 @@ func (t *DBusResolvedResolver) Start() error {
"org.freedesktop.DBus",
"NameOwnerChanged",
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1"),
).Err
if err != nil {
return E.Cause(err, "configure resolved restart listener")
}
err = t.systemBus.BusObject().AddMatchSignal(
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
dbus.WithMatchSender("org.freedesktop.resolve1"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
).Err
if err != nil {
return E.Cause(err, "configure resolved properties listener")
}
go t.loopUpdateStatus()
return nil
}
func (t *DBusResolvedResolver) Close() error {
var closeErr error
t.closeOnce.Do(func() {
serverSet := t.savedServerSet.Swap(nil)
if serverSet != nil {
closeErr = serverSet.Close()
}
if t.interfaceCallback != nil {
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
}
@@ -100,99 +131,97 @@ func (t *DBusResolvedResolver) Close() error {
_ = t.systemBus.Close()
}
})
return nil
return closeErr
}
func (t *DBusResolvedResolver) Object() any {
return common.PtrOrNil(t.resoledObject.Load())
}
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
resolvedObject := object.(*ResolvedObject)
call := resolvedObject.CallWithContext(
ctx,
"org.freedesktop.resolve1.Manager.ResolveRecord",
0,
resolvedObject.InterfaceIndex,
question.Name,
question.Qclass,
question.Qtype,
uint64(0),
)
if call.Err != nil {
var dbusError dbus.Error
if errors.As(call.Err, &dbusError) && dbusError.Name == "org.freedesktop.resolve1.NoNameServers" {
t.updateStatus()
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
serverSet := t.savedServerSet.Load()
if serverSet == nil {
var err error
serverSet, err = t.checkResolved(context.Background())
if err != nil {
return nil, err
}
previousServerSet := t.savedServerSet.Swap(serverSet)
if previousServerSet != nil {
_ = previousServerSet.Close()
}
return nil, E.Cause(call.Err, " resolve record via resolved")
}
var (
records []resolved.ResourceRecord
outflags uint64
)
err := call.Store(&records, &outflags)
if err != nil {
response, err := t.exchangeServerSet(ctx, message, serverSet)
if err == nil {
return response, nil
}
t.updateStatus()
refreshedServerSet := t.savedServerSet.Load()
if refreshedServerSet == nil || refreshedServerSet == serverSet {
return nil, err
}
response := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: mDNS.RcodeSuccess,
},
Question: []mDNS.Question{question},
}
for _, record := range records {
var rr mDNS.RR
rr, _, err = mDNS.UnpackRR(record.Data, 0)
if err != nil {
return nil, E.Cause(err, "unpack resource record")
}
response.Answer = append(response.Answer, rr)
}
return response, nil
return t.exchangeServerSet(ctx, message, refreshedServerSet)
}
func (t *DBusResolvedResolver) loopUpdateStatus() {
signalChan := make(chan *dbus.Signal, 1)
t.systemBus.Signal(signalChan)
for signal := range signalChan {
var restarted bool
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
switch signal.Name {
case "org.freedesktop.DBus.NameOwnerChanged":
if len(signal.Body) != 3 {
continue
}
newOwner, loaded := signal.Body[2].(string)
if !loaded || newOwner == "" {
continue
}
t.updateStatus()
case "org.freedesktop.DBus.Properties.PropertiesChanged":
if !shouldUpdateResolvedServerSet(signal) {
continue
} else {
restarted = true
}
}
if restarted {
t.updateStatus()
}
}
}
func (t *DBusResolvedResolver) updateStatus() {
dbusObject, err := t.checkResolved(context.Background())
oldValue := t.resoledObject.Swap(dbusObject)
serverSet, err := t.checkResolved(context.Background())
oldServerSet := t.savedServerSet.Swap(serverSet)
if oldServerSet != nil {
_ = oldServerSet.Close()
}
if err != nil {
var dbusErr dbus.Error
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" {
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" {
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
}
if oldValue != nil {
if oldServerSet != nil {
t.logger.Debug("systemd-resolved service is gone")
}
return
} else if oldValue == nil {
} else if oldServerSet == nil {
t.logger.Debug("using systemd-resolved service as resolver")
}
}
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) {
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) {
if serverSet == nil || len(serverSet.servers) == 0 {
return nil, E.New("link has no DNS servers configured")
}
var lastError error
for _, server := range serverSet.servers {
response, err := server.primaryTransport.Exchange(ctx, message)
if err != nil && server.fallbackTransport != nil {
response, err = server.fallbackTransport.Exchange(ctx, message)
}
if err != nil {
lastError = err
continue
}
return response, nil
}
return nil, lastError
}
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
if err != nil {
@@ -220,16 +249,19 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
if linkObject == nil {
return nil, E.New("missing link object for default interface")
}
dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
if err != nil {
return nil, err
}
var linkDNS []resolved.LinkDNS
err = dnsProp.Store(&linkDNS)
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
if err != nil {
return nil, err
}
if len(linkDNS) == 0 {
linkDNS, err := loadResolvedLinkDNS(linkObject)
if err != nil {
return nil, err
}
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
if inbound.Type() == C.TypeTun {
return nil, E.New("No appropriate name servers or networks for name found")
@@ -237,12 +269,233 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
}
return nil, E.New("link has no DNS servers configured")
}
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: defaultInterface.Name,
UDPFragmentDefault: true,
})
if err != nil {
return nil, err
}
var serverSpecifications []resolvedServerSpecification
if len(linkDNSEx) > 0 {
for _, entry := range linkDNSEx {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
} else {
for _, entry := range linkDNS {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
}
if len(serverSpecifications) == 0 {
return nil, E.New("no valid DNS servers on link")
}
serverSet := &resolvedServerSet{
servers: make([]resolvedServer, 0, len(serverSpecifications)),
}
for _, serverSpecification := range serverSpecifications {
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
if createErr != nil {
_ = serverSet.Close()
return nil, createErr
}
serverSet.servers = append(serverSet.servers, server)
}
return serverSet, nil
}
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
if dnsOverTLSMode == "yes" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
}, nil
}
if dnsOverTLSMode == "opportunistic" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
if err != nil {
_ = primaryTransport.Close()
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
fallbackTransport: fallbackTransport,
}, nil
}
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
if err != nil {
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
}, nil
}
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
if useTLS {
tlsAddress := serverSpecification.address
if tlsAddress.Zone() != "" {
tlsAddress = tlsAddress.WithZone("")
}
serverName := serverSpecification.serverName
if serverName == "" {
serverName = tlsAddress.String()
}
tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{
Enabled: true,
ServerName: serverName,
})
if err != nil {
return nil, err
}
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
err = serverTransport.Start(adapter.StartStateStart)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
err := serverTransport.Start(adapter.StartStateStart)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
func (s *resolvedServerSet) Close() error {
var errors []error
for _, server := range s.servers {
errors = append(errors, server.primaryTransport.Close())
if server.fallbackTransport != nil {
errors = append(errors, server.fallbackTransport.Close())
}
}
return E.Errors(errors...)
}
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
address, loaded := netip.AddrFromSlice(rawAddress)
if !loaded {
return resolvedServerSpecification{}, false
}
if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" {
address = address.WithZone(interfaceName)
}
return resolvedServerSpecification{
address: address,
port: port,
serverName: serverName,
}, true
}
func resolvedServerPort(port uint16, useTLS bool) uint16 {
if port > 0 {
return port
}
if useTLS {
return 853
}
return 53
}
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
}
var linkDNS []resolved.LinkDNS
err = dnsProperty.Store(&linkDNS)
if err != nil {
return nil, err
}
return linkDNS, nil
}
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
}
var linkDNSEx []resolved.LinkDNSEx
err = dnsProperty.Store(&linkDNSEx)
if err != nil {
return nil, err
}
return linkDNSEx, nil
}
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return "", nil
}
return "", err
}
var dnsOverTLSMode string
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
if err != nil {
return "", err
}
return dnsOverTLSMode, nil
}
func isResolvedUnknownPropertyError(err error) bool {
var dbusError dbus.Error
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
}
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
if len(signal.Body) != 3 {
return true
}
changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant)
if !loaded {
return true
}
for propertyName := range changedProperties {
switch propertyName {
case "DNS", "DNSEx", "DNSOverTLS":
return true
}
}
invalidatedProperties, loaded := signal.Body[2].([]string)
if !loaded {
return true
}
for _, propertyName := range invalidatedProperties {
switch propertyName {
case "DNS", "DNSEx", "DNSOverTLS":
return true
}
}
return false
}
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {
t.updateStatus()
}

View File

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

View File

@@ -9,6 +9,7 @@ import (
"net/url"
"strconv"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
@@ -40,18 +41,23 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
type HTTP3Transport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
serverAddr M.Socksaddr
tlsConfig *tls.STDConfig
transportAccess sync.Mutex
transport *http3.Transport
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
handshakeTimeout time.Duration
serverAddr M.Socksaddr
tlsConfig *tls.STDConfig
transportAccess sync.Mutex
transport *http3.Transport
}
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
remoteOptions := option.RemoteDNSServerOptions{
DNSServerAddressOptions: options.DNSServerAddressOptions,
}
remoteOptions.DialerOptions = options.DialerOptions
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
if err != nil {
return nil, err
}
@@ -61,6 +67,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil {
return nil, err
}
handshakeTimeout := tlsConfig.HandshakeTimeout()
stdConfig, err := tlsConfig.STDConfig()
if err != nil {
return nil, err
@@ -102,11 +109,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, E.New("invalid server address: ", serverAddr)
}
t := &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
logger: logger,
dialer: transportDialer,
destination: &destinationURL,
headers: headers,
handshakeTimeout: handshakeTimeout,
serverAddr: serverAddr,
tlsConfig: stdConfig,
}
@@ -115,8 +123,17 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
}
func (t *HTTP3Transport) newTransport() *http3.Transport {
quicConfig := &quic.Config{}
if t.handshakeTimeout > 0 {
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
}
return &http3.Transport{
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
cfg = cfg.Clone()
cfg.HandshakeIdleTimeout = t.handshakeTimeout
}
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if dialErr != nil {
return nil, dialErr

View File

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

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