Compare commits

..

183 Commits

Author SHA1 Message Date
世界
00bb5a292d documentation: Bump version 2025-05-20 18:27:07 +08:00
世界
4f425e13aa Add SSM API service 2025-05-20 18:24:52 +08:00
世界
bdc008c8ce Add resolved service and DNS server 2025-05-20 18:24:51 +08:00
世界
cd262b9893 Add DERP service 2025-05-20 18:24:51 +08:00
世界
534856d2ff Add service component type 2025-05-20 18:24:51 +08:00
世界
2aaa72ae61 Fix tproxy tcp control 2025-05-20 18:24:51 +08:00
愚者
41766cfab4 release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-05-20 18:24:51 +08:00
世界
940d313e4c prevent creation of bind and mark controls on unsupported platforms 2025-05-20 18:24:50 +08:00
PuerNya
6ae911a704 documentation: Fix description of reject DNS action behavior 2025-05-20 18:24:50 +08:00
Restia-Ashbell
2e9cfd8198 Fix TLS record fragment 2025-05-20 18:24:50 +08:00
世界
28f29b8b01 Add missing accept_routes option for Tailscale 2025-05-20 18:24:50 +08:00
世界
1cca8e4e01 Add TLS record fragment support 2025-05-20 18:24:49 +08:00
世界
6900de4093 release: Update Go to 1.24.3 2025-05-20 18:24:49 +08:00
世界
90e2ac8ee6 Fix set edns0 client subnet 2025-05-20 18:24:49 +08:00
世界
7ff28ce451 Update minor dependencies 2025-05-20 18:24:49 +08:00
世界
d581b295aa Update certmagic and providers 2025-05-20 18:24:49 +08:00
世界
9d8efbcb7a Update protobuf and grpc 2025-05-20 18:24:48 +08:00
世界
25ec957778 Add control options for listeners 2025-05-20 18:24:48 +08:00
世界
45c1ea7871 Update quic-go to v0.51.0 2025-05-20 18:24:48 +08:00
世界
edb4415169 Update utls to v1.7.2 2025-05-20 18:24:48 +08:00
世界
c9734355f4 Handle EDNS version downgrade 2025-05-20 18:24:48 +08:00
世界
d0a62267a0 documentation: Fix anytls padding scheme description 2025-05-20 18:24:48 +08:00
安容
ecbcbe2544 Report invalid DNS address early 2025-05-20 18:24:47 +08:00
世界
e8b9b2a9df Fix wireguard listen_port 2025-05-20 18:24:47 +08:00
世界
298f5e66ce clash-api: Add more meta api 2025-05-20 18:24:47 +08:00
世界
07df84b56b Fix DNS lookup 2025-05-20 18:24:47 +08:00
世界
4abbaea260 Fix fetch ECH configs 2025-05-20 18:24:47 +08:00
reletor
550cb8d60c documentation: Minor fixes 2025-05-20 18:24:47 +08:00
caelansar
1a8adc22c2 Fix callback deletion in UDP transport 2025-05-20 18:24:46 +08:00
世界
c964c426ac documentation: Try to make the play review happy 2025-05-20 18:24:46 +08:00
世界
485798f57f Fix missing handling of legacy domain_strategy options 2025-05-20 18:24:45 +08:00
世界
001d83acc3 Improve local DNS server 2025-05-20 18:24:45 +08:00
anytls
b516c514e2 Update anytls
Co-authored-by: anytls <anytls>
2025-05-20 18:24:45 +08:00
世界
0c2bcb9acb Fix DNS dialer 2025-05-20 18:24:45 +08:00
世界
404077cd59 release: Skip override version for iOS 2025-05-20 18:24:44 +08:00
iikira
8988b5221d Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-05-20 18:24:44 +08:00
ReleTor
d189bc4ed1 Fix fetch ECH configs 2025-05-20 18:24:44 +08:00
世界
bbbaca9b73 Allow direct outbounds without domain_resolver 2025-05-20 18:24:44 +08:00
世界
b17c50734c Fix Tailscale dialer 2025-05-20 18:24:43 +08:00
dyhkwong
5c817a2a50 Fix DNS over QUIC stream close 2025-05-20 18:24:43 +08:00
anytls
9cb11cdd7b Update anytls
Co-authored-by: anytls <anytls>
2025-05-20 18:24:43 +08:00
Rambling2076
cd5c236433 Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-05-20 18:24:43 +08:00
世界
d1cc0e4486 Fail when default DNS server not found 2025-05-20 18:24:42 +08:00
世界
0cf631a1e1 Update gVisor to 20250319.0 2025-05-20 18:24:42 +08:00
世界
6836e7236a Explicitly reject detour to empty direct outbounds 2025-05-20 18:24:42 +08:00
世界
e14659298a Add netns support 2025-05-20 18:24:42 +08:00
世界
f2e49a5ecc Add wildcard name support for predefined records 2025-05-20 18:24:41 +08:00
世界
809c3f8020 Remove map usage in options 2025-05-20 18:24:41 +08:00
世界
717cfe5dfe Fix unhandled DNS loop 2025-05-20 18:24:41 +08:00
世界
b2c0a808d5 Add wildcard-sni support for shadow-tls inbound 2025-05-20 18:24:40 +08:00
k9982874
7a5e14ce77 Add ntp protocol sniffing 2025-05-20 18:24:40 +08:00
世界
ec97b71f09 option: Fix marshal legacy DNS options 2025-05-20 18:24:40 +08:00
世界
523d0c01f8 Make domain_resolver optional when only one DNS server is configured 2025-05-20 18:24:40 +08:00
世界
94a4b70e79 Fix DNS lookup context pollution 2025-05-20 18:24:39 +08:00
世界
6617b1d1e7 Fix http3 DNS server connecting to wrong address 2025-05-20 18:24:39 +08:00
Restia-Ashbell
8d98e30756 documentation: Fix typo 2025-05-20 18:24:39 +08:00
anytls
face2dc73e Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-20 18:24:38 +08:00
k9982874
c4cc6b1fba Fix hosts DNS server 2025-05-20 18:24:38 +08:00
世界
2940beea12 Fix UDP DNS server crash 2025-05-20 18:24:38 +08:00
世界
ba02559b90 documentation: Fix missing ip_accept_any DNS rule option 2025-05-20 18:24:38 +08:00
世界
f5d295f894 Fix anytls dialer usage 2025-05-20 18:24:37 +08:00
世界
fd2732a2f1 Move predefined DNS server to rule action 2025-05-20 18:24:37 +08:00
世界
52e949c7ca Fix domain resolver on direct outbound 2025-05-20 18:24:37 +08:00
Zephyruso
6810701c46 Fix missing AnyTLS display name 2025-05-20 18:24:36 +08:00
anytls
2bbc9e5c5b Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-20 18:24:36 +08:00
Estel
396de4306d documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-05-20 18:24:36 +08:00
TargetLocked
b88c2956c6 Fix parsing legacy DNS options 2025-05-20 18:24:36 +08:00
世界
ca21c899d0 Fix DNS fallback 2025-05-20 18:24:35 +08:00
世界
5c06c6cc56 documentation: Fix missing hosts DNS server 2025-05-20 18:24:35 +08:00
anytls
4808efea68 Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-05-20 18:24:35 +08:00
ReleTor
036b95f32e documentation: Minor fixes 2025-05-20 18:24:35 +08:00
libtry486
7f1cb1220d documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-05-20 18:24:34 +08:00
Alireza Ahmadi
7160086b38 Fix Outbound deadlock 2025-05-20 18:24:34 +08:00
世界
f2f2a1691c documentation: Fix AnyTLS doc 2025-05-20 18:24:34 +08:00
anytls
0ebed65bc2 Add AnyTLS protocol 2025-05-20 18:24:33 +08:00
世界
9375e7b19a Migrate to stdlib ECH support 2025-05-20 18:24:33 +08:00
世界
1133b16b42 Add fallback local DNS server for iOS 2025-05-20 18:24:32 +08:00
世界
fa38bc85f7 Get darwin local DNS server from libresolv 2025-05-20 18:24:32 +08:00
世界
0b77d97157 Improve resolve action 2025-05-20 18:24:32 +08:00
世界
4edd316902 Add back port hopping to hysteria 1 2025-05-20 18:24:32 +08:00
xchacha20-poly1305
1a9dedde82 Remove single quotes of raw Moziila certs 2025-05-20 18:24:32 +08:00
世界
1a5b618cdb Add Tailscale endpoint 2025-05-20 18:24:31 +08:00
世界
15b21206ab Build legacy binaries with latest Go 2025-05-20 18:24:31 +08:00
世界
7e60b4b060 documentation: Remove outdated icons 2025-05-20 18:24:30 +08:00
世界
c691bea8f3 documentation: Certificate store 2025-05-20 18:24:30 +08:00
世界
f9c0e1a8c5 documentation: TLS fragment 2025-05-20 18:24:30 +08:00
世界
a6e9e1c98d documentation: Outbound domain resolver 2025-05-20 18:24:30 +08:00
世界
045c076c89 documentation: Refactor DNS 2025-05-20 18:24:29 +08:00
世界
364865dd62 Add certificate store 2025-05-20 18:24:29 +08:00
世界
313957336e Add TLS fragment support 2025-05-20 18:24:29 +08:00
世界
18d60ba288 refactor: Outbound domain resolver 2025-05-20 18:24:29 +08:00
世界
db29bcabde refactor: DNS 2025-05-20 18:24:29 +08:00
世界
82b096185f Fix update route address set 2025-05-20 18:17:30 +08:00
世界
e2bfbdd389 Fix interface finder 2025-05-20 16:12:15 +08:00
世界
c5ecca3938 Bump version 2025-05-18 16:48:44 +08:00
世界
900888731c Fix DNS reject response 2025-05-13 18:05:31 +08:00
世界
13e648e4b1 Fix set edns0 subnet 2025-05-07 15:12:17 +08:00
世界
aff12ff671 Bump version 2025-05-05 09:37:47 +08:00
世界
101fb88255 Fix allocator put 2025-05-05 09:37:44 +08:00
世界
8b489354e4 Undeprecate the block outbound 2025-05-04 18:45:53 +08:00
世界
7dea6eb7a6 Fix missing read waiter for cancelers 2025-05-04 18:14:21 +08:00
世界
af1bfe4e3e Make rule_set.format optional 2025-05-04 18:14:21 +08:00
世界
d574e9eb52 Update smux to v1.5.34 2025-04-30 19:39:15 +08:00
世界
2d7df1e1f2 Fix hysteria bytes format 2025-04-29 20:45:19 +08:00
世界
1c0ffcf5b1 Fix counter position in auto redirect dnat rules 2025-04-28 11:20:23 +08:00
世界
348cc39975 Bump version 2025-04-27 21:33:31 +08:00
世界
987899f94a Fix usages of wireguard listener 2025-04-27 21:29:23 +08:00
世界
d8b2d5142f Fix panic on some stupid input 2025-04-25 16:03:58 +08:00
世界
134802d1ee Fix ssh outbound 2025-04-25 16:03:57 +08:00
世界
e5e81b4de1 Fix wireguard listening 2025-04-25 16:03:57 +08:00
世界
300c961efa option: Fix listable again and again 2025-04-25 16:03:57 +08:00
世界
7c7f512405 option: Fix omitempty reject method 2025-04-25 16:03:57 +08:00
世界
03e8d029c2 release: Fix apt-get install 2025-04-25 16:03:57 +08:00
世界
787b5f1931 Fix set wireguard reserved on Linux 2025-04-25 16:03:57 +08:00
世界
56a7624618 Fix vmess working with zero uuids 2025-04-25 16:03:57 +08:00
世界
3a84acf122 Fix hysteria1 server panic 2025-04-25 16:03:57 +08:00
世界
f600e02e47 Fix DNS crash 2025-04-25 16:03:57 +08:00
世界
e6d19de58a Fix overriding address 2025-04-22 14:55:44 +08:00
dyhkwong
f2bbf6b2aa Fix sniffer errors override each others
* Fix sniffer errors override each others

* Do not return ErrNeedMoreData if header is not expected
2025-04-22 14:44:55 +08:00
dyhkwong
c54d50fd36 Fix websocket detour
Signed-off-by: trimgop <20010323+trimgop@users.noreply.github.com>
Co-authored-by: trimgop <20010323+trimgop@users.noreply.github.com>
2025-04-22 14:44:34 +08:00
世界
6a051054db release: Fix packages 2025-04-19 19:12:01 +08:00
世界
49498f6439 Bump version 2025-04-18 08:54:40 +08:00
世界
144a890c71 release: Add openwrt packages 2025-04-18 08:54:40 +08:00
世界
afb4993445 Fix urltest outbound 2025-04-18 08:54:40 +08:00
世界
4c9455b944 Fix wireguard endpoint 2025-04-18 08:54:40 +08:00
世界
5fdc051a08 Fix override_port in direct inbound 2025-04-16 17:04:13 +08:00
世界
cb68a40c43 documentation: Update actual behaviors of auto_redirect and strict_route 2025-04-12 13:06:16 +08:00
纳西妲 · Nahida
023218e6e7 Fix build will fail when use space to split each tag 2025-04-12 13:06:16 +08:00
世界
2a24b94b8d Minor fixes 2025-04-12 13:06:15 +08:00
世界
c6531cf184 Fix NTP service 2025-04-12 13:06:15 +08:00
世界
d4fa0ed349 Improve auto redirect 2025-04-12 13:06:10 +08:00
世界
10874d2dc4 Bump version 2025-04-08 14:34:09 +08:00
Fei1Yang
5adaf1ac75 Mark config file as noreplace for rpm 2025-04-08 14:21:08 +08:00
世界
9668ea69b8 Fix windows process searcher 2025-04-08 14:16:27 +08:00
testing
ae9bc7acf1 documentation: Fix typo
Signed-off-by: testing <58134720+testing765@users.noreply.github.com>
2025-04-08 14:16:23 +08:00
世界
594ee480a2 option: Fix listable 2025-04-08 14:16:23 +08:00
世界
a15b5a2463 Fix no_drop not work 2025-04-08 14:16:23 +08:00
Mahdi
991e755789 Fix conn copy 2025-04-08 14:16:22 +08:00
世界
97d41ffde8 Improve pause management 2025-04-08 14:16:22 +08:00
世界
24af0766ac Fix uTP sniffer 2025-04-08 14:16:22 +08:00
世界
af17eaa537 Improve sniffer 2025-04-08 14:16:22 +08:00
世界
3adc10a797 Fix hysteria2 close 2025-04-08 14:16:22 +08:00
xchacha20-poly1305
5eeef6b28e Fix multiple trackers 2025-04-08 14:16:22 +08:00
世界
f4c29840c3 Fix DNS sniffer 2025-03-31 20:45:04 +08:00
世界
47fc3ebda4 Add duplicate tag check 2025-03-29 23:10:22 +08:00
世界
9774a659b0 Fix DoQ / truncate DNS message 2025-03-29 17:41:22 +08:00
世界
2e4a6de4e7 release: Fix read tag 2025-03-27 20:30:57 +08:00
世界
a530e424e9 Bump version 2025-03-27 18:17:39 +08:00
世界
0bfd487ee9 Fix udpnat2 handler again 2025-03-27 18:17:39 +08:00
世界
6aae834493 release: Fix workflow 2025-03-27 18:17:39 +08:00
世界
f56131f38e Make linter happy 2025-03-24 20:38:42 +08:00
世界
273a11d550 Fix crash on udpnat2 handler 2025-03-24 18:14:32 +08:00
世界
ae8ce75e41 Fix websocket crash 2025-03-24 17:44:14 +08:00
世界
d6d94b689f release: Replace goreleaser build with scripts 2025-03-24 13:48:37 +08:00
世界
30d785f1ee release: Use fake goreleaser key 2025-03-21 22:25:51 +08:00
世界
db5ec3cdfc Fix connectionCopyEarly 2025-03-21 10:51:16 +08:00
世界
9aca54d039 Fix socks5 UDP 2025-03-16 14:46:44 +08:00
世界
d55d5009c2 Fix processing multiple sniffs 2025-03-16 09:21:54 +08:00
世界
4f3ee61104 Fix copy early conn 2025-03-15 08:09:04 +08:00
世界
96eb98c00a Fix httpupgrade crash 2025-03-14 17:17:28 +08:00
世界
68ce9577c6 Fix context in v2ray http transports 2025-03-14 17:07:17 +08:00
世界
3ae036e997 Downgrade goreleaser to stable since nfpm fixed 2025-03-13 18:53:19 +08:00
世界
5da2d1d470 release: Fix goreleaser version 2025-03-12 16:15:50 +08:00
世界
8e2baf40f1 Bump version 2025-03-11 20:18:34 +08:00
世界
c24c40dfee platform: Fix android start 2025-03-11 20:18:34 +08:00
世界
32e52ce1ed Fix udp nat for fakeip 2025-03-11 19:09:27 +08:00
世界
ed46438359 release: Use nightly goreleaser to fix rpm bug 2025-03-11 13:29:08 +08:00
世界
0b5490d5a3 Fix resolve domain for WireGuard 2025-03-11 12:02:25 +08:00
Tal Rasha
2d73ef511d Fix grpclite memory leak
Co-authored-by: talrasha007 <talrasha007@gmail.om>
2025-03-10 14:48:02 +08:00
Mahdi
63e6c85f6f Fix shadowsocks UoT 2025-03-10 14:47:59 +08:00
世界
8946a6d2d0 release: Use latest goreleaser 2025-03-09 15:27:04 +08:00
世界
d3132645fb documentation: Fix description of the UoT protocol 2025-03-09 15:26:42 +08:00
世界
373f158fe0 Fix download external ui with query params 2025-03-09 15:26:36 +08:00
世界
ce36835fab Fix override destination 2025-03-09 15:25:06 +08:00
世界
619fa671d7 Skip binding to the default interface as it will fail on some Android devices 2025-02-26 07:25:35 +08:00
世界
eb07c7a79e Bump version 2025-02-24 07:27:55 +08:00
Gavin Luo
7eb3535094 release: Fix systemd permissions 2025-02-24 07:27:55 +08:00
世界
93b68312cf platform: Add update WIFI state func 2025-02-23 08:35:30 +08:00
世界
97ce666e43 Fix http.FileServer short write 2025-02-23 08:35:30 +08:00
世界
4000e1e66d release: Fix update android version 2025-02-23 08:35:30 +08:00
世界
270740e859 Fix crash on route address set update 2025-02-23 08:35:30 +08:00
世界
6cad142cfe Bump Go to go1.24 2025-02-23 08:35:30 +08:00
世界
093013687c Fix sniff QUIC hidden in three or more packets 2025-02-18 18:14:59 +08:00
305 changed files with 9639 additions and 3680 deletions

30
.fpm_openwrt Normal file
View File

@@ -0,0 +1,30 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--no-deb-generate-changes
--config-files /etc/config/sing-box
--config-files /etc/sing-box/config.json
--depends ca-bundle
--depends kmod-inet-diag
--depends kmod-tun
--depends firewall4
--before-remove release/config/openwrt.prerm
release/config/config.json=/etc/sing-box/config.json
release/config/openwrt.conf=/etc/config/sing-box
release/config/openwrt.init=/etc/init.d/sing-box
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE

24
.fpm_systemd Normal file
View File

@@ -0,0 +1,24 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes
--config-files /etc/sing-box/config.json
release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE

28
.github/deb2ipk.sh vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
set -e -o pipefail
PROJECT=$(dirname "$0")/../..
TMP_PATH=`mktemp -d`
cp $2 $TMP_PATH
pushd $TMP_PATH
DEB_NAME=`ls *.deb`
ar x $DEB_NAME
mkdir control
pushd control
tar xf ../control.tar.gz
rm md5sums
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
cat control
tar czf ../control.tar.gz ./*
popd
DEB_NAME=${DEB_NAME%.deb}
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
popd
cp $TMP_PATH/$DEB_NAME.ipk $3
rm -r $TMP_PATH

View File

@@ -1,10 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.23.6" VERSION="1.23.6"
mkdir -p $HOME/go
cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go $HOME/go/go_legacy mv go go_legacy
cd $HOME/go/go_legacy cd go_legacy
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x # this patch file only works on golang1.23.x

View File

@@ -46,16 +46,16 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
echo "version=${{ inputs.version }}" echo "version=${{ inputs.version }}"
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV" echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
- name: Calculate version - name: Calculate version
if: github.event_name != 'workflow_dispatch' if: github.event_name != 'workflow_dispatch'
run: |- run: |-
go run -v ./cmd/internal/read_tag --nightly go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs - name: Set outputs
id: outputs id: outputs
run: |- run: |-
@@ -69,72 +69,48 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- name: linux_386 - { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
goos: linux - { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
goarch: 386 - { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- name: linux_amd64 - { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
goos: linux - { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
goarch: amd64 - { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
- name: linux_arm64 - { os: linux, arch: arm, 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" }
goos: linux - { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
goarch: arm64 - { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- name: linux_arm - { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
goos: linux - { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
goarch: arm - { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
goarm: 6 - { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- name: linux_arm_v7 - { os: linux, arch: s390x, debian: s390x, rpm: s390x }
goos: linux - { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
goarch: arm - { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
goarm: 7 - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- name: linux_s390x
goos: linux - { os: windows, arch: amd64 }
goarch: s390x - { os: windows, arch: amd64, legacy_go: true }
- name: linux_riscv64 - { os: windows, arch: "386" }
goos: linux - { os: windows, arch: "386", legacy_go: true }
goarch: riscv64 - { os: windows, arch: arm64 }
- name: linux_mips64le
goos: linux - { os: darwin, arch: amd64 }
goarch: mips64le - { os: darwin, arch: arm64 }
- name: windows_amd64
goos: windows - { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
goarch: amd64 - { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
require_legacy_go: true - { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
- name: windows_386 - { os: android, arch: "386", ndk: "i686-linux-android21" }
goos: windows
goarch: 386
require_legacy_go: true
- name: windows_arm64
goos: windows
goarch: arm64
- name: darwin_arm64
goos: darwin
goarch: arm64
- name: darwin_amd64
goos: darwin
goarch: amd64
- name: android_arm64
goos: android
goarch: arm64
- name: android_arm
goos: android
goarch: arm
goarm: 7
- name: android_amd64
goos: android
goarch: amd64
- name: android_386
goos: android
goarch: 386
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: Cache legacy Go - name: Cache Legacy Go
if: matrix.require_legacy_go if: matrix.require_legacy_go
id: cache-legacy-go id: cache-legacy-go
uses: actions/cache@v4 uses: actions/cache@v4
@@ -142,64 +118,167 @@ jobs:
path: | path: |
~/go/go_legacy ~/go/go_legacy
key: go_legacy_1236 key: go_legacy_1236
- name: Setup legacy Go - name: Setup Legacy Go
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true' if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: bash .github/setup_legacy_go.sh run: |-
.github/setup_legacy_go.sh
- name: Setup Legacy Go 2
if: matrix.legacy_go
run: |-
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.goos == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
with: with:
ndk-version: r28 ndk-version: r28
local-cache: true local-cache: true
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: 2.5.1
install-only: true
- name: Extract signing key
run: |-
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }}
EOF
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Set tag - name: Set tag
run: |- run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
if: matrix.goos != 'android' if: matrix.os != 'android'
run: |- run: |
goreleaser release --clean --split set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box
env: env:
GOOS: ${{ matrix.goos }} CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }} GOOS: ${{ matrix.os }}
GOPATH: ${{ env.HOME }}/go GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }} GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Build Android - name: Build Android
if: matrix.goos == 'android' if: matrix.os == 'android'
run: |- run: |
set -xeuo pipefail
go install -v ./cmd/internal/build go install -v ./cmd/internal/build
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split export CC='${{ matrix.ndk }}-clang'
export CXX="${CC}++"
mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box
env: env:
BUILD_GOOS: ${{ matrix.goos }} CGO_ENABLED: "1"
BUILD_GOARCH: ${{ matrix.goarch }} BUILD_GOOS: ${{ matrix.os }}
GOARM: ${{ matrix.goarm }} BUILD_GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - name: Set name
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key run: |-
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
DIR_NAME="${DIR_NAME}-legacy"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
set -xeuo pipefail
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
rm -rf $HOME/.gnupg
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
- name: Package RPM
if: matrix.rpm != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box
cat > $HOME/.rpmmacros <<EOF
%_gpg_name ${{ secrets.GPG_KEY_ID }}
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
EOF
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
rpmsign --addsign dist/*.rpm
- name: Package Pacman
if: matrix.pacman != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y libarchive-tools
cp .fpm_systemd .fpm
fpm -t pacman \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
--architecture ${{ matrix.pacman }} \
dist/sing-box=/usr/bin/sing-box
- name: Package OpenWrt
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_openwrt .fpm
fpm -t deb \
-v "$PKG_VERSION" \
-p "dist/openwrt.deb" \
--architecture all \
dist/sing-box=/usr/bin/sing-box
for architecture in ${{ matrix.openwrt }}; do
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done
rm "dist/openwrt.deb"
- name: Archive
run: |
set -xeuo pipefail
cd dist
mkdir -p "${DIR_NAME}"
cp ../LICENSE "${DIR_NAME}"
if [ '${{ matrix.os }}' = 'windows' ]; then
cp sing-box "${DIR_NAME}/sing-box.exe"
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else
cp sing-box "${DIR_NAME}"
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
fi
rm -r "${DIR_NAME}"
- name: Cleanup
run: rm dist/sing-box
- name: Upload artifact - name: Upload artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-${{ matrix.name }} name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
path: 'dist' path: "dist"
build_android: build_android:
name: Build Android name: Build Android
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android' if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
@@ -215,7 +294,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -271,13 +350,11 @@ jobs:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
- name: Prepare upload - name: Prepare upload
if: github.event_name == 'workflow_dispatch'
run: |- run: |-
mkdir -p dist/release mkdir -p dist
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release cp clients/android/app/build/outputs/apk/play/release/*.apk dist
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
- name: Upload artifact - name: Upload artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-android-apks name: binary-android-apks
@@ -297,7 +374,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -395,7 +472,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: Setup Xcode stable - name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next' if: matrix.if && github.ref == 'refs/heads/main-next'
run: |- run: |-
@@ -435,19 +512,19 @@ jobs:
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles" PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILES_PATH" mkdir -p "$PROFILES_PATH"
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH" unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12 ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
xcrun notarytool store-credentials "notarytool-password" \ xcrun notarytool store-credentials "notarytool-password" \
--key $ASC_KEY_PATH \ --key $ASC_KEY_PATH \
--key-id $ASC_KEY_ID \ --key-id $ASC_KEY_ID \
--issuer $ASC_KEY_ISSUER_ID --issuer $ASC_KEY_ISSUER_ID
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
@@ -472,10 +549,13 @@ jobs:
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version) MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV" echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
- name: Update version
if: matrix.if && matrix.name != 'iOS'
run: |-
go run -v ./cmd/internal/update_apple_version --ci
- name: Build - name: Build
if: matrix.if if: matrix.if
run: |- run: |-
go run -v ./cmd/internal/update_apple_version --ci
cd clients/apple cd clients/apple
xcodebuild archive \ xcodebuild archive \
-scheme "${{ matrix.scheme }}" \ -scheme "${{ matrix.scheme }}" \
@@ -523,10 +603,10 @@ jobs:
cd "${{ matrix.archive }}" cd "${{ matrix.archive }}"
zip -r SFM.dSYMs.zip dSYMs zip -r SFM.dSYMs.zip dSYMs
popd popd
mkdir -p dist/release mkdir -p dist
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg" cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip" cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
- name: Upload image - name: Upload image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -547,12 +627,6 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: 2.5.1
install-only: true
- name: Cache ghr - name: Cache ghr
uses: actions/cache@v4 uses: actions/cache@v4
id: cache-ghr id: cache-ghr
@@ -577,26 +651,17 @@ jobs:
with: with:
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Merge builds
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
run: |-
goreleaser continue --merge --skip publish
mkdir -p dist/release
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- name: Upload builds - name: Upload builds
if: ${{ env.PUBLISHED == 'false' }} if: ${{ env.PUBLISHED == 'false' }}
run: |- run: |-
export PATH="$PATH:$HOME/go/bin" export PATH="$PATH:$HOME/go/bin"
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Replace builds - name: Replace builds
if: ${{ env.PUBLISHED != 'false' }} if: ${{ env.PUBLISHED != 'false' }}
run: |- run: |-
export PATH="$PATH:$HOME/go/bin" export PATH="$PATH:$HOME/go/bin"
ghr --replace -p 5 "v${VERSION}" dist/release ghr --replace -p 5 "v${VERSION}" dist
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -28,10 +28,11 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:
version: latest version: latest
args: --timeout=30m args: --timeout=30m
install-mode: binary install-mode: binary
verify: false

View File

@@ -1,13 +1,22 @@
name: Release to Linux repository name: Build Linux Packages
on: on:
workflow_dispatch:
inputs:
version:
description: "Version name"
required: true
type: string
release: release:
types: types:
- published - published
jobs: jobs:
build: calculate_version:
name: Calculate version
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
@@ -16,23 +25,160 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.3
- name: Extract signing key - name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |- run: |-
mkdir -p $HOME/.gnupg echo "version=${{ inputs.version }}"
cat > $HOME/.gnupg/sagernet.key <<EOF echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs
id: outputs
run: |-
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build binary
runs-on: ubuntu-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: "386", debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24.3
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r28
local-cache: true
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set mtime
run: |-
TZ=UTC touch -t '197001010000' dist/sing-box
- name: Set name
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-')
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
set -xeuo pipefail
sudo gem install fpm
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
--name "${NAME}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
rm -rf $HOME/.gnupg
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }} ${{ secrets.GPG_KEY }}
EOF EOF
echo "HOME=$HOME" >> "$GITHUB_ENV" debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
- name: Publish release - name: Package RPM
uses: goreleaser/goreleaser-action@v6 if: matrix.rpm != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \
--name "${NAME}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box
cat > $HOME/.rpmmacros <<EOF
%_gpg_name ${{ secrets.GPG_KEY_ID }}
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
EOF
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
rpmsign --addsign dist/*.rpm
- name: Cleanup
run: rm dist/sing-box
- name: Upload artifact
uses: actions/upload-artifact@v4
with: with:
distribution: goreleaser-pro name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
version: latest path: "dist"
args: release -f .goreleaser.fury.yaml --clean upload:
env: name: Upload builds
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: ubuntu-latest
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} needs:
FURY_TOKEN: ${{ secrets.FURY_TOKEN }} - calculate_version
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key - build
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Publish packages
run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/

View File

@@ -21,15 +21,13 @@ linters-settings:
- -SA1003 - -SA1003
run: run:
go: "1.24" go: "1.23"
build-tags: build-tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_ech
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api

View File

@@ -14,9 +14,7 @@ builds:
- with_quic - with_quic
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_ech
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
@@ -51,12 +49,18 @@ nfpms:
contents: contents:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: config type: "config|noreplace"
- src: release/config/sing-box.service - src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -16,14 +16,13 @@ builds:
- with_quic - with_quic
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_ech
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- GOTOOLCHAIN=local
targets: targets:
- linux_386 - linux_386
- linux_amd64_v1 - linux_amd64_v1
@@ -47,7 +46,6 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
@@ -62,6 +60,7 @@ builds:
<<: *template <<: *template
env: env:
- CGO_ENABLED=1 - CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides: overrides:
- goos: android - goos: android
goarch: arm goarch: arm
@@ -96,10 +95,12 @@ archives:
builds: builds:
- main - main
- android - android
format: tar.gz formats:
- tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip formats:
- zip
wrap_in_directory: true wrap_in_directory: true
files: files:
- LICENSE - LICENSE
@@ -123,18 +124,24 @@ nfpms:
- deb - deb
- rpm - rpm
- archlinux - archlinux
# - apk # - apk
# - ipk # - ipk
priority: extra priority: extra
contents: contents:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: config type: "config|noreplace"
- src: release/config/sing-box.service - src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -13,7 +13,7 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \ && go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \ "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
-o /go/bin/sing-box \ -o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box

View File

@@ -1,35 +1,30 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_clash_api,with_quic,with_utls,with_tailscale
TAGS_GO121 = with_ech TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls
TAGS_GO123 = with_tailscale
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121),$(TAGS_GO123)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid=" PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS) MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH) PREFIX ?= $(shell go env GOPATH)
.PHONY: test release docs build .PHONY: test release docs build
build: build:
export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
ci_build_go120:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build: ci_build:
go build $(PARAMS) $(MAIN) export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
generate_completions: generate_completions:
go run -v --tags $(TAGS),generate,generate_completions $(MAIN) go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
install: install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
@@ -231,8 +226,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve
@@ -251,4 +246,4 @@ clean:
update: update:
git fetch git fetch
git reset FETCH_HEAD --hard git reset FETCH_HEAD --hard
git clean -fdx git clean -fdx

View File

@@ -7,7 +7,9 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -31,11 +33,30 @@ type DNSClient interface {
} }
type DNSQueryOptions struct { type DNSQueryOptions struct {
Transport DNSTransport Transport DNSTransport
Strategy C.DomainStrategy Strategy C.DomainStrategy
DisableCache bool LookupStrategy C.DomainStrategy
RewriteTTL *uint32 DisableCache bool
ClientSubnet netip.Prefix RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
if options == nil {
return &DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
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{}),
}, nil
} }
type RDRCStore interface { type RDRCStore interface {
@@ -45,10 +66,10 @@ type RDRCStore interface {
} }
type DNSTransport interface { type DNSTransport interface {
Lifecycle
Type() string Type() string
Tag() string Tag() string
Dependencies() []string Dependencies() []string
Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
} }

View File

@@ -7,7 +7,7 @@ import (
) )
type FakeIPStore interface { type FakeIPStore interface {
Service SimpleLifecycle
Contains(address netip.Addr) bool Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error) Create(domain string, isIPv6 bool) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool) Lookup(address netip.Addr) (string, bool)

View File

@@ -53,10 +53,11 @@ type InboundContext struct {
// sniffer // sniffer
Protocol string Protocol string
Domain string Domain string
Client string Client string
SniffContext any SniffContext any
PacketSniffError error
// cache // cache
@@ -73,6 +74,7 @@ type InboundContext struct {
UDPTimeout time.Duration UDPTimeout time.Duration
TLSFragment bool TLSFragment bool
TLSFragmentFallbackDelay time.Duration TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
NetworkStrategy *C.NetworkStrategy NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType NetworkType []C.InterfaceType

View File

@@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
func (m *Manager) Start(stage adapter.StartStage) error { func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage { if m.started && m.stage >= stage {
panic("already started") panic("already started")
} }
m.started = true m.started = true
m.stage = stage m.stage = stage
for _, inbound := range m.inbounds { inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
err := adapter.LegacyStart(inbound, stage) err := adapter.LegacyStart(inbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")

View File

@@ -2,6 +2,11 @@ package adapter
import E "github.com/sagernet/sing/common/exceptions" import E "github.com/sagernet/sing/common/exceptions"
type SimpleLifecycle interface {
Start() error
Close() error
}
type StartStage uint8 type StartStage uint8
const ( const (

View File

@@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
} }
type lifecycleServiceWrapper struct { type lifecycleServiceWrapper struct {
Service SimpleLifecycle
name string name string
} }
func NewLifecycleService(service Service, name string) LifecycleService { func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{ return &lifecycleServiceWrapper{
Service: service, SimpleLifecycle: service,
name: name, name: name,
} }
} }
@@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
} }
func (l *lifecycleServiceWrapper) Start(stage StartStage) error { func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.Service, stage) return LegacyStart(l.SimpleLifecycle, stage)
} }
func (l *lifecycleServiceWrapper) Close() error { func (l *lifecycleServiceWrapper) Close() error {
return l.Service.Close() return l.SimpleLifecycle.Close()
} }

View File

@@ -25,6 +25,7 @@ type NetworkManager interface {
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
WIFIState() WIFIState WIFIState() WIFIState
ResetNetwork() ResetNetwork()
UpdateWIFIState()
} }
type NetworkOptions struct { type NetworkOptions struct {

View File

@@ -5,7 +5,6 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -19,11 +18,6 @@ type Outbound interface {
N.Dialer N.Dialer
} }
type DirectRouteOutbound interface {
Outbound
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext) (tun.DirectRouteDestination, error)
}
type OutboundRegistry interface { type OutboundRegistry interface {
option.OutboundOptionsRegistry option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)

View File

@@ -246,8 +246,6 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
if err != nil { if err != nil {
return err return err
} }
m.access.Lock()
defer m.access.Unlock()
if m.started { if m.started {
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(outbound, stage) err = adapter.LegacyStart(outbound, stage)
@@ -256,6 +254,8 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
} }
} }
} }
m.access.Lock()
defer m.access.Unlock()
if existsOutbound, loaded := m.outboundByTag[tag]; loaded { if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
if m.started { if m.started {
err = common.Close(existsOutbound) err = common.Close(existsOutbound)

View File

@@ -8,7 +8,6 @@ import (
"sync" "sync"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
@@ -20,12 +19,12 @@ import (
type Router interface { type Router interface {
Lifecycle Lifecycle
ConnectionRouter ConnectionRouter
PreMatch(metadata InboundContext, context tun.DirectRouteContext) (tun.DirectRouteDestination, error) PreMatch(metadata InboundContext) error
ConnectionRouterEx ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool) RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool NeedWIFIState() bool
Rules() []Rule Rules() []Rule
SetTracker(tracker ConnectionTracker) AppendTracker(tracker ConnectionTracker)
ResetNetwork() ResetNetwork()
} }

View File

@@ -11,7 +11,7 @@ type HeadlessRule interface {
type Rule interface { type Rule interface {
HeadlessRule HeadlessRule
Service SimpleLifecycle
Type() string Type() string
Action() RuleAction Action() RuleAction
} }

View File

@@ -1,6 +1,27 @@
package adapter package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface { type Service interface {
Start() error Lifecycle
Close() error Type() string
Tag() string
}
type ServiceRegistry interface {
option.ServiceOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
}
type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
} }

View File

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

144
adapter/service/manager.go Normal file
View File

@@ -0,0 +1,144 @@
package service
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
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
services := m.services
m.access.Unlock()
for _, service := range services {
err := adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
return m.services
}
func (m *Manager) Get(tag string) (adapter.Service, bool) {
m.access.Lock()
service, found := m.serviceByTag[tag]
m.access.Unlock()
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
return nil
}

View File

@@ -0,0 +1,72 @@
package service
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.Service, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ServiceRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, 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(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
}

18
adapter/ssm.go Normal file
View File

@@ -0,0 +1,18 @@
package adapter
import (
"net"
N "github.com/sagernet/sing/common/network"
)
type ManagedSSMServer interface {
Inbound
SetTracker(tracker SSMTracker)
UpdateUsers(users []string, uPSKs []string) error
}
type SSMTracker interface {
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
}

View File

@@ -3,6 +3,6 @@ package adapter
import "time" import "time"
type TimeService interface { type TimeService interface {
Service SimpleLifecycle
TimeFunc() func() time.Time TimeFunc() func() time.Time
} }

136
box.go
View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -34,22 +35,23 @@ import (
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
) )
var _ adapter.Service = (*Box)(nil) var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct { type Box struct {
createdAt time.Time createdAt time.Time
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
network *route.NetworkManager network *route.NetworkManager
endpoint *endpoint.Manager endpoint *endpoint.Manager
inbound *inbound.Manager inbound *inbound.Manager
outbound *outbound.Manager outbound *outbound.Manager
dnsTransport *dns.TransportManager service *boxService.Manager
dnsRouter *dns.Router dnsTransport *dns.TransportManager
connection *route.ConnectionManager dnsRouter *dns.Router
router *route.Router connection *route.ConnectionManager
services []adapter.LifecycleService router *route.Router
done chan struct{} internalService []adapter.LifecycleService
done chan struct{}
} }
type Options struct { type Options struct {
@@ -64,6 +66,7 @@ func Context(
outboundRegistry adapter.OutboundRegistry, outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry, endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry, dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
) context.Context { ) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil { service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -84,6 +87,10 @@ func Context(
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
} }
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
return ctx return ctx
} }
@@ -99,6 +106,7 @@ func New(options Options) (*Box, error) {
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
if endpointRegistry == nil { if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context") return nil, E.New("missing endpoint registry in context")
@@ -109,6 +117,12 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil { if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context") return nil, E.New("missing outbound registry in context")
} }
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -142,7 +156,7 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create log factory") return nil, E.Cause(err, "create log factory")
} }
var services []adapter.LifecycleService var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate) certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 || len(certificateOptions.Certificate) > 0 ||
@@ -153,7 +167,7 @@ func New(options Options) (*Box, error) {
return nil, err return nil, err
} }
service.MustRegister[adapter.CertificateStore](ctx, certificateStore) service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore) internalServices = append(internalServices, certificateStore)
} }
routeOptions := common.PtrValueOrDefault(options.Route) routeOptions := common.PtrValueOrDefault(options.Route)
@@ -162,10 +176,12 @@ func New(options Options) (*Box, error) {
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
@@ -216,8 +232,15 @@ func New(options Options) (*Box, error) {
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
endpointCtx := ctx
if tag != "" {
// TODO: remove this
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
Outbound: tag,
})
}
err = endpointManager.Create( err = endpointManager.Create(
ctx, endpointCtx,
router, router,
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
tag, tag,
@@ -273,6 +296,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]") return nil, E.Cause(err, "initialize outbound[", i, "]")
} }
} }
for i, serviceOptions := range options.Services {
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, "]")
}
}
outboundManager.Initialize(common.Must1( outboundManager.Initialize(common.Must1(
direct.NewOutbound( direct.NewOutbound(
ctx, ctx,
@@ -298,7 +339,7 @@ func New(options Options) (*Box, error) {
if needCacheFile { if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile) service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile) internalServices = append(internalServices, cacheFile)
} }
if needClashAPI { if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@@ -307,9 +348,9 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create clash-server") return nil, E.Cause(err, "create clash-server")
} }
router.SetTracker(clashServer) router.AppendTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer)
services = append(services, clashServer) internalServices = append(internalServices, clashServer)
} }
if needV2RayAPI { if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@@ -317,8 +358,8 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create v2ray-server") return nil, E.Cause(err, "create v2ray-server")
} }
if v2rayServer.StatsService() != nil { if v2rayServer.StatsService() != nil {
router.SetTracker(v2rayServer.StatsService()) router.AppendTracker(v2rayServer.StatsService())
services = append(services, v2rayServer) internalServices = append(internalServices, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
} }
} }
@@ -336,22 +377,23 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem, WriteToSystem: ntpOptions.WriteToSystem,
}) })
timeService.TimeService = ntpService timeService.TimeService = ntpService
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service")) internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
} }
return &Box{ return &Box{
network: networkManager, network: networkManager,
endpoint: endpointManager, endpoint: endpointManager,
inbound: inboundManager, inbound: inboundManager,
outbound: outboundManager, outbound: outboundManager,
dnsTransport: dnsTransportManager, dnsTransport: dnsTransportManager,
dnsRouter: dnsRouter, service: serviceManager,
connection: connectionManager, dnsRouter: dnsRouter,
router: router, connection: connectionManager,
createdAt: createdAt, router: router,
logFactory: logFactory, createdAt: createdAt,
logger: logFactory.Logger(), logFactory: logFactory,
services: services, logger: logFactory.Logger(),
done: make(chan struct{}), internalService: internalServices,
done: make(chan struct{}),
}, nil }, nil
} }
@@ -401,11 +443,11 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return E.Cause(err, "start logger") return E.Cause(err, "start logger")
} }
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
@@ -421,31 +463,27 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStateStart, s.services) err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = s.inbound.Start(adapter.StartStateStart) err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.endpoint) err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint) err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStatePostStart, s.services) err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
if err != nil { if err != nil {
return err return err
} }
@@ -462,7 +500,7 @@ func (s *Box) Close() error {
err := common.Close( err := common.Close(
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
) )
for _, lifecycleService := range s.services { for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) return E.Cause(err, "close ", lifecycleService.Name())
}) })

View File

@@ -45,6 +45,7 @@ var (
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
iosTags []string iosTags []string
memcTags []string
debugTags []string debugTags []string
) )
@@ -58,8 +59,9 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_tailscale") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack") iosTags = append(iosTags, "with_dhcp", "with_low_memory")
memcTags = append(memcTags, "with_tailscale")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }
@@ -99,18 +101,19 @@ func buildAndroid() {
"-javapkg=io.nekohasekai", "-javapkg=io.nekohasekai",
"-libname=box", "-libname=box",
} }
if !debugEnabled { if !debugEnabled {
args = append(args, sharedFlags...) args = append(args, sharedFlags...)
} else { } else {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
args = append(args, "-tags") tags := append(sharedTags, memcTags...)
if !debugEnabled { if debugEnabled {
args = append(args, strings.Join(sharedTags, ",")) tags = append(tags, debugTags...)
} else {
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
} }
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox") args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
@@ -148,7 +151,9 @@ func buildApple() {
"-v", "-v",
"-target", bindTarget, "-target", bindTarget,
"-libname=box", "-libname=box",
"-tags-macos=" + strings.Join(memcTags, ","),
} }
if !debugEnabled { if !debugEnabled {
args = append(args, sharedFlags...) args = append(args, sharedFlags...)
} else { } else {
@@ -156,12 +161,11 @@ func buildApple() {
} }
tags := append(sharedTags, iosTags...) tags := append(sharedTags, iosTags...)
args = append(args, "-tags") if debugEnabled {
if !debugEnabled { tags = append(tags, debugTags...)
args = append(args, strings.Join(tags, ","))
} else {
args = append(args, strings.Join(append(tags, debugTags...), ","))
} }
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox") args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)

View File

@@ -5,40 +5,49 @@ import (
"os" "os"
"github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
) )
var nightly bool var (
flagRunInCI bool
flagRunNightly bool
)
func init() { func init() {
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag") flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
} }
func main() { func main() {
flag.Parse() flag.Parse()
if nightly { var (
version, err := build_shared.ReadTagVersionRev() versionStr string
err error
)
if flagRunNightly {
var version badversion.Version
version, err = build_shared.ReadTagVersion()
if err == nil {
versionStr = version.String()
}
} else {
versionStr, err = build_shared.ReadTag()
}
if flagRunInCI {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var versionStr string
if version.PreReleaseIdentifier != "" {
versionStr = version.VersionString() + "-nightly"
} else {
version.Patch++
versionStr = version.VersionString() + "-nightly"
}
err = setGitHubEnv("version", versionStr) err = setGitHubEnv("version", versionStr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
tag, err := build_shared.ReadTag()
if err != nil { if err != nil {
log.Error(err) log.Error(err)
os.Stdout.WriteString("unknown\n") os.Stdout.WriteString("unknown\n")
} else { } else {
os.Stdout.WriteString(tag + "\n") os.Stdout.WriteString(versionStr + "\n")
} }
} }
} }

View File

@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry()) globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
} }

View File

@@ -9,8 +9,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var pqSignatureSchemesEnabled bool
var commandGenerateECHKeyPair = &cobra.Command{ var commandGenerateECHKeyPair = &cobra.Command{
Use: "ech-keypair <plain_server_name>", Use: "ech-keypair <plain_server_name>",
Short: "Generate TLS ECH key pair", Short: "Generate TLS ECH key pair",
@@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
} }
func init() { func init() {
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
commandGenerate.AddCommand(commandGenerateECHKeyPair) commandGenerate.AddCommand(commandGenerateECHKeyPair)
} }
func generateECHKeyPair(serverName string) error { func generateECHKeyPair(serverName string) error {
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled) configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -5,6 +5,7 @@ import (
"context" "context"
"io" "io"
"os" "os"
"path/filepath"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
@@ -56,6 +57,14 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return E.Cause(err, "read rule-set") return E.Cause(err, "read rule-set")
} }
if flagRuleSetMatchFormat == "" {
switch filepath.Ext(sourcePath) {
case ".json":
flagRuleSetMatchFormat = C.RuleSetFormatSource
case ".srs":
flagRuleSetMatchFormat = C.RuleSetFormatBinary
}
}
var ruleSet option.PlainRuleSetCompat var ruleSet option.PlainRuleSetCompat
switch flagRuleSetMatchFormat { switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource: case C.RuleSetFormatSource:

View File

@@ -1,31 +0,0 @@
//go:build go1.21 && !without_badtls && with_ech
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing/common"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.Conn](conn)
if !loaded {
return
}
return true, func() error {
return echReadRecord(tlsConn)
}, func() error {
return echHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
func echReadRecord(c *tls.Conn) error
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
func echHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -7,7 +7,8 @@ import (
_ "unsafe" _ "unsafe"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/utls"
"github.com/metacubex/utls"
) )
func init() { func init() {
@@ -24,8 +25,8 @@ func init() {
}) })
} }
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord //go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage //go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error func utlsHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -34,10 +34,13 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
switch options.Store { switch options.Store {
case C.CertificateStoreSystem, "": case C.CertificateStoreSystem, "":
systemPool = x509.NewCertPool() systemPool = x509.NewCertPool()
platformInterface := service.FromContext[platform.Interface](ctx)
var systemValid bool var systemValid bool
for _, cert := range service.FromContext[platform.Interface](ctx).SystemCertificates() { if platformInterface != nil {
if systemPool.AppendCertsFromPEM([]byte(cert)) { for _, cert := range platformInterface.SystemCertificates() {
systemValid = true if systemPool.AppendCertsFromPEM([]byte(cert)) {
systemValid = true
}
} }
} }
if !systemValid { if !systemValid {

View File

@@ -10,6 +10,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@@ -35,6 +36,7 @@ type DefaultDialer struct {
udpListener net.ListenConfig udpListener net.ListenConfig
udpAddr4 string udpAddr4 string
udpAddr6 string udpAddr6 string
netns string
networkManager adapter.NetworkManager networkManager adapter.NetworkManager
networkStrategy *C.NetworkStrategy networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool defaultNetworkStrategy bool
@@ -64,23 +66,19 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
interfaceFinder = control.NewDefaultInterfaceFinder() interfaceFinder = control.NewDefaultInterfaceFinder()
} }
if options.BindInterface != "" { if options.BindInterface != "" {
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
}
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark > 0 { if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark))) if !C.IsLinux {
listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark))) return nil, E.New("`routing_mark` is only supported on Linux")
}
if networkManager != nil {
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
if autoRedirectOutputMark > 0 {
if options.RoutingMark > 0 {
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
}
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
} }
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
} }
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
if disableDefaultBind || options.TCPFastOpen { if disableDefaultBind || options.TCPFastOpen {
@@ -125,8 +123,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} }
} }
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark)) dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
} }
} }
if options.ReuseAddr { if options.ReuseAddr {
@@ -198,6 +196,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
udpListener: listener, udpListener: listener,
udpAddr4: udpAddr4, udpAddr4: udpAddr4,
udpAddr6: udpAddr6, udpAddr6: udpAddr6,
netns: options.NetNs,
networkManager: networkManager, networkManager: networkManager,
networkStrategy: networkStrategy, networkStrategy: networkStrategy,
defaultNetworkStrategy: defaultNetworkStrategy, defaultNetworkStrategy: defaultNetworkStrategy,
@@ -207,24 +206,44 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}, nil }, nil
} }
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
if networkManager == nil {
return control.RoutingMark(mark)
}
return func(network, address string, conn syscall.RawConn) error {
if networkManager.AutoRedirectOutputMark() != 0 {
if isDefault {
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
} else {
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
}
}
return control.RoutingMark(mark)(network, address, conn)
}
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) { func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() { if !address.IsValid() {
return nil, E.New("invalid address") return nil, E.New("invalid address")
} else if address.IsFqdn() {
return nil, E.New("domain not resolved")
} }
if d.networkStrategy == nil { if d.networkStrategy == nil {
switch N.NetworkName(network) { return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
case N.NetworkUDP: switch N.NetworkName(network) {
if !address.IsIPv6() { case N.NetworkUDP:
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String())) if !address.IsIPv6() {
} else { return d.udpDialer4.DialContext(ctx, network, address.String())
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String())) } else {
return d.udpDialer6.DialContext(ctx, network, address.String())
}
} }
} if !address.IsIPv6() {
if !address.IsIPv6() { return DialSlowContext(&d.dialer4, ctx, network, address)
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address)) } else {
} else { return DialSlowContext(&d.dialer6, ctx, network, address)
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address)) }
} }))
} else { } else {
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
} }
@@ -280,13 +299,15 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if d.networkStrategy == nil { if d.networkStrategy == nil {
if destination.IsIPv6() { return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)) if destination.IsIPv6() {
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)) } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
} else { return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)) } else {
} return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
}
}))
} else { } else {
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
} }
@@ -326,7 +347,17 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
} }
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
return d.udpListener.ListenPacket(context.Background(), network, address) udpListener := d.udpListener
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
for _, wgControlFn := range WgControlFns {
err := wgControlFn(network, address, conn)
if err != nil {
return err
}
}
return nil
})
return udpListener.ListenPacket(context.Background(), network, address)
} }
func trackConn(conn net.Conn, err error) (net.Conn, error) { func trackConn(conn net.Conn, err error) (net.Conn, error) {

View File

@@ -18,6 +18,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, false, E.New("no available network interface") return nil, false, E.New("no available network interface")
} }
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
if fallbackDelay == 0 { if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay fallbackDelay = N.DefaultFallbackDelay
} }
@@ -31,7 +32,9 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
results := make(chan dialResult) // unbuffered results := make(chan dialResult) // unbuffered
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
perNetDialer := dialer perNetDialer := dialer
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) if defaultInterface == nil || iif.Index != defaultInterface.Index {
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
}
conn, err := perNetDialer.DialContext(ctx, network, addr) conn, err := perNetDialer.DialContext(ctx, network, addr)
if err != nil { if err != nil {
select { select {
@@ -89,6 +92,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, false, E.New("no available network interface") return nil, false, E.New("no available network interface")
} }
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
if fallbackDelay == 0 { if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay fallbackDelay = N.DefaultFallbackDelay
} }
@@ -103,7 +107,9 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
results := make(chan dialResult) // unbuffered results := make(chan dialResult) // unbuffered
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
perNetDialer := dialer perNetDialer := dialer
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) if defaultInterface == nil || iif.Index != defaultInterface.Index {
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
}
conn, err := perNetDialer.DialContext(ctx, network, addr) conn, err := perNetDialer.DialContext(ctx, network, addr)
if err != nil { if err != nil {
select { select {
@@ -149,10 +155,13 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, E.New("no available network interface") return nil, E.New("no available network interface")
} }
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
var errors []error var errors []error
for _, primaryInterface := range primaryInterfaces { for _, primaryInterface := range primaryInterfaces {
perNetListener := listener perNetListener := listener
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index)) if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
}
conn, err := perNetListener.ListenPacket(ctx, network, addr) conn, err := perNetListener.ListenPacket(ctx, network, addr)
if err == nil { if err == nil {
return conn, nil return conn, nil
@@ -161,7 +170,9 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
} }
for _, fallbackInterface := range fallbackInterfaces { for _, fallbackInterface := range fallbackInterfaces {
perNetListener := listener perNetListener := listener
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index)) if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
}
conn, err := perNetListener.ListenPacket(ctx, network, addr) conn, err := perNetListener.ListenPacket(ctx, network, addr)
if err == nil { if err == nil {
return conn, nil return conn, nil

View File

@@ -6,26 +6,39 @@ import (
"sync" "sync"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type DirectDialer interface {
IsEmpty() bool
}
type DetourDialer struct { type DetourDialer struct {
outboundManager adapter.OutboundManager outboundManager adapter.OutboundManager
detour string detour string
legacyDNSDialer bool
dialer N.Dialer dialer N.Dialer
initOnce sync.Once initOnce sync.Once
initErr error initErr error
} }
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer { func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
return &DetourDialer{outboundManager: outboundManager, detour: detour} return &DetourDialer{
outboundManager: outboundManager,
detour: detour,
legacyDNSDialer: legacyDNSDialer,
}
} }
func (d *DetourDialer) Start() error { func InitializeDetour(dialer N.Dialer) error {
_, err := d.Dialer() detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
return err if !isDetour {
return nil
}
return common.Error(detourDialer.Dialer())
} }
func (d *DetourDialer) Dialer() (N.Dialer, error) { func (d *DetourDialer) Dialer() (N.Dialer, error) {
@@ -34,11 +47,20 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
} }
func (d *DetourDialer) init() { func (d *DetourDialer) init() {
var loaded bool dialer, loaded := d.outboundManager.Outbound(d.detour)
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
if !loaded { if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour) d.initErr = E.New("outbound detour not found: ", d.detour)
return
} }
if !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")
return
}
}
}
d.dialer = dialer
} }
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

View File

@@ -23,6 +23,8 @@ type Options struct {
DirectResolver bool DirectResolver bool
ResolverOnDetour bool ResolverOnDetour bool
NewDialer bool NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
} }
// TODO: merge with NewWithOptions // TODO: merge with NewWithOptions
@@ -45,14 +47,14 @@ func NewWithOptions(options Options) (N.Dialer, error) {
if outboundManager == nil { if outboundManager == nil {
return nil, E.New("missing outbound manager") return nil, E.New("missing outbound manager")
} }
dialer = NewDetour(outboundManager, dialOptions.Detour) dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
} else { } else {
dialer, err = NewDefault(options.Context, dialOptions) dialer, err = NewDefault(options.Context, dialOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour) { if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
networkManager := service.FromContext[adapter.NetworkManager](options.Context) networkManager := service.FromContext[adapter.NetworkManager](options.Context)
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context) dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
var defaultOptions adapter.NetworkOptions var defaultOptions adapter.NetworkOptions
@@ -81,6 +83,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck //nolint:staticcheck
strategy = C.DomainStrategy(dialOptions.DomainStrategy) strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
} }
server = dialOptions.DomainResolver.Server server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{ dnsQueryOptions = adapter.DNSQueryOptions{
@@ -93,18 +96,32 @@ func NewWithOptions(options Options) (N.Dialer, error) {
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver { } else if options.DirectResolver {
return nil, E.New("missing domain resolver for domain server address") return nil, E.New("missing domain resolver for domain server address")
} else if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else { } else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else {
transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else if !options.DirectOutbound {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
}
if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
} }
dialer = NewResolveDialer( dialer = NewResolveDialer(
options.Context, options.Context,

View File

@@ -44,6 +44,20 @@ type resolveDialer struct {
} }
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer { func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
return &resolveParallelNetworkDialer{
resolveDialer{
transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx),
dialer: dialer,
parallel: parallel,
server: server,
queryOptions: queryOptions,
fallbackDelay: fallbackDelay,
},
parallelDialer,
}
}
return &resolveDialer{ return &resolveDialer{
transport: service.FromContext[adapter.DNSTransportManager](ctx), transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx), router: service.FromContext[adapter.DNSRouter](ctx),
@@ -60,21 +74,6 @@ type resolveParallelNetworkDialer struct {
dialer ParallelInterfaceDialer dialer ParallelInterfaceDialer
} }
func NewResolveParallelInterfaceDialer(ctx context.Context, dialer ParallelInterfaceDialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ParallelInterfaceResolveDialer {
return &resolveParallelNetworkDialer{
resolveDialer{
transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx),
dialer: dialer,
parallel: parallel,
server: server,
queryOptions: queryOptions,
fallbackDelay: fallbackDelay,
},
dialer,
}
}
func (d *resolveDialer) initialize() error { func (d *resolveDialer) initialize() error {
d.initOnce.Do(d.initServer) d.initOnce.Do(d.initServer)
return d.initErr return d.initErr

View File

@@ -1,158 +0,0 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var defaultSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
var memorysSizeTable = map[string]uint64{
"b": Byte,
"kb": KiByte,
"mb": MiByte,
"gb": GiByte,
"tb": TiByte,
"pb": PiByte,
"eb": EiByte,
"": Byte,
"k": KiByte,
"m": MiByte,
"g": GiByte,
"t": TiByte,
"p": PiByte,
"e": EiByte,
}
var (
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
)
func Bytes(s uint64) string {
return humanateBytes(s, 1000, defaultSizes)
}
func MemoryBytes(s uint64) string {
return humanateBytes(s, 1024, defaultSizes)
}
func IBytes(s uint64) string {
return humanateBytes(s, 1024, iSizes)
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
func ParseBytes(s string) (uint64, error) {
return parseBytes0(s, defaultSizeTable)
}
func ParseMemoryBytes(s string) (uint64, error) {
return parseBytes0(s, memorysSizeTable)
}
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := sizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -4,6 +4,8 @@ import (
"context" "context"
"net" "net"
"net/netip" "net/netip"
"runtime"
"strings"
"sync/atomic" "sync/atomic"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -14,6 +16,8 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/vishvananda/netns"
) )
type Listener struct { type Listener struct {
@@ -28,6 +32,7 @@ type Listener struct {
disablePacketOutput bool disablePacketOutput bool
setSystemProxy bool setSystemProxy bool
systemProxySOCKS bool systemProxySOCKS bool
tproxy bool
tcpListener net.Listener tcpListener net.Listener
systemProxy settings.SystemProxy systemProxy settings.SystemProxy
@@ -50,6 +55,7 @@ type Options struct {
DisablePacketOutput bool DisablePacketOutput bool
SetSystemProxy bool SetSystemProxy bool
SystemProxySOCKS bool SystemProxySOCKS bool
TProxy bool
} }
func New( func New(
@@ -67,6 +73,7 @@ func New(
disablePacketOutput: options.DisablePacketOutput, disablePacketOutput: options.DisablePacketOutput,
setSystemProxy: options.SetSystemProxy, setSystemProxy: options.SetSystemProxy,
systemProxySOCKS: options.SystemProxySOCKS, systemProxySOCKS: options.SystemProxySOCKS,
tproxy: options.TProxy,
} }
} }
@@ -135,3 +142,30 @@ func (l *Listener) UDPConn() *net.UDPConn {
func (l *Listener) ListenOptions() option.ListenOptions { func (l *Listener) ListenOptions() option.ListenOptions {
return l.listenOptions return l.listenOptions
} }
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
if nameOrPath != "" {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
currentNs, err := netns.Get()
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get current netns")
}
defer netns.Set(currentNs)
var targetNs netns.NsHandle
if strings.HasPrefix(nameOrPath, "/") {
targetNs, err = netns.GetFromPath(nameOrPath)
} else {
targetNs, err = netns.GetFromName(nameOrPath)
}
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
}
defer targetNs.Close()
err = netns.Set(targetNs)
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
}
}
return block()
}

View File

@@ -3,23 +3,39 @@ package listener
import ( import (
"net" "net"
"net/netip" "net/netip"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/metacubex/tfo-go" "github.com/metacubex/tfo-go"
) )
func (l *Listener) ListenTCP() (net.Listener, error) { func (l *Listener) ListenTCP() (net.Listener, error) {
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
var err error var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var tcpListener net.Listener
var listenConfig net.ListenConfig var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
if l.listenOptions.TCPKeepAlive >= 0 { if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
@@ -37,20 +53,26 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
} }
setMultiPathTCP(&listenConfig) setMultiPathTCP(&listenConfig)
} }
if l.listenOptions.TCPFastOpen { if l.tproxy {
var tfoConfig tfo.ListenConfig listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
tfoConfig.ListenConfig = listenConfig return control.Raw(conn, func(fd uintptr) error {
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) return redir.TProxy(fd, M.ParseSocksaddr(address).IsIPv6(), false)
} else { })
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) })
} }
if err == nil { tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
l.logger.Info("tcp server started at ", tcpListener.Addr()) if l.listenOptions.TCPFastOpen {
} var tfoConfig tfo.ListenConfig
//nolint:staticcheck tfoConfig.ListenConfig = listenConfig
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader { return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") } else {
return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
}
})
if err != nil {
return nil, err
} }
l.logger.Info("tcp server started at ", tcpListener.Addr())
l.tcpListener = tcpListener l.tcpListener = tcpListener
return tcpListener, err return tcpListener, err
} }

View File

@@ -1,20 +1,34 @@
package listener package listener
import ( import (
"context"
"net" "net"
"net/netip" "net/netip"
"os" "os"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
) )
func (l *Listener) ListenUDP() (net.PacketConn, error) { func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var lc net.ListenConfig var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
var udpFragment bool var udpFragment bool
if l.listenOptions.UDPFragment != nil { if l.listenOptions.UDPFragment != nil {
udpFragment = *l.listenOptions.UDPFragment udpFragment = *l.listenOptions.UDPFragment
@@ -22,9 +36,18 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
udpFragment = l.listenOptions.UDPFragmentDefault udpFragment = l.listenOptions.UDPFragmentDefault
} }
if !udpFragment { if !udpFragment {
lc.Control = control.Append(lc.Control, control.DisableUDPFragment()) listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
} }
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, M.ParseSocksaddr(address).IsIPv6(), true)
})
})
}
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -34,6 +57,36 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
return udpConn, err return udpConn, err
} }
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
if l.listenOptions.BindInterface != "" {
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
dialer.Control = control.Append(dialer.Control, control.ReuseAddr())
}
return dialer.DialContext(ctx, network, address)
})
}
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
return listenConfig.ListenPacket(ctx, network, address)
})
}
func (l *Listener) UDPAddr() M.Socksaddr { func (l *Listener) UDPAddr() M.Socksaddr {
return l.udpAddr return l.udpAddr
} }

View File

@@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func TProxy(fd uintptr, isIPv6 bool) error { func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
if err == nil { if err == nil {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
@@ -20,11 +20,13 @@ func TProxy(fd uintptr, isIPv6 bool) error {
if err == nil && isIPv6 { if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
} }
if err == nil { if isUDP {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) if err == nil {
} err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1)
if err == nil && isIPv6 { }
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1)
}
} }
return err return err
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
) )
func TProxy(fd uintptr, isIPv6 bool) error { func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
return os.ErrInvalid return os.ErrInvalid
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
) )
const ( const (
@@ -23,21 +24,26 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
var first byte var first byte
err := binary.Read(reader, binary.BigEndian, &first) err := binary.Read(reader, binary.BigEndian, &first)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if first != 19 { if first != 19 {
return os.ErrInvalid return os.ErrInvalid
} }
const header = "BitTorrent protocol"
var protocol [19]byte var protocol [19]byte
_, err = reader.Read(protocol[:]) var n int
if err != nil { n, err = reader.Read(protocol[:])
return err if string(protocol[:n]) != header[:n] {
}
if string(protocol[:]) != "BitTorrent protocol" {
return os.ErrInvalid return os.ErrInvalid
} }
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
if n < 19 {
return ErrNeedMoreData
}
metadata.Protocol = C.ProtocolBitTorrent metadata.Protocol = C.ProtocolBitTorrent
return nil return nil
@@ -67,7 +73,9 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
if err != nil { if err != nil {
return err return err
} }
if extension > 0x04 {
return os.ErrInvalid
}
var length byte var length byte
err = binary.Read(reader, binary.BigEndian, &length) err = binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {

View File

@@ -32,6 +32,27 @@ func TestSniffBittorrent(t *testing.T) {
} }
} }
func TestSniffIncompleteBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e74")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e75")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffUTP(t *testing.T) { func TestSniffUTP(t *testing.T) {
t.Parallel() t.Parallel()
@@ -71,3 +92,19 @@ func TestSniffUDPTracker(t *testing.T) {
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
} }
} }
func TestSniffNotUTP(t *testing.T) {
t.Parallel()
packets := []string{
"0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UTP(context.TODO(), &metadata, pkt)
require.Error(t, err)
}
}

View File

@@ -5,14 +5,11 @@ import (
"encoding/binary" "encoding/binary"
"io" "io"
"os" "os"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/task"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
@@ -21,35 +18,40 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
var length uint16 var length uint16
err := binary.Read(reader, binary.BigEndian, &length) err := binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return os.ErrInvalid return E.Cause1(ErrNeedMoreData, err)
} }
if length == 0 { if length < 12 {
return os.ErrInvalid return os.ErrInvalid
} }
buffer := buf.NewSize(int(length)) buffer := buf.NewSize(int(length))
defer buffer.Release() defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) var n int
var readTask task.Group n, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
readTask.Append0(func(ctx context.Context) error { packet := buffer.Bytes()
return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen())) if n > 2 && packet[2]&0x80 != 0 { // QR
}) return os.ErrInvalid
err = readTask.Run(readCtx)
cancel()
if err != nil {
return err
} }
return DomainNameQuery(readCtx, metadata, buffer.Bytes()) if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT
return os.ErrInvalid
}
for i := 6; i < 10; i++ {
// ANCOUNT, NSCOUNT
if n > i && packet[i] != 0 {
return os.ErrInvalid
}
}
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
return DomainNameQuery(readCtx, metadata, packet)
} }
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg var msg mDNS.Msg
err := msg.Unpack(packet) err := msg.Unpack(packet)
if err != nil { if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
return err return err
} }
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDNS metadata.Protocol = C.ProtocolDNS
return nil return nil
} }

53
common/sniff/dns_test.go Normal file
View File

@@ -0,0 +1,53 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffIncompleteStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@@ -3,10 +3,12 @@ package sniff
import ( import (
std_bufio "bufio" std_bufio "bufio"
"context" "context"
"errors"
"io" "io"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/http"
) )
@@ -14,7 +16,11 @@ import (
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
request, err := http.ReadRequest(std_bufio.NewReader(reader)) request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil { if err != nil {
return err if errors.Is(err, io.ErrUnexpectedEOF) {
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
} }
metadata.Protocol = C.ProtocolHTTP metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString() metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()

58
common/sniff/ntp.go Normal file
View File

@@ -0,0 +1,58 @@
package sniff
import (
"context"
"encoding/binary"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
// NTP packets must be at least 48 bytes long (standard NTP header size).
pLen := len(packet)
if pLen < 48 {
return os.ErrInvalid
}
// Check the LI (Leap Indicator) and Version Number (VN) in the first byte.
// We'll primarily focus on ensuring the version is valid for NTP.
// Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations)
firstByte := packet[0]
li := (firstByte >> 6) & 0x03 // Extract LI
vn := (firstByte >> 3) & 0x07 // Extract VN
mode := firstByte & 0x07 // Extract Mode
// Leap Indicator should be a valid value (0-3).
if li > 3 {
return os.ErrInvalid
}
// Version Check (common NTP versions are 3 and 4)
if vn != 3 && vn != 4 {
return os.ErrInvalid
}
// Check the Mode field for a client request (Mode 3). This validates it *is* a request.
if mode != 3 {
return os.ErrInvalid
}
// Check Root Delay and Root Dispersion. While not strictly *required* for a request,
// we can check if they appear to be reasonable values (not excessively large).
rootDelay := binary.BigEndian.Uint32(packet[4:8])
rootDispersion := binary.BigEndian.Uint32(packet[8:12])
// Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds.
// Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds.
if float64(rootDelay)/65536.0 > 16.0 {
return os.ErrInvalid
}
if float64(rootDispersion)/65536.0 > 16.0 {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolNTP
return nil
}

33
common/sniff/ntp_test.go Normal file
View File

@@ -0,0 +1,33 @@
package sniff_test
import (
"context"
"encoding/hex"
"os"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffNTP(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.NTP(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolNTP)
}
func TestSniffNTPFailed(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.NTP(context.Background(), &metadata, packet)
require.ErrorIs(t, err, os.ErrInvalid)
}

View File

@@ -20,8 +20,6 @@ import (
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet) reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte() typeByte, err := reader.ReadByte()
@@ -308,7 +306,7 @@ find:
metadata.Protocol = C.ProtocolQUIC metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium metadata.Client = C.ClientChromium
metadata.SniffContext = fragments metadata.SniffContext = fragments
return ErrClientHelloFragmented return E.Cause1(ErrNeedMoreData, err)
} }
metadata.Domain = fingerprint.ServerName metadata.Domain = fingerprint.ServerName
for metadata.Client == "" { for metadata.Client == "" {

View File

@@ -12,6 +12,26 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestSniffQUICChromeNew(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, "www.google.com", metadata.Domain)
}
func TestSniffQUICChromium(t *testing.T) { func TestSniffQUICChromium(t *testing.T) {
t.Parallel() t.Parallel()
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620") pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
@@ -20,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) {
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium) require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented) require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err) require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)

View File

@@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
) )
@@ -15,7 +16,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktVersion uint8 var tpktVersion uint8
err := binary.Read(reader, binary.BigEndian, &tpktVersion) err := binary.Read(reader, binary.BigEndian, &tpktVersion)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if tpktVersion != 0x03 { if tpktVersion != 0x03 {
return os.ErrInvalid return os.ErrInvalid
@@ -24,7 +25,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktReserved uint8 var tpktReserved uint8
err = binary.Read(reader, binary.BigEndian, &tpktReserved) err = binary.Read(reader, binary.BigEndian, &tpktReserved)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if tpktReserved != 0x00 { if tpktReserved != 0x00 {
return os.ErrInvalid return os.ErrInvalid
@@ -33,7 +34,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktLength uint16 var tpktLength uint16
err = binary.Read(reader, binary.BigEndian, &tpktLength) err = binary.Read(reader, binary.BigEndian, &tpktLength)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if tpktLength != 19 { if tpktLength != 19 {
@@ -43,7 +44,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpLength uint8 var cotpLength uint8
err = binary.Read(reader, binary.BigEndian, &cotpLength) err = binary.Read(reader, binary.BigEndian, &cotpLength)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if cotpLength != 14 { if cotpLength != 14 {
@@ -53,7 +54,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpTpduType uint8 var cotpTpduType uint8
err = binary.Read(reader, binary.BigEndian, &cotpTpduType) err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if cotpTpduType != 0xE0 { if cotpTpduType != 0xE0 {
return os.ErrInvalid return os.ErrInvalid
@@ -61,13 +62,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
err = rw.SkipN(reader, 5) err = rw.SkipN(reader, 5)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
var rdpType uint8 var rdpType uint8
err = binary.Read(reader, binary.BigEndian, &rdpType) err = binary.Read(reader, binary.BigEndian, &rdpType)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if rdpType != 0x01 { if rdpType != 0x01 {
return os.ErrInvalid return os.ErrInvalid
@@ -75,12 +76,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var rdpFlags uint8 var rdpFlags uint8
err = binary.Read(reader, binary.BigEndian, &rdpFlags) err = binary.Read(reader, binary.BigEndian, &rdpFlags)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
var rdpLength uint8 var rdpLength uint8
err = binary.Read(reader, binary.BigEndian, &rdpLength) err = binary.Read(reader, binary.BigEndian, &rdpLength)
if err != nil { if err != nil {
return err return E.Cause1(ErrNeedMoreData, err)
} }
if rdpLength != 8 { if rdpLength != 8 {
return os.ErrInvalid return os.ErrInvalid

View File

@@ -3,12 +3,14 @@ package sniff
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"io" "io"
"net" "net"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
@@ -18,6 +20,8 @@ type (
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
) )
var ErrNeedMoreData = E.New("need more data")
func Skip(metadata *adapter.InboundContext) bool { func Skip(metadata *adapter.InboundContext) bool {
// skip server first protocols // skip server first protocols
switch metadata.Destination.Port { switch metadata.Destination.Port {
@@ -34,12 +38,12 @@ func Skip(metadata *adapter.InboundContext) bool {
return false return false
} }
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error { func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
if timeout == 0 { if timeout == 0 {
timeout = C.ReadPayloadTimeout timeout = C.ReadPayloadTimeout
} }
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
var errors []error var sniffError error
for i := 0; ; i++ { for i := 0; ; i++ {
err := conn.SetReadDeadline(deadline) err := conn.SetReadDeadline(deadline)
if err != nil { if err != nil {
@@ -53,26 +57,32 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
} }
return E.Cause(err, "read payload") return E.Cause(err, "read payload")
} }
errors = nil sniffError = nil
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes())) reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
return bytes.NewReader(it.Bytes())
})...)
err = sniffer(ctx, metadata, reader)
if err == nil { if err == nil {
return nil return nil
} }
errors = append(errors, err) sniffError = E.Errors(sniffError, err)
}
if !errors.Is(sniffError, ErrNeedMoreData) {
break
} }
} }
return E.Errors(errors...) return sniffError
} }
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
var errors []error var sniffError []error
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
err := sniffer(ctx, metadata, packet) err := sniffer(ctx, metadata, packet)
if err == nil { if err == nil {
return nil return nil
} }
errors = append(errors, err) sniffError = append(sniffError, err)
} }
return E.Errors(errors...) return E.Errors(sniffError...)
} }

View File

@@ -5,22 +5,27 @@ import (
"context" "context"
"io" "io"
"os" "os"
"strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
) )
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
scanner := bufio.NewScanner(reader) const sshPrefix = "SSH-2.0-"
if !scanner.Scan() { bReader := bufio.NewReader(reader)
prefix, err := bReader.Peek(len(sshPrefix))
if string(prefix[:]) != sshPrefix[:len(prefix)] {
return os.ErrInvalid return os.ErrInvalid
} }
fistLine := scanner.Text() if err != nil {
if !strings.HasPrefix(fistLine, "SSH-2.0-") { return E.Cause1(ErrNeedMoreData, err)
return os.ErrInvalid }
fistLine, _, err := bReader.ReadLine()
if err != nil {
return err
} }
metadata.Protocol = C.ProtocolSSH metadata.Protocol = C.ProtocolSSH
metadata.Client = fistLine[8:] metadata.Client = string(fistLine)[8:]
return nil return nil
} }

View File

@@ -24,3 +24,24 @@ func TestSniffSSH(t *testing.T) {
require.Equal(t, C.ProtocolSSH, metadata.Protocol) require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client) require.Equal(t, "dropbear", metadata.Client)
} }
func TestSniffIncompleteSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e30")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e31")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@@ -3,11 +3,13 @@ package sniff
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"io" "io"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
) )
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
@@ -23,5 +25,9 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
metadata.Domain = clientHello.ServerName metadata.Domain = clientHello.ServerName
return nil return nil
} }
return err if errors.Is(err, io.ErrUnexpectedEOF) {
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
} }

View File

@@ -37,7 +37,7 @@ func (w *acmeWrapper) Close() error {
return nil return nil
} }
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) { func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string var acmeServer string
switch options.Provider { switch options.Provider {
case "", "letsencrypt": case "", "letsencrypt":

View File

@@ -11,6 +11,6 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) { func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
} }

View File

@@ -29,15 +29,12 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
if !options.Enabled { if !options.Enabled {
return nil, nil return nil, nil
} }
if options.ECH != nil && options.ECH.Enabled { if options.Reality != nil && options.Reality.Enabled {
return NewECHClient(ctx, serverAddress, options)
} else if options.Reality != nil && options.Reality.Enabled {
return NewRealityClient(ctx, serverAddress, options) return NewRealityClient(ctx, serverAddress, options)
} else if options.UTLS != nil && options.UTLS.Enabled { } else if options.UTLS != nil && options.UTLS.Enabled {
return NewUTLSClient(ctx, serverAddress, options) return NewUTLSClient(ctx, serverAddress, options)
} else {
return NewSTDClient(ctx, serverAddress, options)
} }
return NewSTDClient(ctx, serverAddress, options)
} }
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {

View File

@@ -18,6 +18,7 @@ type (
STDConfig = tls.Config STDConfig = tls.Config
STDConn = tls.Conn STDConn = tls.Conn
ConnectionState = tls.ConnectionState ConnectionState = tls.ConnectionState
CurveID = tls.CurveID
) )
func ParseTLSVersion(version string) (uint16, error) { func ParseTLSVersion(version string) (uint16, error) {

194
common/tls/ech.go Normal file
View File

@@ -0,0 +1,194 @@
//go:build go1.24
package tls
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/pem"
"net"
"os"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
"golang.org/x/crypto/cryptobyte"
)
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
var echConfig []byte
if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
} else if options.ECH.ConfigPath != "" {
content, err := os.ReadFile(options.ECH.ConfigPath)
if err != nil {
return nil, E.Cause(err, "read ECH config")
}
echConfig = content
}
//nolint:staticcheck
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
}
if len(echConfig) > 0 {
block, rest := pem.Decode(echConfig)
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem")
}
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
return &STDClientConfig{tlsConfig}, nil
} else {
return &STDECHClientConfig{
STDClientConfig: STDClientConfig{tlsConfig},
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
}, nil
}
}
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
var echKey []byte
if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil {
return E.Cause(err, "read ECH keys")
}
echKey = content
*echKeyPath = options.ECH.KeyPath
} else {
return E.New("missing ECH keys")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
//nolint:staticcheck
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
}
return nil
}
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
echKey, err := os.ReadFile(echKeyPath)
if err != nil {
return E.Cause(err, "reload ECH keys from ", echKeyPath)
}
block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
}
type STDECHClientConfig struct {
STDClientConfig
access sync.Mutex
dnsRouter adapter.DNSRouter
lastTTL time.Duration
lastUpdate time.Time
}
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := s.fetchAndHandshake(ctx, conn)
if err != nil {
return nil, err
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
return tlsConn, nil
}
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock()
defer s.access.Unlock()
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: mDNS.Fqdn(s.config.ServerName),
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
},
}
response, err := s.dnsRouter.Exchange(ctx, message, adapter.DNSQueryOptions{})
if err != nil {
return nil, E.Cause(err, "fetch ECH config list")
}
if response.Rcode != mDNS.RcodeSuccess {
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
}
match:
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
for _, value := range resource.Value {
if value.Key().String() == "ech" {
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
if err != nil {
return nil, E.Cause(err, "decode ECH config")
}
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
s.lastUpdate = time.Now()
s.config.EncryptedClientHelloConfigList = echConfigList
break match
}
}
}
}
if len(s.config.EncryptedClientHelloConfigList) == 0 {
return nil, E.New("no ECH config found in DNS records")
}
}
return s.Client(conn)
}
func (s *STDECHClientConfig) Clone() Config {
return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
}
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
var keys []tls.EncryptedClientHelloKey
rawString := cryptobyte.String(raw)
for !rawString.Empty() {
var key tls.EncryptedClientHelloKey
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, E.New("error parsing private key")
}
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, E.New("error parsing config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, E.New("empty ECH keys")
}
return keys, nil
}

View File

@@ -1,244 +0,0 @@
//go:build with_ech
package tls
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"net"
"net/netip"
"os"
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
type echClientConfig struct {
config *cftls.Config
}
func (c *echClientConfig) ServerName() string {
return c.config.ServerName
}
func (c *echClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (c *echClientConfig) NextProtos() []string {
return c.config.NextProtos
}
func (c *echClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *echClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
}
func (c *echClientConfig) Clone() Config {
return &echClientConfig{
config: c.config.Clone(),
}
}
type echConnWrapper struct {
*cftls.Conn
}
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
state := c.Conn.ConnectionState()
//nolint:staticcheck
return tls.ConnectionState{
Version: state.Version,
HandshakeComplete: state.HandshakeComplete,
DidResume: state.DidResume,
CipherSuite: state.CipherSuite,
NegotiatedProtocol: state.NegotiatedProtocol,
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
ServerName: state.ServerName,
PeerCertificates: state.PeerCertificates,
VerifiedChains: state.VerifiedChains,
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
OCSPResponse: state.OCSPResponse,
TLSUnique: state.TLSUnique,
}
}
func (c *echConnWrapper) Upstream() any {
return c.Conn
}
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
if _, err := netip.ParseAddr(serverName); err != nil {
serverName = serverAddress
}
}
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
}
var tlsConfig cftls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName
}
if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI {
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state cftls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = options.ALPN
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range cftls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if len(certificate) > 0 {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(certificate) {
return nil, E.New("failed to parse certificate:\n\n", certificate)
}
tlsConfig.RootCAs = certPool
}
// ECH Config
tlsConfig.ECHEnabled = true
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
var echConfig []byte
if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
} else if options.ECH.ConfigPath != "" {
content, err := os.ReadFile(options.ECH.ConfigPath)
if err != nil {
return nil, E.Cause(err, "read ECH config")
}
echConfig = content
}
if len(echConfig) > 0 {
block, rest := pem.Decode(echConfig)
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem")
}
echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
if err != nil {
return nil, E.Cause(err, "parse ECH configs")
}
tlsConfig.ClientECHConfigs = echConfigs
} else {
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
}
return &echClientConfig{&tlsConfig}, nil
}
func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: serverName + ".",
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
},
}
response, err := service.FromContext[adapter.DNSRouter](ctx).Exchange(ctx, message, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
if response.Rcode != mDNS.RcodeSuccess {
return nil, dns.RCodeError(response.Rcode)
}
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
for _, value := range resource.Value {
if value.Key().String() == "ech" {
echConfig, err := base64.StdEncoding.DecodeString(value.String())
if err != nil {
return nil, E.Cause(err, "decode ECH config")
}
return cftls.UnmarshalECHConfigs(echConfig)
}
}
default:
return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
}
}
return nil, E.New("no ECH config found")
}
}

View File

@@ -1,5 +1,3 @@
//go:build with_ech
package tls package tls
import ( import (
@@ -7,14 +5,13 @@ import (
"encoding/binary" "encoding/binary"
"encoding/pem" "encoding/pem"
cftls "github.com/sagernet/cloudflare-tls"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke" "github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem"
) )
func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) { func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{ cipherSuites := []echCipherSuite{
{ {
kdf: hpke.KDF_HKDF_SHA256, kdf: hpke.KDF_HKDF_SHA256,
@@ -24,13 +21,9 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
aead: hpke.AEAD_ChaCha20Poly1305, aead: hpke.AEAD_ChaCha20Poly1305,
}, },
} }
keyConfig := []myECHKeyConfig{ keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256}, {id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
} }
if pqSignatureSchemesEnabled {
keyConfig = append(keyConfig, myECHKeyConfig{id: 1, kem: hpke.KEM_X25519_KYBER768_DRAFT00})
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites) keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
if err != nil { if err != nil {
@@ -59,7 +52,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
type echKeyConfigPair struct { type echKeyConfigPair struct {
id uint8 id uint8
key cftls.EXP_ECHKey
rawKey []byte rawKey []byte
conf myECHKeyConfig conf myECHKeyConfig
rawConf []byte rawConf []byte
@@ -155,15 +147,6 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
sk = append(sk, secBuf...) sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b))) sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...) sk = append(sk, b...)
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
if err != nil {
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
}
if len(cfECHKeys) != 1 {
return nil, E.New("bug: unexpected server key count")
}
pair.key = cfECHKeys[0]
pair.rawKey = sk pair.rawKey = sk
pairs = append(pairs, pair) pairs = append(pairs, pair)

View File

@@ -1,55 +0,0 @@
//go:build with_quic && with_ech
package tls
import (
"context"
"net"
"net/http"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/quic-go/ech"
"github.com/sagernet/quic-go/http3_ech"
"github.com/sagernet/sing-quic"
M "github.com/sagernet/sing/common/metadata"
)
var (
_ qtls.Config = (*echClientConfig)(nil)
_ qtls.ServerConfig = (*echServerConfig)(nil)
)
func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
return quic.Dial(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
return quic.DialEarly(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
return &http3.Transport{
TLSClientConfig: c.config,
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
if err != nil {
return nil, err
}
*quicConnPtr = quicConn
return quicConn, nil
},
}
}
func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.Listener, error) {
return quic.Listen(conn, c.config, config)
}
func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.EarlyListener, error) {
return quic.ListenEarly(conn, c.config, config)
}
func (c *echServerConfig) ConfigureHTTP3() {
http3.ConfigureTLSConfig(c.config)
}

View File

@@ -1,278 +0,0 @@
//go:build with_ech
package tls
import (
"context"
"crypto/tls"
"encoding/pem"
"net"
"os"
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
)
type echServerConfig struct {
config *cftls.Config
logger log.Logger
certificate []byte
key []byte
certificatePath string
keyPath string
echKeyPath string
watcher *fswatch.Watcher
}
func (c *echServerConfig) ServerName() string {
return c.config.ServerName
}
func (c *echServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (c *echServerConfig) NextProtos() []string {
return c.config.NextProtos
}
func (c *echServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *echServerConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (c *echServerConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
}
func (c *echServerConfig) Server(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Server(conn, c.config)}, nil
}
func (c *echServerConfig) Clone() Config {
return &echServerConfig{
config: c.config.Clone(),
}
}
func (c *echServerConfig) Start() error {
err := c.startWatcher()
if err != nil {
c.logger.Warn("create credentials watcher: ", err)
}
return nil
}
func (c *echServerConfig) startWatcher() error {
var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials"))
}
},
})
if err != nil {
return err
}
err = watcher.Start()
if err != nil {
return err
}
c.watcher = watcher
return nil
}
func (c *echServerConfig) credentialsUpdated(path string) error {
if path == c.certificatePath || path == c.keyPath {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return err
}
c.certificate = certificate
} else {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return err
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "parse key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
}
return nil
}
func (c *echServerConfig) Close() error {
var err error
if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close credentials watcher")
})
}
return err
}
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
var tlsConfig cftls.Config
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in ech")
}
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range tls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
var key []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if len(options.Key) > 0 {
key = []byte(strings.Join(options.Key, "\n"))
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.KeyPath)
if err != nil {
return nil, E.Cause(err, "read key")
}
key = content
}
if certificate == nil {
return nil, E.New("missing certificate")
} else if key == nil {
return nil, E.New("missing key")
}
keyPair, err := cftls.X509KeyPair(certificate, key)
if err != nil {
return nil, E.Cause(err, "parse x509 key pair")
}
tlsConfig.Certificates = []cftls.Certificate{keyPair}
var echKey []byte
if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil {
return nil, E.Cause(err, "read ECH key")
}
echKey = content
} else {
return nil, E.New("missing ECH key")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return nil, E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return nil, E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return nil, E.Cause(err, "create ECH key set")
}
tlsConfig.ECHEnabled = true
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
tlsConfig.ServerECHProvider = echKeySet
return &echServerConfig{
config: &tlsConfig,
logger: logger,
certificate: certificate,
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
echKeyPath: options.ECH.KeyPath,
}, nil
}

View File

@@ -1,25 +1,23 @@
//go:build !with_ech //go:build !go1.24
package tls package tls
import ( import (
"context" "context"
"crypto/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
var errECHNotIncluded = E.New(`ECH is not included in this build, rebuild with -tags with_ech`) func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
return nil, E.New("ECH requires go1.24, please recompile your binary.")
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, errECHNotIncluded
} }
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
return nil, errECHNotIncluded return E.New("ECH requires go1.24, please recompile your binary.")
} }
func ECHKeygenDefault(host string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) { func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return "", "", errECHNotIncluded return E.New("ECH requires go1.24, please recompile your binary.")
} }

View File

@@ -0,0 +1,5 @@
//go:build with_ech
package tls
var _ int = "Due to the migration to stdlib, the separate `with_ech` build tag has been deprecated and is no longer needed, please update your build configuration."

View File

@@ -12,6 +12,9 @@ import (
) )
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) { func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
if timeFunc == nil {
timeFunc = time.Now
}
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour)) privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -24,9 +27,6 @@ func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() ti
} }
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) { func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
if timeFunc == nil {
timeFunc = time.Now
}
key, err := rsa.GenerateKey(rand.Reader, 2048) key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {
return return

View File

@@ -29,12 +29,13 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug" "github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
utls "github.com/sagernet/utls"
utls "github.com/metacubex/utls"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@@ -114,6 +115,22 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, extension := range uConn.Extensions {
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool {
return curveID != utls.X25519MLKEM768
})
}
if ks, ok := extension.(*utls.KeyShareExtension); ok {
ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool {
return share.Group != utls.X25519MLKEM768
})
}
}
err = uConn.BuildHandshakeState()
if err != nil {
return nil, err
}
if len(uConfig.NextProtos) > 0 { if len(uConfig.NextProtos) > 0 {
for _, extension := range uConn.Extensions { for _, extension := range uConn.Extensions {
@@ -148,9 +165,13 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
if err != nil { if err != nil {
return nil, err return nil, err
} }
ecdheKey := uConn.HandshakeState.State13.EcdheKey keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys
if keyShareKeys == nil {
return nil, E.New("nil KeyShareKeys")
}
ecdheKey := keyShareKeys.Ecdhe
if ecdheKey == nil { if ecdheKey == nil {
return nil, E.New("nil ecdhe_key") return nil, E.New("nil ecdheKey")
} }
authKey, err := ecdheKey.ECDH(publicKey) authKey, err := ecdheKey.ECDH(publicKey)
if err != nil { if err != nil {
@@ -214,10 +235,6 @@ func realityClientFallback(ctx context.Context, uConn net.Conn, serverName strin
response.Body.Close() response.Body.Close()
} }
func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
e.uClient.config.SessionIDGenerator = generator
}
func (e *RealityClientConfig) Clone() Config { func (e *RealityClientConfig) Clone() Config {
return &RealityClientConfig{ return &RealityClientConfig{
e.ctx, e.ctx,

View File

@@ -1,4 +1,4 @@
//go:build with_reality_server //go:build with_utls
package tls package tls
@@ -7,28 +7,29 @@ import (
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt"
"net" "net"
"time" "time"
"github.com/sagernet/reality"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
utls "github.com/metacubex/utls"
) )
var _ ServerConfigCompat = (*RealityServerConfig)(nil) var _ ServerConfigCompat = (*RealityServerConfig)(nil)
type RealityServerConfig struct { type RealityServerConfig struct {
config *reality.Config config *utls.RealityConfig
} }
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) { func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
var tlsConfig reality.Config var tlsConfig utls.RealityConfig
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality") return nil, E.New("acme is unavailable in reality")
@@ -74,6 +75,11 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
} }
tlsConfig.SessionTicketsDisabled = true tlsConfig.SessionTicketsDisabled = true
tlsConfig.Log = func(format string, v ...any) {
if logger != nil {
logger.Trace(fmt.Sprintf(format, v...))
}
}
tlsConfig.Type = N.NetworkTCP tlsConfig.Type = N.NetworkTCP
tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String() tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String()
@@ -89,16 +95,20 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference) tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
tlsConfig.ShortIds = make(map[[8]byte]bool) tlsConfig.ShortIds = make(map[[8]byte]bool)
for i, shortIDString := range options.Reality.ShortID { if len(options.Reality.ShortID) == 0 {
var shortID [8]byte tlsConfig.ShortIds[[8]byte{0}] = true
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString)) } else {
if err != nil { for i, shortIDString := range options.Reality.ShortID {
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString) var shortID [8]byte
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
if err != nil {
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
}
if decodedLen > 8 {
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
}
tlsConfig.ShortIds[shortID] = true
} }
if decodedLen > 8 {
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
}
tlsConfig.ShortIds[shortID] = true
} }
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain()) handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain())
@@ -109,10 +119,6 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
} }
if debug.Enabled {
tlsConfig.Show = true
}
return &RealityServerConfig{&tlsConfig}, nil return &RealityServerConfig{&tlsConfig}, nil
} }
@@ -153,7 +159,7 @@ func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) {
} }
func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) { func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
tlsConn, err := reality.Server(ctx, conn, c.config) tlsConn, err := utls.RealityServer(ctx, conn, c.config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -169,7 +175,7 @@ func (c *RealityServerConfig) Clone() Config {
var _ Conn = (*realityConnWrapper)(nil) var _ Conn = (*realityConnWrapper)(nil)
type realityConnWrapper struct { type realityConnWrapper struct {
*reality.Conn *utls.Conn
} }
func (c *realityConnWrapper) ConnectionState() ConnectionState { func (c *realityConnWrapper) ConnectionState() ConnectionState {

View File

@@ -1,15 +1,5 @@
//go:build !with_reality_server //go:build with_reality_server
package tls package tls
import ( var _ int = "The separate `with_reality_server` build tag has been merged into `with_utls` and is no longer needed, please update your build configuration."
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
}

View File

@@ -16,13 +16,10 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
if !options.Enabled { if !options.Enabled {
return nil, nil return nil, nil
} }
if options.ECH != nil && options.ECH.Enabled { if options.Reality != nil && options.Reality.Enabled {
return NewECHServer(ctx, logger, options)
} else if options.Reality != nil && options.Reality.Enabled {
return NewRealityServer(ctx, logger, options) return NewRealityServer(ctx, logger, options)
} else {
return NewSTDServer(ctx, logger, options)
} }
return NewSTDServer(ctx, logger, options)
} }
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {

View File

@@ -127,5 +127,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
if options.ECH != nil && options.ECH.Enabled {
return parseECHClientConfig(ctx, options, &tlsConfig)
}
return &STDClientConfig{&tlsConfig}, nil return &STDClientConfig{&tlsConfig}, nil
} }

View File

@@ -6,6 +6,7 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"time"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -21,11 +22,12 @@ var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
acmeService adapter.Service acmeService adapter.SimpleLifecycle
certificate []byte certificate []byte
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
echKeyPath string
watcher *fswatch.Watcher watcher *fswatch.Watcher
} }
@@ -94,6 +96,9 @@ func (c *STDServerConfig) startWatcher() error {
if c.keyPath != "" { if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath) watchPath = append(watchPath, c.keyPath)
} }
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
watcher, err := fswatch.NewWatcher(fswatch.Options{ watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath, Path: watchPath,
Callback: func(path string) { Callback: func(path string) {
@@ -115,25 +120,33 @@ func (c *STDServerConfig) startWatcher() error {
} }
func (c *STDServerConfig) certificateUpdated(path string) error { func (c *STDServerConfig) certificateUpdated(path string) error {
if path == c.certificatePath { if path == c.certificatePath || path == c.keyPath {
certificate, err := os.ReadFile(c.certificatePath) if path == c.certificatePath {
if err != nil { certificate, err := os.ReadFile(c.certificatePath)
return E.Cause(err, "reload certificate from ", c.certificatePath) if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
} else if path == c.keyPath {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
} }
c.certificate = certificate keyPair, err := tls.X509KeyPair(c.certificate, c.key)
} else if path == c.keyPath {
key, err := os.ReadFile(c.keyPath)
if err != nil { if err != nil {
return E.Cause(err, "reload key from ", c.keyPath) return E.Cause(err, "reload key pair")
} }
c.key = key c.config.Certificates = []tls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else if path == c.echKeyPath {
err := reloadECHKeys(c.echKeyPath, c.config)
if err != nil {
return err
}
c.logger.Info("reloaded ECH keys")
} }
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []tls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil return nil
} }
@@ -152,7 +165,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, nil return nil, nil
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
var acmeService adapter.Service var acmeService adapter.SimpleLifecycle
var err error var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck //nolint:staticcheck
@@ -221,8 +234,12 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
key = content key = content
} }
if certificate == nil && key == nil && options.Insecure { if certificate == nil && key == nil && options.Insecure {
timeFunc := ntp.TimeFuncFromContext(ctx)
if timeFunc == nil {
timeFunc = time.Now
}
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName) return GenerateKeyPair(nil, nil, timeFunc, info.ServerName)
} }
} else { } else {
if certificate == nil { if certificate == nil {
@@ -238,6 +255,13 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
tlsConfig.Certificates = []tls.Certificate{keyPair} tlsConfig.Certificates = []tls.Certificate{keyPair}
} }
} }
var echKeyPath string
if options.ECH != nil && options.ECH.Enabled {
err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
if err != nil {
return nil, err
}
}
return &STDServerConfig{ return &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
@@ -246,5 +270,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
key: key, key: key,
certificatePath: options.CertificatePath, certificatePath: options.CertificatePath,
keyPath: options.KeyPath, keyPath: options.KeyPath,
echKeyPath: echKeyPath,
}, nil }, nil
} }

View File

@@ -16,8 +16,8 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
utls "github.com/sagernet/utls"
utls "github.com/metacubex/utls"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )

View File

@@ -5,6 +5,7 @@ package tls
import ( import (
"context" "context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
@@ -14,5 +15,9 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
} }
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`) return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
}
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
} }

View File

@@ -1,7 +1,9 @@
package tf package tf
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"math/rand" "math/rand"
"net" "net"
"strings" "strings"
@@ -17,17 +19,19 @@ type Conn struct {
tcpConn *net.TCPConn tcpConn *net.TCPConn
ctx context.Context ctx context.Context
firstPacketWritten bool firstPacketWritten bool
splitRecord bool
fallbackDelay time.Duration fallbackDelay time.Duration
} }
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) { func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn) tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{ return &Conn{
Conn: conn, Conn: conn,
tcpConn: tcpConn, tcpConn: tcpConn,
ctx: ctx, ctx: ctx,
splitRecord: splitRecord,
fallbackDelay: fallbackDelay, fallbackDelay: fallbackDelay,
}, nil }
} }
func (c *Conn) Write(b []byte) (n int, err error) { func (c *Conn) Write(b []byte) (n int, err error) {
@@ -37,10 +41,12 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}() }()
serverName := indexTLSServerName(b) serverName := indexTLSServerName(b)
if serverName != nil { if serverName != nil {
if c.tcpConn != nil { if !c.splitRecord {
err = c.tcpConn.SetNoDelay(true) if c.tcpConn != nil {
if err != nil { err = c.tcpConn.SetNoDelay(true)
return if err != nil {
return
}
} }
} }
splits := strings.Split(serverName.ServerName, ".") splits := strings.Split(serverName.ServerName, ".")
@@ -61,16 +67,25 @@ func (c *Conn) Write(b []byte) (n int, err error) {
currentIndex++ currentIndex++
} }
} }
var buffer bytes.Buffer
for i := 0; i <= len(splitIndexes); i++ { for i := 0; i <= len(splitIndexes); i++ {
var payload []byte var payload []byte
if i == 0 { if i == 0 {
payload = b[:splitIndexes[i]] payload = b[:splitIndexes[i]]
if c.splitRecord {
payload = payload[recordLayerHeaderLen:]
}
} else if i == len(splitIndexes) { } else if i == len(splitIndexes) {
payload = b[splitIndexes[i-1]:] payload = b[splitIndexes[i-1]:]
} else { } else {
payload = b[splitIndexes[i-1]:splitIndexes[i]] payload = b[splitIndexes[i-1]:splitIndexes[i]]
} }
if c.tcpConn != nil && i != len(splitIndexes) { if c.splitRecord {
payloadLen := uint16(len(payload))
buffer.Write(b[:3])
binary.Write(&buffer, binary.BigEndian, payloadLen)
buffer.Write(payload)
} else if c.tcpConn != nil && i != len(splitIndexes) {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
if err != nil { if err != nil {
return return
@@ -82,11 +97,18 @@ func (c *Conn) Write(b []byte) (n int, err error) {
} }
} }
} }
if c.tcpConn != nil { if c.splitRecord {
err = c.tcpConn.SetNoDelay(false) _, err = c.Conn.Write(buffer.Bytes())
if err != nil { if err != nil {
return return
} }
} else {
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false)
if err != nil {
return
}
}
} }
return len(b), nil return len(b), nil
} }

View File

@@ -0,0 +1,32 @@
package tf_test
import (
"context"
"crypto/tls"
"net"
"testing"
tf "github.com/sagernet/sing-box/common/tlsfragment"
"github.com/stretchr/testify/require"
)
func TestTLSFragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}
func TestTLSRecordFragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}

View File

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

View File

@@ -19,11 +19,15 @@ const (
TypeTor = "tor" TypeTor = "tor"
TypeSSH = "ssh" TypeSSH = "ssh"
TypeShadowTLS = "shadowtls" TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr" TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless" TypeVLESS = "vless"
TypeTUIC = "tuic" TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2" TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale" TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
) )
const ( const (
@@ -77,6 +81,8 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC" return "TUIC"
case TypeHysteria2: case TypeHysteria2:
return "Hysteria2" return "Hysteria2"
case TypeAnyTLS:
return "AnyTLS"
case TypeSelector: case TypeSelector:
return "Selector" return "Selector"
case TypeURLTest: case TypeURLTest:

View File

@@ -33,6 +33,7 @@ const (
RuleActionTypeHijackDNS = "hijack-dns" RuleActionTypeHijackDNS = "hijack-dns"
RuleActionTypeSniff = "sniff" RuleActionTypeSniff = "sniff"
RuleActionTypeResolve = "resolve" RuleActionTypeResolve = "resolve"
RuleActionTypePredefined = "predefined"
) )
const ( const (

View File

@@ -24,9 +24,9 @@ func applyDebugOptions(options option.DebugOptions) {
if options.TraceBack != "" { if options.TraceBack != "" {
debug.SetTraceback(options.TraceBack) debug.SetTraceback(options.TraceBack)
} }
if options.MemoryLimit != 0 { if options.MemoryLimit.Value() != 0 {
debug.SetMemoryLimit(int64(float64(options.MemoryLimit) / 1.5)) debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5))
conntrack.MemoryLimit = uint64(options.MemoryLimit) conntrack.MemoryLimit = options.MemoryLimit.Value()
} }
if options.OOMKiller != nil { if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller conntrack.KillerEnabled = *options.OOMKiller

View File

@@ -7,9 +7,9 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"github.com/sagernet/sing-box/common/humanize"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badjson"
@@ -38,9 +38,9 @@ func applyDebugListenOption(options option.DebugOptions) {
runtime.ReadMemStats(&memStats) runtime.ReadMemStats(&memStats)
var memObject badjson.JSONObject var memObject badjson.JSONObject
memObject.Put("heap", humanize.MemoryBytes(memStats.HeapInuse)) memObject.Put("heap", byteformats.FormatMemoryBytes(memStats.HeapInuse))
memObject.Put("stack", humanize.MemoryBytes(memStats.StackInuse)) memObject.Put("stack", byteformats.FormatMemoryBytes(memStats.StackInuse))
memObject.Put("idle", humanize.MemoryBytes(memStats.HeapIdle-memStats.HeapReleased)) memObject.Put("idle", byteformats.FormatMemoryBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("goroutines", runtime.NumGoroutine()) memObject.Put("goroutines", runtime.NumGoroutine())
memObject.Put("rss", rusageMaxRSS()) memObject.Put("rss", rusageMaxRSS())

View File

@@ -17,7 +17,7 @@ import (
"github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/contrab/maphash"
"github.com/miekg/dns" dns "github.com/miekg/dns"
) )
var ( var (
@@ -105,7 +105,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
question := message.Question[0] question := message.Question[0]
if options.ClientSubnet.IsValid() { if options.ClientSubnet.IsValid() {
message = SetClientSubnet(message, options.ClientSubnet, true) message = SetClientSubnet(message, options.ClientSubnet)
} }
isSimpleRequest := len(message.Question) == 1 && isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 && len(message.Ns) == 0 &&
@@ -232,10 +232,20 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
record.Header().Ttl = timeToLive record.Header().Ttl = timeToLive
} }
} }
response.Id = messageId
if !disableCache { if !disableCache {
c.storeCache(transport, question, response, timeToLive) c.storeCache(transport, question, response, timeToLive)
} }
response.Id = messageId
requestEDNSOpt := message.IsEdns0()
responseEDNSOpt := response.IsEdns0()
if responseEDNSOpt != nil && (requestEDNSOpt == nil || requestEDNSOpt.Version() < responseEDNSOpt.Version()) {
response.Extra = common.Filter(response.Extra, func(it dns.RR) bool {
return it.Header().Rrtype != dns.TypeOPT
})
if requestEDNSOpt != nil {
response.SetEdns0(responseEDNSOpt.UDPSize(), responseEDNSOpt.Do())
}
}
logExchangedResponse(c.logger, ctx, response, timeToLive) logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err return response, err
} }
@@ -243,9 +253,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
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(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain) domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain) dnsName := dns.Fqdn(domain)
if options.Strategy == C.DomainStrategyIPv4Only { var strategy C.DomainStrategy
if options.LookupStrategy != C.DomainStrategyAsIS {
strategy = options.LookupStrategy
} else {
strategy = options.Strategy
}
if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if options.Strategy == C.DomainStrategyIPv6Only { } else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
} }
var response4 []netip.Addr var response4 []netip.Addr
@@ -271,7 +287,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
if len(response4) == 0 && len(response6) == 0 { if len(response4) == 0 && len(response6) == 0 {
return nil, err return nil, err
} }
return sortAddresses(response4, response6, options.Strategy), nil return sortAddresses(response4, response6, strategy), nil
} }
func (c *Client) ClearCache() { func (c *Client) ClearCache() {
@@ -483,8 +499,8 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
} }
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) { func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { if response.Rcode != dns.RcodeSuccess {
return nil, RCodeError(response.Rcode) return nil, RcodeError(response.Rcode)
} }
addresses := make([]netip.Addr, 0, len(response.Answer)) addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer { for _, rawAnswer := range response.Answer {
@@ -508,10 +524,10 @@ func wrapError(err error) error {
switch dnsErr := err.(type) { switch dnsErr := err.(type) {
case *net.DNSError: case *net.DNSError:
if dnsErr.IsNotFound { if dnsErr.IsNotFound {
return RCodeNameError return RcodeNameError
} }
case *net.AddrError: case *net.AddrError:
return RCodeNameError return RcodeNameError
} }
return err return err
} }
@@ -527,17 +543,31 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
return value, loaded return value, loaded
} }
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
return &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Rcode: rcode,
Response: true,
},
Question: message.Question,
}
}
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg { func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
} }
for _, address := range addresses { for _, address := range addresses {
if address.Is4() { if address.Is4() && question.Qtype == dns.TypeA {
response.Answer = append(response.Answer, &dns.A{ response.Answer = append(response.Answer, &dns.A{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
Name: question.Name, Name: question.Name,
@@ -547,7 +577,7 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
}, },
A: address.AsSlice(), A: address.AsSlice(),
}) })
} else { } else if address.Is6() && question.Qtype == dns.TypeAAAA {
response.Answer = append(response.Answer, &dns.AAAA{ response.Answer = append(response.Answer, &dns.AAAA{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
Name: question.Name, Name: question.Name,
@@ -561,3 +591,82 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
} }
return &response return &response
} }
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
Answer: []dns.RR{
&dns.CNAME{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: timeToLive,
},
Target: record,
},
},
}
return &response
}
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
Answer: []dns.RR{
&dns.TXT{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: timeToLive,
},
Txt: records,
},
},
}
return &response
}
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
}
for _, record := range records {
response.Answer = append(response.Answer, &dns.MX{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: timeToLive,
},
Preference: record.Pref,
Mx: record.Host,
})
}
return &response
}

View File

@@ -15,6 +15,8 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
} }
responseLen := response.Len() responseLen := response.Len()
if responseLen > maxLen { if responseLen > maxLen {
copyResponse := *response
response = &copyResponse
response.Truncate(maxLen) response.Truncate(maxLen)
} }
buffer := buf.NewSize(headroom*2 + 1 + responseLen) buffer := buf.NewSize(headroom*2 + 1 + responseLen)

View File

@@ -6,7 +6,11 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) *dns.Msg { func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix) *dns.Msg {
return setClientSubnet(message, clientSubnet, true)
}
func setClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, clone bool) *dns.Msg {
var ( var (
optRecord *dns.OPT optRecord *dns.OPT
subnetOption *dns.EDNS0_SUBNET subnetOption *dns.EDNS0_SUBNET
@@ -19,9 +23,6 @@ findExists:
var isEDNS0Subnet bool var isEDNS0Subnet bool
subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET) subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET)
if isEDNS0Subnet { if isEDNS0Subnet {
if !override {
return message
}
break findExists break findExists
} }
} }
@@ -37,14 +38,14 @@ findExists:
}, },
} }
message.Extra = append(message.Extra, optRecord) message.Extra = append(message.Extra, optRecord)
} else { } else if clone {
message = message.Copy() return setClientSubnet(message.Copy(), clientSubnet, false)
} }
if subnetOption == nil { if subnetOption == nil {
subnetOption = new(dns.EDNS0_SUBNET) subnetOption = new(dns.EDNS0_SUBNET)
subnetOption.Code = dns.EDNS0SUBNET
optRecord.Option = append(optRecord.Option, subnetOption) optRecord.Option = append(optRecord.Option, subnetOption)
} }
subnetOption.Code = dns.EDNS0SUBNET
if clientSubnet.Addr().Is4() { if clientSubnet.Addr().Is4() {
subnetOption.Family = 1 subnetOption.Family = 1
} else { } else {

View File

@@ -1,33 +1,17 @@
package dns package dns
import F "github.com/sagernet/sing/common/format" import (
mDNS "github.com/miekg/dns"
const (
RCodeSuccess RCodeError = 0 // NoError
RCodeFormatError RCodeError = 1 // FormErr
RCodeServerFailure RCodeError = 2 // ServFail
RCodeNameError RCodeError = 3 // NXDomain
RCodeNotImplemented RCodeError = 4 // NotImp
RCodeRefused RCodeError = 5 // Refused
) )
type RCodeError uint16 const (
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused
)
func (e RCodeError) Error() string { type RcodeError int
switch e {
case RCodeSuccess: func (e RcodeError) Error() string {
return "success" return mDNS.RcodeToString[int(e)]
case RCodeFormatError:
return "format error"
case RCodeServerFailure:
return "server failure"
case RCodeNameError:
return "name error"
case RCodeNotImplemented:
return "not implemented"
case RCodeRefused:
return "refused"
default:
return F.ToString("unknown error: ", uint16(e))
}
} }

View File

@@ -190,6 +190,8 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
} }
case *R.RuleActionReject: case *R.RuleActionReject:
return nil, currentRule, currentRuleIndex return nil, currentRule, currentRuleIndex
case *R.RuleActionPredefined:
return nil, currentRule, currentRuleIndex
} }
} }
} }
@@ -256,10 +258,19 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
case *R.RuleActionReject: case *R.RuleActionReject:
switch action.Method { switch action.Method {
case C.RuleActionRejectMethodDefault: case C.RuleActionRejectMethodDefault:
return FixedResponse(message.Id, message.Question[0], nil, 0), nil return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop return nil, tun.ErrDrop
} }
case *R.RuleActionPredefined:
return action.Response(message), nil
} }
} }
var responseCheck func(responseAddrs []netip.Addr) bool var responseCheck func(responseAddrs []netip.Addr) bool
@@ -281,7 +292,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} else if errors.Is(err, ErrResponseRejected) { } else if errors.Is(err, ErrResponseRejected) {
rejected = true rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
*/
} else if len(message.Question) > 0 { } else if len(message.Question) > 0 {
rejected = true
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else { } else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>")) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
@@ -319,6 +335,9 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err error err error
) )
printResult := func() { printResult := func() {
if err == nil && len(responseAddrs) == 0 {
err = E.New("empty result")
}
if err != nil { if err != nil {
if errors.Is(err, ErrResponseRejectedCached) { if errors.Is(err, ErrResponseRejectedCached) {
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)") r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
@@ -327,15 +346,15 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
} else { } else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
} }
} else if len(responseAddrs) == 0 { }
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") if err != nil {
err = RCodeNameError err = E.Cause(err, "lookup ", domain)
} }
} }
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy) responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached { if cached {
if len(responseAddrs) == 0 { if len(responseAddrs) == 0 {
return nil, RCodeNameError return nil, E.New("lookup ", domain, ": empty result (cached)")
} }
return responseAddrs, nil return responseAddrs, nil
} }
@@ -366,7 +385,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
ruleIndex = -1 ruleIndex = -1
for { for {
dnsCtx := adapter.OverrideContext(ctx) dnsCtx := adapter.OverrideContext(ctx)
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &options) dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
if rule != nil { if rule != nil {
switch action := rule.Action().(type) { switch action := rule.Action().(type) {
case *R.RuleActionReject: case *R.RuleActionReject:
@@ -376,6 +396,20 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop return nil, tun.ErrDrop
} }
case *R.RuleActionPredefined:
if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode)
} else {
for _, answer := range action.Answer {
switch record := answer.(type) {
case *mDNS.A:
responseAddrs = append(responseAddrs, M.AddrFromIP(record.A))
case *mDNS.AAAA:
responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA))
}
}
}
goto response
} }
} }
var responseCheck func(responseAddrs []netip.Addr) bool var responseCheck func(responseAddrs []netip.Addr) bool
@@ -385,16 +419,17 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
return rule.MatchAddressLimit(metadata) return rule.MatchAddressLimit(metadata)
} }
} }
if options.Strategy == C.DomainStrategyAsIS { if dnsOptions.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy dnsOptions.Strategy = r.defaultDomainStrategy
} }
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, options, responseCheck) responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck)
if responseCheck == nil || err == nil { if responseCheck == nil || err == nil {
break break
} }
printResult() printResult()
} }
} }
response:
printResult() printResult()
if len(responseAddrs) > 0 { if len(responseAddrs) > 0 {
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
@@ -429,6 +464,6 @@ func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
func (r *Router) ResetNetwork() { func (r *Router) ResetNetwork() {
r.ClearCache() r.ClearCache()
for _, transport := range r.transport.Transports() { for _, transport := range r.transport.Transports() {
transport.Reset() transport.Close()
} }
} }

View File

@@ -81,7 +81,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
func (t *Transport) Close() error { func (t *Transport) Close() error {
for _, transport := range t.transports { for _, transport := range t.transports {
transport.Reset() transport.Close()
} }
if t.interfaceCallback != nil { if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
@@ -89,12 +89,6 @@ func (t *Transport) Close() error {
return nil return nil
} }
func (t *Transport) Reset() {
for _, transport := range t.transports {
transport.Reset()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
err := t.fetchServers() err := t.fetchServers()
if err != nil { if err != nil {
@@ -252,7 +246,7 @@ func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.So
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr)) transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
} }
for _, transport := range t.transports { for _, transport := range t.transports {
transport.Reset() transport.Close()
} }
t.transports = transports t.transports = transports
return nil return nil

View File

@@ -55,7 +55,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA { if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
return nil, E.New("only IP queries are supported by fakeip") return nil, E.New("only IP queries are supported by fakeip")
} }
address, err := t.store.Create(question.Name, question.Qtype == mDNS.TypeAAAA) address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,12 +2,15 @@ package hosts
import ( import (
"context" "context"
"net/netip"
"os"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/service/filemanager"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
@@ -20,31 +23,49 @@ var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct { type Transport struct {
dns.TransportAdapter dns.TransportAdapter
files []*File files []*File
predefined map[string][]netip.Addr
} }
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) { func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
var files []*File var (
files []*File
predefined = make(map[string][]netip.Addr)
)
if len(options.Path) == 0 { if len(options.Path) == 0 {
files = append(files, NewFile(DefaultPath)) files = append(files, NewFile(DefaultPath))
} else { } else {
for _, path := range options.Path { for _, path := range options.Path {
files = append(files, NewFile(path)) files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path))))
}
}
if options.Predefined != nil {
for _, entry := range options.Predefined.Entries() {
predefined[mDNS.CanonicalName(entry.Key)] = entry.Value
} }
} }
return &Transport{ return &Transport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil), TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil),
files: files, files: files,
predefined: predefined,
}, nil }, nil
} }
func (t *Transport) Reset() { func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
return nil
} }
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name) domain := mDNS.CanonicalName(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
if addresses, ok := t.predefined[domain]; ok {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
for _, file := range t.files { for _, file := range t.files {
addresses := file.Lookup(domain) addresses := file.Lookup(domain)
if len(addresses) > 0 { if len(addresses) > 0 {

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