Compare commits

...
Author SHA1 Message Date
patternihaandClaude Opus 5.5 f2f292c90c TUN inbound: Support "tcp/port" and "udp/port" in autoSystemWfpBlockLeak on Windows; "dns" covers DoT and DoQ as well
"dns" in autoSystemWfpBlockLeak only blocked port 53 outside the TUN, plus
the DNS Client service on Windows 11 and Server 2022 and later, so other
programs' DNS over TLS (TCP 853) and DNS over QUIC (UDP 853) could still
leave outside the TUN.

autoSystemWfpBlockLeak now takes:

- "tcp/<port>", "udp/<port>": connections to that port may only go through
  the TUN (or come from Xray itself).
- "windnsclient": Windows' DNS Client service may only connect through the
  TUN, except for mDNS and LLMNR. As before, this only applies from Windows
  11 and Server 2022 on, where the service may use DoH or DoT to any port;
  earlier versions only query port 53.
- "dns": stands for "udp/53", "tcp/53", "windnsclient", "tcp/853" and
  "udp/853".
- "misconfigtun": unchanged.

Values are case-insensitive, deduplicated, and anything else, such as
"tcp/0" or "icmp/1", is rejected. Keeping the system's DNS inside the TUN,
with "udp/53", "tcp/53" or "windnsclient", needs dns to be set, as "dns"
did; ports like 853 alone do not. Each port gets its own permit-through-TUN
and block filters with a protocol condition, where port 53 used to have one
for both protocols, and the log lists what is blocked, e.g. "[tun] blocked
outside the TUN: udp/53, tcp/53, tcp/853, udp/853, windnsclient".

Tested on Windows 11 with a Wintun adapter as the outside interface: with
"tcp/853", "udp/853", "tcp/7215" and "udp/7216", those are blocked outside
the TUN over IPv4 and IPv6, while tcp/7216, udp/7215 and 443 are not, port
853 works through the TUN, Xray's own connections to port 853 still leave
outside it, and nothing is blocked after Xray stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:52:11 +03:30
RPRXandGitHub 0afec66085 Xray-core v26.10.29
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-10-29 08:38:50 +00:00
55eecaed16 Config: Move inbound module initialization to the end (#7284)
Fixes https://github.com/XTLS/Xray-core/pull/7284#issuecomment-6491545028

---------

Co-authored-by: Copilot <copilot@github.com>
2026-10-29 08:34:16 +00:00
2dfee4b842 XDRIVE transport: Carry a Google Drive session in a ring of slots (#7217)
https://github.com/XTLS/Xray-core/pull/7217#issuecomment-6489554617

---------

Co-authored-by: Ahmad-2213 <106820058+Ahmad-2213@users.noreply.github.com>
2026-10-29 06:47:42 +00:00
Жора ЗмейкинandGitHub b4123b5748 XDRIVE transport: Add "s3" storage backend (#7231)
https://github.com/XTLS/Xray-core/pull/7231#issuecomment-6488077212
2026-10-29 04:44:41 +00:00
ec124ff9e0 WebSocket transport: Treat normal close frames as io.EOF (#7236)
Fixes https://github.com/XTLS/Xray-core/issues/7235#issuecomment-6353389899

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-29 03:42:11 +00:00
SolomonandGitHub 8a3c0cbf1d Sockopt: Fix swapped TCP keepalive idle and interval on Darwin (#7219)
https://github.com/XTLS/Xray-core/pull/7219#pullrequestreview-5691960668
2026-10-29 02:54:53 +00:00
78b5a6ec5c GitHub Action CI: Enforce stricter checks on generated protobuf files (#7263)
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6447862934
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6484719865
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6485805671

---------

Co-authored-by: Fangliding <Fangliding.fshxy@outlook.com>
2026-10-29 02:34:47 +00:00
5865e3ac3d DNS outbound: Keep hijacked A/AAAA replies within the buffer (#7225)
https://github.com/XTLS/Xray-core/pull/7225#pullrequestreview-5613859264

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-29 00:42:19 +00:00
48f62b801a Geodata: Limit download size, file extensions, and update frequency (#7267)
https://github.com/XTLS/Xray-core/pull/7265#issuecomment-6464588139

---------

Co-authored-by: xqzr <34030394+xqzr@users.noreply.github.com>
2026-10-29 00:22:51 +00:00
MeowandGitHub 43c29f8eb1 Geodata: Update built-in generalized "private" IP ranges (#7249)
And https://github.com/XTLS/Xray-core/pull/7249#issuecomment-6483478811

https://github.com/XTLS/Xray-core/pull/7249#issuecomment-6391494484
https://github.com/XTLS/Xray-core/pull/7249#issuecomment-6483591550
2026-10-28 23:45:22 +00:00
MeowandGitHub 613d8147bb Direct/Freedom outbound: Warn about migrating fragment and noises to Finalmask (#7242)
https://github.com/XTLS/Xray-core/issues/7240#issuecomment-6361862358
2026-10-28 22:49:30 +00:00
38f090255c Commands: Fix xray api adrules c1.json c2.json (#7276)
https://github.com/XTLS/Xray-core/pull/7276#pullrequestreview-5680949792

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-28 21:59:27 +00:00
35a3121a83 File loader: Check file extensions without resolving symlinks (#7255)
https://github.com/XTLS/Xray-core/pull/7255#issuecomment-6481194940
https://github.com/XTLS/Xray-core/pull/7250#issuecomment-6398816842

Fixes https://github.com/XTLS/Xray-core/issues/7239#issuecomment-6356122816

---------

Co-authored-by: Eljees <3.14hell@gmail.com>
2026-10-28 21:59:03 +00:00
风扇滑翔翼andGitHub bca08ac080 Routing: Fix Linux IPv6 process (#7232)
Fixes https://github.com/XTLS/Xray-core/issues/7227
2026-10-28 20:49:08 +00:00
6a4274ab75 TUN inbound: autoOutboundsInterface should not set IP_BOUND_IF on IPv6 sockets on Darwin (mainly macOS) (#7230)
Fixes https://github.com/XTLS/Xray-core/issues/7228

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:34:40 +00:00
solodecodeandGitHub 229799a7a3 Direct/Freedom outbound: Fix blockDelay for DispatchLink() inbounds such as VLESS, Hysteria, etc. (#7238)
https://github.com/XTLS/Xray-core/pull/7238#pullrequestreview-5627787937
https://github.com/XTLS/Xray-core/pull/7238#issuecomment-6467092152
https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4384511163
2026-10-28 11:02:55 +00:00
3071fbfa25 Sockopt: Handle single-family interface binding on Windows, including TUN inbound's autoOutboundsInterface (#7214)
And https://github.com/XTLS/Xray-core/pull/7214#issuecomment-6329118637

https://github.com/XTLS/Xray-core/pull/7201#issuecomment-6314453444

Fixes https://github.com/XTLS/Xray-core/issues/7160#issuecomment-6246944012

---------

Co-authored-by: patterniha <71074308+patterniha@users.noreply.github.com>
2026-10-28 10:15:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1576e79563 Bump github.com/pires/go-proxyproto from 0.15.0 to 0.16.0 (#7237)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 04:31:46 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
67e7f82ca1 Bump docker/login-action from 4.6.0 to 4.7.0 (#7257)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4.6.0...v4.7.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 04:31:31 +00:00
RPRXandGitHub 1b1647cd9d Xray-core v26.10.21
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-10-21 11:59:30 +00:00
Artem LytkinandGitHub 557657b576 TUN inbound: Read large TCP segments straight into Xray-core's buffers on Linux (#7179)
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6119897360
https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6205035216
https://github.com/XTLS/Xray-core/pull/7179#issuecomment-6250145934
2026-10-21 11:49:36 +00:00
Artem LytkinandGitHub fc5ba8e6a2 XDRIVE transport: Support h3 (Chrome CH by default) and XMUX as well; Send Chrome HTTP headers (User-Agent, etc.) by default (#7187)
https://github.com/XTLS/Xray-core/pull/7181#issuecomment-6229694351
https://github.com/XTLS/Xray-core/pull/7187#issuecomment-6250200781
https://github.com/XTLS/Xray-core/pull/7187#issuecomment-6314784068
2026-10-21 11:33:55 +00:00
genkauzenandRPRX 00c0277787 XDRIVE transport: Support two-step "template" operations (hrefPath) (#7188)
https://github.com/XTLS/Xray-core/pull/7188#issuecomment-6250146420
2026-10-21 10:55:36 +00:00
MeowandGitHub 3653dcb4fc Finalmask: Add filter for inbound (TCP & UDP, source IP filtering) (#7206)
https://github.com/XTLS/Xray-core/pull/7206#issuecomment-6312634417
2026-10-21 10:48:15 +00:00
061c2df291 HTTP outbound: Fix hanging on server-first traffic (#7193)
https://github.com/XTLS/Xray-core/pull/7193#issuecomment-6312885274

Fixes https://github.com/XTLS/Xray-core/issues/3459

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-21 10:33:27 +00:00
23e2292344 Transport: Pass UDP destination to dialer controllers (#7201)
https://github.com/XTLS/Xray-core/pull/7201#issuecomment-6275732621

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-21 10:20:35 +00:00
41797c234d Socks inbound: Initialize TempUDPConn's Timer at the beginning (#7203)
Fixes https://github.com/XTLS/Xray-core/pull/7200

---------

Co-authored-by: Eliot the Cougar <1861169+eliotcougar@users.noreply.github.com>
2026-10-21 09:17:34 +00:00
MeowandGitHub 9424810830 Docker: Restrict log & conf permissions (#7204)
https://github.com/XTLS/Xray-core/pull/7204#issuecomment-6292218518
2026-10-21 08:55:32 +00:00
12b7f85de3 Routing: Fix Linux UDP process (#7202)
Fixes https://github.com/XTLS/Xray-core/issues/7192 and https://github.com/XTLS/Xray-core/pull/7195

---------

Co-authored-by: Faraquic! <141701604+faraquic@users.noreply.github.com>
2026-10-21 08:35:52 +00:00
𐲓𐳛𐳪𐳂𐳐 𐲀𐳢𐳦𐳫𐳢 𐲥𐳔𐳛𐳪𐳌𐳑𐳖𐳇andGitHub de1bbbff88 GitHub Actions: Switch geodata to XTLS/Xray-rules-dat (#7185)
https://github.com/XTLS/Xray-core/pull/7185#issuecomment-6247177356
2026-10-21 08:18:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ec73b73f5e Bump github.com/klauspost/cpuid/v2 from 2.4.0 to 2.4.1 (#7197)
Bumps [github.com/klauspost/cpuid/v2](https://github.com/klauspost/cpuid) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/klauspost/cpuid/releases)
- [Commits](https://github.com/klauspost/cpuid/compare/v2.4.0...v2.4.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/cpuid/v2
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-21 08:07:31 +00:00
风扇滑翔翼andRPRX 6307fa1265 TLS client: Update built-in RootCAs to remove newly low-credit certs (#7212)
Fixes https://github.com/XTLS/Xray-core/pull/7212#issuecomment-6310150214

... https://github.com/XTLS/Xray-core/pull/7105#issuecomment-6056053599

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-10-21 07:55:00 +00:00
RPRXandGitHub 92ccc939aa Xray-core v26.10.17
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-10-17 15:23:56 +00:00
Artem LytkinandGitHub 95461cc148 XDRIVE transport: Require VLESS Multiplex+Encryption; "Google Drive"->"google-drive" (#7182)
https://github.com/XTLS/Xray-core/pull/6748#issuecomment-5752780799
2026-10-17 14:13:57 +00:00
Artem LytkinandGitHub 3834e504b8 XDRIVE transport: Prefer h2 (Chrome CH by default); End stalled requests; Other small changes (#7181)
https://github.com/XTLS/Xray-core/pull/7170#issuecomment-6204133548
2026-10-17 09:24:53 +00:00
0f127f934c XTLS Vision: Compatible with Go 1.27.3+ (#7176)
https://github.com/XTLS/Xray-core/pull/7176#issuecomment-6229087350

Fixes https://github.com/XTLS/Xray-core/issues/7162#issuecomment-6210430747

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-10-17 03:37:14 +00:00
0f3d884786 WireGuard outbound: Keep the device when its setup fails (#7151)
https://github.com/XTLS/Xray-core/pull/7151#issuecomment-6186150131

Fixes https://github.com/XTLS/Xray-core/pull/7151#issuecomment-6190189547

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-10-16 09:50:47 +00:00
da2248c516 Hysteria client: Reset stalled writes when the connection is closed (#7172)
https://github.com/XTLS/Xray-core/pull/7172#issuecomment-6208589708

Fixes https://github.com/XTLS/Xray-core/issues/7169#issuecomment-6204450876

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-10-16 15:25:17 +08:00
fe2ae23f42 Sockopt: Cancel dialing after failed to bind interface, external controllers included such as TUN inbound's (#7173)
https://github.com/XTLS/Xray-core/pull/7161#issuecomment-6185101741

Fixes https://github.com/XTLS/Xray-core/issues/7160#issuecomment-6200788453

---------

Co-authored-by: Gleb <a1212gleb@gmail.com>
2026-10-16 06:37:52 +00:00
d15966c76c Fixes: Regard int() as int32() by default, which should be a coding standard (#7155)
https://github.com/XTLS/Xray-core/pull/7155#issuecomment-6208455069

Fixes https://github.com/XTLS/Xray-core/pull/7148#issuecomment-6173633056

---------

Co-authored-by: Denis Orlov <freshcrowd@gmail.com>
Co-authored-by: Milomilo777 <117558067+Milomilo777@users.noreply.github.com>
2026-10-16 05:12:52 +00:00
206a24a9f0 Noise finalmask: Support Lua script as well (on main branch: inline scripts were disabled for now; sandbox-only for now) (#7124)
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6108165942
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6149363945
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6207934753

---------

Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-10-16 04:43:55 +00:00
MeowandRPRX ac5d8a83ba Lua: Sandbox by default (whitelisted functions); Disable inline scripts for now; Add more checks for security reasons (#7136)
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6158632073
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6170831601
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6184362904
2026-10-16 01:55:35 +00:00
Artem LytkinandGitHub 8703fdb101 WireGuard, MASQUE proxies: Use Xray-core's built-in lightweight TCP/IP stack instead of gVisor as well (#7156)
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6116258188
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6120742739
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6137264458
https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6202933664
https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6203239736
2026-10-15 23:38:14 +00:00
LjhAUMEMandGitHub 10879a4a8b XDNS finalmask: Support DoH ("https://", tlsConfig, domain fronting, etc., quicConfig, Chrome Client Hello by default) with empty HTTP User-Agent (#7140)
https://github.com/XTLS/Xray-core/pull/7103#issuecomment-6118333973
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6161808702
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6167254749
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6185965142
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6200775375
2026-10-15 22:41:19 +00:00
0888beb38a SS2022 proxy: Correct interrupt behavior and UDP conn change (#7143)
Fixes https://github.com/XTLS/Xray-core/pull/7142 and https://github.com/XTLS/Xray-core/pull/7143#issuecomment-6163926468

---------

Co-authored-by: ChownYourLife <260039878+ChownYourLife@users.noreply.github.com>
2026-10-14 11:05:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
34acded003 Bump golang.org/x/net from 0.60.0 to 0.61.0 (#7147)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.60.0 to 0.61.0.
- [Commits](https://github.com/golang/net/compare/v0.60.0...v0.61.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 10:57:32 +00:00
RPRXandGitHub 676a600edb Update github.com/xtls/reality to 20261014101318
https://github.com/XTLS/REALITY/commit/83f621788ca88b83179810b12baa7ae518aacb83
https://github.com/XTLS/REALITY/commit/14cefac9bd7a724a811b02759fa97aa86a1ec894
https://github.com/XTLS/REALITY/commit/3c98159dee388c2914a2b9e73ea57bc10023f9d3
2026-10-14 10:44:41 +00:00
173 changed files with 14019 additions and 2894 deletions
+5 -11
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty
@@ -40,21 +40,15 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image
FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray
VOLUME /usr/local/etc/xray
VOLUME /var/log/xray
+5 -11
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty
@@ -40,11 +40,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image
# Note on Distroless Base Image and Architecture Support:
# - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux:
@@ -60,10 +55,9 @@ FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray
VOLUME /usr/local/etc/xray
VOLUME /var/log/xray
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -40,7 +40,7 @@ jobs:
max_attempts: 60
command: |
[ -d 'resources' ] || mkdir resources
LIST=('Loyalsoldier v2ray-rules-dat geoip geoip' 'Loyalsoldier v2ray-rules-dat geosite geosite')
LIST=('XTLS Xray-rules-dat geoip/geoip-lite geoip' 'XTLS Xray-rules-dat geosite/geosite geosite')
for i in "${LIST[@]}"
do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}'))
+3 -18
View File
@@ -35,24 +35,7 @@ jobs:
if: steps.check-assets.outputs.missing == 'true'
run: sleep 90
check-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do
if ! cmp -s ref.txt <(head -n 4 "$file"); then
echo "Error: Header mismatch in $file"
head -n 4 "$file"
exit 1
fi
done
check-format:
check-format-and-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions:
@@ -68,6 +51,8 @@ jobs:
cache: false
- name: Check Format
run: go run ./infra/vformat/main.go -mode check -pwd ./
- name: Check Protobuf
run: go run ./infra/vprotogen/main.go -mode check -pwd ./
test:
needs: check-assets
+1 -1
View File
@@ -52,7 +52,7 @@ func luaServers(s *DNS) []luaDNSServer {
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
L.PreloadModule("xray.dns", func(L *lua.LState) int {
pushIPs := xlua.NewSlicePusher[net.IP](L)
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
serverList := L.CreateTable(len(servers), 0)
for i, client := range servers {
+1
View File
@@ -30,6 +30,7 @@ func newScriptEngine(script string) (*scriptEngine, error) {
func (e *scriptEngine) start(server *DNS) error {
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
scriptExecutionTimeout*20,
256,
func(L *lua.LState) {
geodata.RegisterLua(L)
log.RegisterLua(L)
+4 -4
View File
@@ -128,7 +128,7 @@ func TestDNSScriptGeoIPFallback(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
script := `
local servers = require("xray.dns").Servers
local us_ips = require("xray.geodata").BuildIPMatcher("geoip:us")
local ru_ips = require("xray.geodata").BuildIPMatcher("geoip:ru")
local by_id = {}
for _, server in ipairs(servers) do
@@ -138,7 +138,7 @@ assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are r
function HandleDNSQuery(domain, ipv4, ipv6, fake)
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
if not err and us_ips:AnyMatch(ips) then
if not err and ru_ips:AnyMatch(ips) then
return ips, ttl, nil
end
return by_id.fallback:Query(domain, ipv4, ipv6, fake)
@@ -152,7 +152,7 @@ end
primary := &scriptNameServer{
name: "primary",
answers: map[string]net.IP{
"us.example": net.ParseIP("2001:4860:4860::8888"),
"ru.example": net.ParseIP("2a02:6b8::feed:0ff"),
"other.example": net.ParseIP("127.0.0.1"),
},
ttl: 30,
@@ -191,7 +191,7 @@ end
ip net.IP
ttl uint32
}{
{"Us.Example.", net.ParseIP("2001:4860:4860::8888"), 30},
{"Ru.Example.", net.ParseIP("2a02:6b8::feed:0ff"), 30},
{"other.example", net.ParseIP("9.9.9.9"), 60},
} {
ips, ttl, err := server.LookupIP(tc.domain, option)
+45 -58
View File
@@ -21,7 +21,13 @@ import (
"golang.org/x/net/http2"
)
const idleTimeout = 30 * time.Second
const (
idleTimeout = 30 * time.Second
maxDownloadSize int64 = 256 * 1024 * 1024
)
// DO NOT EDIT. Ignored by geodata, rejected by Lua.
const luaGuardHeader = "\x12\x01\x00"
type stage struct {
target string
@@ -30,7 +36,6 @@ type stage struct {
type downloader struct {
ctx context.Context
httpClient *http.Client
httpsClient *http.Client
}
@@ -58,12 +63,11 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
httpsClient: newClient(ctx, dispatcher, outbound),
}
}
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client {
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client {
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn
err := task.Run(ctx, func() error {
@@ -88,53 +92,33 @@ func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound
Conn: conn,
}, nil
}
if isHTTPS {
return &http.Client{
Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
conn, err := dial(ctx, network, address)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(address)
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
return &http.Client{
Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
conn, err := dial(ctx, network, address)
if err != nil {
return nil, err
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
host, _, _ := net.SplitHostPort(address)
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
conn.Close()
return nil, err
}
return nil
return tlsConn, nil
},
}
} else {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: dial,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 20 {
return errors.New("stopped after 20 redirects")
}
return nil
},
}
}
@@ -170,6 +154,10 @@ func (d *downloader) downloadOne(asset *Asset) (stage, error) {
}
}()
if _, err := temp.WriteString(luaGuardHeader); err != nil {
temp.Close()
return stage{}, err
}
if err := d.fetch(asset.Url, temp); err != nil {
temp.Close()
return stage{}, err
@@ -196,27 +184,26 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
}
utils.TryDefaultHeadersWith(req.Header, "nav")
var client *http.Client
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
resp, err := d.httpsClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
io.Copy(io.Discard, resp.Body)
return errors.New("unexpected status code: ", resp.StatusCode)
}
if resp.ContentLength > maxDownloadSize {
return errors.New("geodata download exceeds 256 MiB")
}
n, err := io.Copy(writer, resp.Body)
n, err := io.Copy(writer, io.LimitReader(resp.Body, maxDownloadSize+1))
if err != nil {
return err
}
if n > maxDownloadSize {
return errors.New("geodata download exceeds 256 MiB")
}
if n == 0 {
return errors.New("empty response body")
}
+126
View File
@@ -0,0 +1,126 @@
package geodata
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
commongeodata "github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
"github.com/yuin/gopher-lua/parse"
"google.golang.org/protobuf/proto"
)
func TestDownloadOneLuaGuard(t *testing.T) {
for _, tc := range []struct {
name string
body string
code string
}{
{"geodata", "\x0a\x03\x0a\x01A", "A"},
{"prefixed geodata", luaGuardHeader + "\x0a\x03\x0a\x01A", "A"},
{"Lua", "function HandleRoute() end", ""},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil {
t.Fatal(err)
}
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(tc.body))
}))
defer server.Close()
d := &downloader{ctx: context.Background(), httpsClient: server.Client()}
staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"})
if err != nil {
t.Fatal(err)
}
tx, err := swapAll([]stage{staged})
if err != nil {
t.Fatal(err)
}
if err := tx.commit(); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(staged.target)
if err != nil || string(data) != luaGuardHeader+tc.body {
t.Fatalf("downloaded data = % x, %v", data, err)
}
program, err := xlua.Compile(staged.target)
var syntaxErr *parse.Error
if program != nil || !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
t.Fatalf("want Lua rejection at the first byte, got %v, %v", program, err)
}
if tc.code != "" {
var list commongeodata.GeoSiteList
if err := proto.Unmarshal(data, &list); err != nil {
t.Fatal(err)
}
if len(list.Entry) != 1 || list.Entry[0].Code != tc.code {
t.Fatalf("header changed geodata: %v", &list)
}
if _, err := commongeodata.ParseDomainRules([]string{"ext:" + filepath.Base(staged.target) + ":" + tc.code}, commongeodata.Domain_Full); err != nil {
t.Fatalf("geodata loader rejected the header: %v", err)
}
}
})
}
}
func TestProtobufConfigValidation(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
for _, name := range []string{"asset.dat", "asset.DAT"} {
if err := os.WriteFile(filepath.Join(dir, name), nil, 0o600); err != nil {
t.Fatal(err)
}
}
if err := os.Mkdir(filepath.Join(dir, "directory.dat"), 0o700); err != nil {
t.Fatal(err)
}
server, err := core.New(&core.Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close() })
for _, tc := range []struct {
name string
config *Config
wantError string
}{
{"minutely cron", &Config{Cron: "* * * * *"}, ""},
{"minimum interval", &Config{Cron: "@every 1m"}, ""},
{"short interval", &Config{Cron: "@every 59s"}, "interval must be at least 1 minute"},
{"fractional interval", &Config{Cron: "@every 59.999s"}, "interval must be at least 1 minute"},
{"invalid cron", &Config{Cron: "invalid"}, "invalid geodata cron"},
{"dat extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.dat"}}}, ""},
{"uppercase extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.DAT"}}}, ""},
{"invalid extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.lua"}}}, "extension must be .dat"},
{"missing extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset"}}}, "extension must be .dat"},
{"missing file", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "missing.dat"}}}, "invalid geodata asset file"},
{"directory", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "directory.dat"}}}, "asset is not a regular file"},
} {
t.Run(tc.name, func(t *testing.T) {
config, err := serial.ToTypedMessage(tc.config).GetInstance()
if err != nil {
t.Fatal(err)
}
_, err = core.CreateObject(server, config)
if tc.wantError == "" {
if err != nil {
t.Fatal(err)
}
} else if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("CreateObject() error = %v; want %q", err, tc.wantError)
}
})
}
}
+23 -3
View File
@@ -2,12 +2,16 @@ package geodata
import (
"context"
"path/filepath"
"strings"
"sync"
"time"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
commongeodata "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
)
@@ -25,6 +29,24 @@ func New(ctx context.Context, config *Config) (*Instance, error) {
if config.Cron == "" {
return &Instance{}, nil
}
if len(config.Assets) > 100 {
return nil, errors.New("geodata supports at most 100 assets")
}
for _, asset := range config.Assets {
if !strings.EqualFold(filepath.Ext(asset.GetFile()), ".dat") {
return nil, errors.New("geodata asset file extension must be .dat")
}
if _, err := filesystem.ResolveAsset(asset.GetFile()); err != nil {
return nil, errors.New("invalid geodata asset file: ", asset.GetFile()).Base(err)
}
}
schedule, err := cron.ParseStandard(config.Cron)
if err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
if interval, ok := schedule.(cron.ConstantDelaySchedule); ok && interval.Delay < time.Minute {
return nil, errors.New("geodata cron interval must be at least 1 minute")
}
g := &Instance{
assets: config.Assets,
@@ -44,9 +66,7 @@ func New(ctx context.Context, config *Config) (*Instance, error) {
cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)),
cron.WithLogger(cron.DiscardLogger),
)
if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
g.tasker.Schedule(schedule, cron.FuncJob(g.execute))
errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron)
return g, nil
+1 -1
View File
@@ -109,7 +109,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
stats.Min = 0
return stats
}
stats.Average = time.Duration(int(sum) / cnt)
stats.Average = sum / time.Duration(cnt)
var std float64
if cnt < 2 {
// no enough data for standard deviation, we assume it's half of the average rtt
+12 -12
View File
@@ -152,23 +152,23 @@ func (x *HealthPingMeasurementResult) GetMin() int64 {
type OutboundStatus struct {
state protoimpl.MessageState `protogen:"open.v1"`
// @Document Whether this outbound is usable
// @Restriction ReadOnlyForUser
//@Restriction ReadOnlyForUser
Alive bool `protobuf:"varint,1,opt,name=alive,proto3" json:"alive,omitempty"`
// @Document The time for probe request to finish.
// @Type time.ms
// @Restriction ReadOnlyForUser
//@Type time.ms
//@Restriction ReadOnlyForUser
Delay int64 `protobuf:"varint,2,opt,name=delay,proto3" json:"delay,omitempty"`
// @Document The last error caused this outbound failed to relay probe request
// @Restriction NotMachineReadable
//@Restriction NotMachineReadable
LastErrorReason string `protobuf:"bytes,3,opt,name=last_error_reason,json=lastErrorReason,proto3" json:"last_error_reason,omitempty"`
// @Document The outbound tag for this Server
// @Type id.outboundTag
//@Type id.outboundTag
OutboundTag string `protobuf:"bytes,4,opt,name=outbound_tag,json=outboundTag,proto3" json:"outbound_tag,omitempty"`
// @Document The time this outbound is known to be alive
// @Type id.outboundTag
//@Type id.outboundTag
LastSeenTime int64 `protobuf:"varint,5,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"`
// @Document The time this outbound is tried
// @Type id.outboundTag
//@Type id.outboundTag
LastTryTime int64 `protobuf:"varint,6,opt,name=last_try_time,json=lastTryTime,proto3" json:"last_try_time,omitempty"`
HealthPing *HealthPingMeasurementResult `protobuf:"bytes,7,opt,name=health_ping,json=healthPing,proto3" json:"health_ping,omitempty"`
unknownFields protoimpl.UnknownFields
@@ -257,14 +257,14 @@ func (x *OutboundStatus) GetHealthPing() *HealthPingMeasurementResult {
type ProbeResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
// @Document Whether this outbound is usable
// @Restriction ReadOnlyForUser
//@Restriction ReadOnlyForUser
Alive bool `protobuf:"varint,1,opt,name=alive,proto3" json:"alive,omitempty"`
// @Document The time for probe request to finish.
// @Type time.ms
// @Restriction ReadOnlyForUser
//@Type time.ms
//@Restriction ReadOnlyForUser
Delay int64 `protobuf:"varint,2,opt,name=delay,proto3" json:"delay,omitempty"`
// @Document The error caused this outbound failed to relay probe request
// @Restriction NotMachineReadable
//@Restriction NotMachineReadable
LastErrorReason string `protobuf:"bytes,3,opt,name=last_error_reason,json=lastErrorReason,proto3" json:"last_error_reason,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -324,7 +324,7 @@ func (x *ProbeResult) GetLastErrorReason() string {
type Intensity struct {
state protoimpl.MessageState `protogen:"open.v1"`
// @Document The time interval for a probe request in ms.
// @Type time.ms
//@Type time.ms
ProbeInterval uint32 `protobuf:"varint,1,opt,name=probe_interval,json=probeInterval,proto3" json:"probe_interval,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
+13 -12
View File
@@ -34,18 +34,19 @@ message RoutingContext {
// opened by xray-core.
// * FieldSelectors selects a subset of fields in routing statistics to return.
// Valid selectors:
// - inbound: Selects connection's inbound tag.
// - network: Selects connection's network.
// - ip: Equivalent as "ip_source" and "ip_target", selects both source and
// target IP.
// - port: Equivalent as "port_source" and "port_target", selects both source
// and target port.
// - domain: Selects target domain.
// - protocol: Select connection's protocol.
// - user: Select connection's inbound user email.
// - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags.
// - inbound: Selects connection's inbound tag.
// - network: Selects connection's network.
// - ip: Equivalent as "ip_source" and "ip_target", selects both source and
// target IP.
// - port: Equivalent as "port_source" and "port_target", selects both source
// and target port.
// - domain: Selects target domain.
// - protocol: Select connection's protocol.
// - user: Select connection's inbound user email.
// - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags.
//
// * If FieldSelectors is left empty, all fields will be returned.
message SubscribeRoutingStatsRequest {
repeated string FieldSelectors = 1;
+1 -1
View File
@@ -415,7 +415,7 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
func BenchmarkMultiGeoIPMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:ir", "geoip:ru"})
common.Must(err)
matcher, err := NewIPMatcher(rules, MatcherAsType_Target)
+1 -1
View File
@@ -62,7 +62,7 @@ func (r *Router) RegisterLua(L *lua.LState) {
}
func registerLuaContext(L *lua.LState) {
pushIPs := xlua.NewSlicePusher[net.IP](L)
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
attributes := L.NewTypeMetatable(luaAttributesType)
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
values := L.CheckUserData(1).Value.(map[string]string)
+1
View File
@@ -31,6 +31,7 @@ func newScriptEngine(script string) (*scriptEngine, error) {
func (e *scriptEngine) start(router *Router) error {
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
scriptExecutionTimeout*20,
256,
func(L *lua.LState) {
geodata.RegisterLua(L)
log.RegisterLua(L)
+59 -17
View File
@@ -8,23 +8,65 @@ import (
var privateIPMatcher = sync.OnceValue(func() IPMatcher {
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
// https://www.iana.org/assignments/iana-ipv4-special-registry
"0.0.0.0/8", // "This network"
"0.0.0.0/32", // "This host on this network"
"10.0.0.0/8", // Private-Use
"100.64.0.0/10", // Shared Address Space
"127.0.0.0/8", // Loopback
"169.254.0.0/16", // Link Local
"172.16.0.0/12", // Private-Use
// "192.0.0.0/24", // IETF Protocol Assignments
"192.0.0.0/29", // IPv4 Service Continuity Prefix
"192.0.0.8/32", // IPv4 dummy address
"192.0.0.9/32", // Port Control Protocol Anycast
"192.0.0.10/32", // Traversal Using Relays around NAT Anycast
"192.0.0.170/32", // NAT64/DNS64 Discovery
"192.0.0.171/32", // NAT64/DNS64 Discovery
"192.0.2.0/24", // Documentation (TEST-NET-1)
// "192.31.196.0/24", // AS112-v4
"192.52.193.0/24", // AMT
// "192.88.99.0/24", // Deprecated (6to4 Relay Anycast)
"192.88.99.2/32", // 6a44-relay anycast address
"192.168.0.0/16", // Private-Use
// "192.175.48.0/24", // Direct Delegation AS112 Service
"198.18.0.0/15", // Benchmarking
"198.51.100.0/24", // Documentation (TEST-NET-2)
"203.0.113.0/24", // Documentation (TEST-NET-3)
"240.0.0.0/4", // Reserved
"255.255.255.255/32", // Limited Broadcast
// https://www.iana.org/assignments/multicast-addresses
"224.0.0.0/4",
// https://www.iana.org/assignments/iana-ipv6-special-registry
"::1/128", // Loopback Address
"::/128", // Unspecified Address
// "::ffff:0:0/96", // IPv4-mapped Address
"64:ff9b::/96", // IPv4-IPv6 Translat.
"64:ff9b:1::/48", // IPv4-IPv6 Translat.
"100::/64", // Discard-Only Address Block
"100:0:0:1::/64", // Dummy IPv6 Prefix
// "2001::/23", // IETF Protocol Assignments
"2001::/32", // TEREDO
"2001:1::1/128", // Port Control Protocol Anycast
"2001:1::2/128", // Traversal Using Relays around NAT Anycast
"2001:1::3/128", // DNS-SD Service Registration Protocol Anycast
"2001:2::/48", // Benchmarking
"2001:3::/32", // AMT
// "2001:4:112::/48", // AS112-v6
// "2001:10::/28", // Deprecated (previously ORCHID)
"2001:20::/28", // ORCHIDv2
"2001:30::/28", // Drone Remote ID Protocol Entity Tags (DETs) Prefix
"2001:db8::/32", // Documentation
"2002::/16", // 6to4
// "2620:4f:8000::/48", // Direct Delegation AS112 Service
"3fff::/20", // Documentation
"5f00::/16", // Segment Routing (SRv6) SIDs
"fc00::/7", // Unique-Local
"fe80::/10", // Link-Local Unicast
// https://www.iana.org/assignments/ipv6-multicast-addresses
"ff00::/8",
}))))
})
+19 -14
View File
@@ -17,12 +17,12 @@ import (
)
func checkFile(file, code string) error {
r, err := filesystem.OpenAsset(file)
r, size, err := filesystem.OpenAsset(file)
if err != nil {
return errors.New("failed to open ", file).Base(err)
}
defer r.Close()
if _, err := find(r, []byte(code), false); err != nil {
if _, err := find(r, []byte(code), false, size); err != nil {
return errors.New("failed to check code ", code, " from ", file).Base(err)
}
return nil
@@ -30,12 +30,12 @@ func checkFile(file, code string) error {
func loadFile(file, code string) ([]byte, error) {
runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file)
r, size, err := filesystem.OpenAsset(file)
if err != nil {
return nil, errors.New("failed to open ", file).Base(err)
}
defer r.Close()
bs, err := find(r, []byte(code), true)
bs, err := find(r, []byte(code), true, size)
if err != nil {
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
}
@@ -60,13 +60,13 @@ func loadIP(file, code string) ([]*CIDR, error) {
// unmarshalling it into a []*Domain, so value is only valid during fn.
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file)
r, size, err := filesystem.OpenAsset(file)
if err != nil {
return errors.New("failed to open ", file).Base(err)
}
defer r.Close()
br := bufio.NewReaderSize(r, 64*1024)
n, err := seek(br, []byte(code))
n, err := seek(br, []byte(code), size)
if err != nil {
return errors.New("failed to load code ", code, " from ", file).Base(err)
}
@@ -114,6 +114,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
if err != nil {
return 0, err
}
if shift == 63 && b > 1 {
break // Only one value bit fits in the tenth byte.
}
x |= (uint64(b) & 0x7F) << shift
if (b & 0x80) == 0 {
return x, nil
@@ -123,9 +126,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
return 0, errors.New("varint overflow")
}
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
func find(r io.Reader, code []byte, readBody bool, size int64) ([]byte, error) {
br := bufio.NewReaderSize(r, 64*1024)
bodyL, err := seek(br, code)
bodyL, err := seek(br, code, size)
if err != nil || !readBody {
return nil, err
}
@@ -137,7 +140,7 @@ func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
}
// seek advances br to the body of the entry for code and returns the body length.
func seek(br *bufio.Reader, code []byte) (int, error) {
func seek(br *bufio.Reader, code []byte, size int64) (int, error) {
codeL := len(code)
if codeL == 0 {
return 0, errors.New("empty code")
@@ -145,7 +148,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
need := 2 + codeL // TODO: if code too long
for {
if _, err := br.ReadByte(); err != nil {
tag, err := br.ReadByte()
if err != nil {
return 0, err
}
@@ -153,10 +157,10 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
if err != nil {
return 0, err
}
bodyL := int(x)
if bodyL <= 0 {
return 0, errors.New("invalid body length: ", bodyL)
if x == 0 || x > uint64(size) || x > uint64(^uint(0)>>1) {
return 0, errors.New("invalid body length: ", x)
}
bodyL := int(x)
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single
// length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does.
@@ -167,7 +171,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
}
return 0, err
}
if bodyL >= need && len(prefix) >= need && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
if tag == 0x0a && bodyL >= need && len(prefix) >= need &&
prefix[0] == 0x0a && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
return bodyL, nil
}
if _, err := br.Discard(bodyL); err != nil {
+53
View File
@@ -1,6 +1,7 @@
package geodata
import (
"bytes"
"fmt"
"os"
"path/filepath"
@@ -12,6 +13,58 @@ import (
"google.golang.org/protobuf/proto"
)
func TestFindChecksEntryAndCodeTags(t *testing.T) {
for _, payload := range [][]byte{
{0x12, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as entry.
{0x08, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Wrong entry wire type.
{0x0a, 6, 0x1a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as code.
{0x0a, 6, 0x08, 4, 'T', 'E', 'S', 'T'}, // Wrong code wire type.
} {
if _, err := find(bytes.NewReader(payload), []byte("TEST"), true, int64(len(payload))); err == nil {
t.Fatalf("matched incorrect entry/code tags in %x", payload)
}
}
}
func TestLoadFileRejectsVarintOverflow(t *testing.T) {
payload := []byte{
0x0a,
0x86, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02, // 2^64 + 6
0x0a, 0x04, 'T', 'E', 'S', 'T',
}
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
if err := os.WriteFile(filepath.Join(dir, "overflow.dat"), payload, 0o600); err != nil {
t.Fatal(err)
}
if _, err := loadFile("overflow.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "varint overflow") {
t.Fatalf("want varint overflow, got %v", err)
}
}
func TestLoadFileRejectsOversizedEntry(t *testing.T) {
payload := []byte{
0x0a, 0x03, 0x0a, 0x01, 'A', // A valid entry before the oversized one.
0x0a, 0x80, 0x80, 0x80, 0x80, 0x04, // Entry length: 1 GiB.
0x0a, 0x04, 'T', 'E', 'S', 'T',
}
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
if err := os.WriteFile(filepath.Join(dir, "oversized.dat"), payload, 0o600); err != nil {
t.Fatal(err)
}
// Check without allocating the body first, so a regression fails safely.
if err := checkFile("oversized.dat", "TEST"); err == nil {
t.Fatal("accepted an entry length larger than the file")
}
if _, err := loadFile("oversized.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "invalid body length") {
t.Fatalf("want invalid body length, got %v", err)
}
if err := loadSite("oversized.dat", "TEST", "", func(Domain_Type, []byte) {}); err == nil || !strings.Contains(err.Error(), "invalid body length") {
t.Fatalf("want invalid body length, got %v", err)
}
}
type siteEntry struct {
Type Domain_Type
Value string
+7 -7
View File
@@ -400,13 +400,13 @@ func TestIPMatcher4CN(t *testing.T) {
}
}
func TestIPMatcher6US(t *testing.T) {
func TestIPMatcher6RU(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:us")
matcher := buildIPMatcher("geoip:ru")
if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
if !matcher.Match(xnet.ParseAddress("2a02:6b8::feed:0ff").IP()) {
t.Error("expect RU geoip contain 2a02:6b8::feed:0ff, but actually not")
}
}
@@ -423,11 +423,11 @@ func BenchmarkIPMatcher4CN(b *testing.B) {
}
}
func BenchmarkIPMatcher6US(b *testing.B) {
func BenchmarkIPMatcher6RU(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:us")
ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
matcher := buildIPMatcher("geoip:ru")
ip := xnet.ParseAddress("2a02:6b8::feed:0ff").IP()
b.ResetTimer()
+2 -2
View File
@@ -44,8 +44,8 @@ func RegisterLua(L *lua.LState) {
"Match": luaIPMatch,
"AnyMatch": luaIPAnyMatch,
"Matches": luaIPMatches,
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[net.IP](L)),
})
"FilterIPs": newLuaIPFilterIPs(xlua.NewReadOnlySlicePusher[net.IP](L)),
}, "SetReverse", "ToggleReverse")
return 1
}))
+6
View File
@@ -78,6 +78,7 @@ local geodata = require("xray.geodata")
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
assert(matcher.Reload == nil and matcher.reload == nil)
assert(matcher:match(ip))
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
assert(not matcher:AnyMatch(nil))
@@ -97,6 +98,11 @@ assert(matcher.missing == nil)
local domain = geodata.BuildDomainMatcher("full:example.com")
assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny)
assert(domain.Reload == nil and domain.reload == nil)
for _, value in ipairs({matcher, domain}) do
assert(not pcall(function() return -value end))
assert(not pcall(function() return value ^ {} end))
end
assert(domain:matchAny("example.com"))
assert(#domain:Match("example.com") == 1)
assert(domain:match("example.com")[1] == 0)
+3 -3
View File
@@ -11,7 +11,7 @@ func TestParseIPRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{
"geoip:us",
"geoip:ru",
"geoip:cn",
"!geoip:cn",
"!!geoip:cn",
@@ -20,9 +20,9 @@ func TestParseIPRules(t *testing.T) {
"!geoip:!cn",
"ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn",
"ext:geoip.dat:ca",
"ext:geoip.dat:ir",
"ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ca",
"ext-ip:geoip.dat:!ir",
"192.168.0.0/24",
"!192.168.0.0/24",
"!!192.168.0.0/24",
@@ -0,0 +1,19 @@
// DO NOT EDIT. Regression test for app/geodata's luaGuardHeader.
package lua
import (
"errors"
"strings"
"testing"
"github.com/yuin/gopher-lua/parse"
)
func TestGeodataLuaGuard(t *testing.T) {
_, err := parse.Parse(strings.NewReader("\x12\x01\x00function HandleRoute() end"), "geodata")
var syntaxErr *parse.Error
if !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
t.Fatalf("want Lua rejection at the first byte, got %v", err)
}
}
+95 -4
View File
@@ -1,6 +1,9 @@
package lua
import (
"net"
"reflect"
glua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
)
@@ -9,7 +12,7 @@ import (
// The returned function wraps slices without reflection or metatable lookup,
// and pushes nil for nil slices. Use it with this state or its coroutines.
func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
metatable := luar.New(L, []T{}).(*glua.LUserData).Metatable
metatable := luar.MT(L, []T(nil)).LTable
return func(L *glua.LState, values []T) {
if values == nil {
L.Push(glua.LNil)
@@ -22,21 +25,105 @@ func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
}
}
// NewReadOnlySlicePusher borrows slices without copying and recursively makes
// nested slice types read-only through their shared metatables. IP byte views
// and masks are also read-only. Other referenced elements retain their mutability.
// Element bindings are initialized on first indexing or iteration.
func NewReadOnlySlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
sliceMetatable := luar.MT(L, []T{})
if sliceMetatable.RawGetString("__readonly") != glua.LTrue {
readOnly := L.NewFunction(func(L *glua.LState) int {
L.RaiseError("slice is read-only")
return 0
})
setSliceReadOnly[[]T](L, readOnly)
}
return NewSlicePusher[T](L)
}
func setSliceReadOnly[S ~[]E, E any](L *glua.LState, readOnly *glua.LFunction) {
var visit func(reflect.Type)
visit = func(typ reflect.Type) {
if typ.Kind() != reflect.Slice {
return
}
metatable := luar.MT(L, reflect.Zero(typ).Interface())
if metatable.RawGetString("__readonly") == glua.LTrue {
return
}
// Mark before descending because named slice types can be recursive.
metatable.RawSetString("__readonly", glua.LTrue)
metatable.RawSetString("__newindex", readOnly)
// Reject explicitly so Lua cannot fall back to the other operand.
metatable.RawSetString("__add", readOnly)
if typ == reflect.TypeFor[net.IP]() {
// A nil receiver lets AppendText return its argument as a writable []byte.
methods := metatable.RawGetString("methods").(*glua.LTable)
appendText := methods.RawGetString("AppendText")
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
if method == appendText {
methods.RawSet(key, glua.LNil)
}
}
visit(reflect.TypeFor[net.IPMask]())
}
if element := typ.Elem(); element.Kind() == reflect.Slice {
index := metatable.RawGetString("__index").(*glua.LFunction)
call := metatable.RawGetString("__call").(*glua.LFunction)
initialize := func() {
visit(element)
metatable.RawSetString("__index", index)
metatable.RawSetString("__call", call)
}
metatable.RawSetString("__index", L.NewFunction(func(L *glua.LState) int {
initialize()
return index.GFunction(L)
}))
metatable.RawSetString("__call", L.NewFunction(func(L *glua.LState) int {
initialize()
return call.GFunction(L)
}))
}
}
visit(reflect.TypeFor[S]())
}
// DirectMethod handles a Lua call without luar's reflected method invocation.
// It returns the result count and whether it handled the arguments. On false,
// it must leave the stack unchanged for the original luar wrapper.
type DirectMethod func(L *glua.LState) (nresults int, handled bool)
// PushMethods pushes a luar userdata exposing only the named Go methods and
// their aliases through reflected calls. The method list is fixed by the first
// binding of that Go type in each LState.
func PushMethods(L *glua.LState, value any, methods ...string) {
PushWithDirectMethods(L, value, nil, methods...)
}
// PushWithDirectMethods pushes a luar userdata with typed Go method bindings.
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
// uses the methods table directly instead of luar's reflected __index handler.
// value must expose methods only. Bindings and their closures are installed once
// per Go type per LState, outside the method-call hot path.
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod) {
// Only directMethods and additionalMethods are exposed, including their aliases.
// Bindings and their closures are installed once per Go type per LState, outside
// the method-call hot path.
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod, additionalMethods ...string) {
userdata := luar.New(L, value).(*glua.LUserData)
metatable := userdata.Metatable.(*glua.LTable)
methods := metatable.RawGetString("methods").(*glua.LTable)
if metatable.RawGetString("__index") != methods {
// Keep all aliases for the explicitly exposed methods.
exposed := make(map[glua.LValue]bool, len(directMethods)+len(additionalMethods))
for name := range directMethods {
exposed[methods.RawGetString(name)] = true
}
for _, name := range additionalMethods {
exposed[methods.RawGetString(name)] = true
}
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
if !exposed[method] {
methods.RawSet(key, glua.LNil)
}
}
for name, direct := range directMethods {
original := methods.RawGetString(name)
fn := L.NewFunction(func(L *glua.LState) int {
@@ -52,6 +139,10 @@ func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]D
}
}
}
// Pointer operations would bypass the method-only API.
metatable.RawSetString("__newindex", glua.LNil)
metatable.RawSetString("__pow", glua.LNil)
metatable.RawSetString("__unm", glua.LNil)
metatable.RawSetString("__index", methods)
}
L.Push(userdata)
+183
View File
@@ -2,6 +2,7 @@ package lua
import (
"net"
"reflect"
"testing"
glua "github.com/yuin/gopher-lua"
@@ -47,6 +48,188 @@ assert(ok and result == true)
}
}
func TestReadOnlySlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
values := []int{3, 5}
NewReadOnlySlicePusher[int](L)(L, values)
L.SetGlobal("values", L.Get(-1))
L.Pop(1)
if err := L.DoString(`
assert(#values == 2 and values[1] == 3 and values[2] == 5)
assert(not pcall(function() values[1] = 7 end))
assert(not pcall(function() return values + 7 end))
local sum = 0
for _, value in values() do sum = sum + value end
assert(sum == 8)
`); err != nil {
t.Fatal(err)
}
got := L.GetGlobal("values").(*glua.LUserData).Value.([]int)
if &got[0] != &values[0] || values[0] != 3 || values[1] != 5 {
t.Fatal("read-only binding copied or modified the slice")
}
}
func TestReadOnlyIPSlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
ips := []net.IP{{192, 0, 2, 1}, net.ParseIP("2001:db8::1")}
NewReadOnlySlicePusher[net.IP](L)(L, ips)
L.SetGlobal("ips", L.Get(-1))
L.Pop(1)
readOnly := luar.MT(L, ips).RawGetString("__newindex")
NewReadOnlySlicePusher[net.IP](L)
if luar.MT(L, ips).RawGetString("__newindex") != readOnly {
t.Fatal("IP metatables were initialized more than once")
}
if err := L.DoString(`
local function readOnly(value)
assert(not pcall(function() value[1] = value[1] end))
assert(not pcall(function() return value + value[1] end))
end
assert(#ips == 2 and ips[1]:String() == "192.0.2.1")
readOnly(ips)
for _, ip in ips() do
readOnly(ip)
readOnly(ip:To16())
if ip:To4() then
readOnly(ip:To4())
readOnly(ip:DefaultMask())
end
assert(ip:Equal(ip:To16()) and type(ip:String()) == "string")
assert(ip.AppendText == nil and ip.appendText == nil)
local text, err = ip:MarshalText()
assert(not err)
text[1] = 88 -- Independently owned byte buffers remain writable.
assert(not pcall(function() return ips + text end))
end
`); err != nil {
t.Fatal(err)
}
got := L.GetGlobal("ips").(*glua.LUserData).Value.([]net.IP)
if &got[0] != &ips[0] || &got[0][0] != &ips[0][0] || &got[1][0] != &ips[1][0] {
t.Fatal("read-only binding copied the IP data")
}
for i, want := range []string{"192.0.2.1", "2001:db8::1"} {
if got[i].String() != want {
t.Fatalf("IP %d changed to %s; want %s", i, got[i], want)
}
}
}
func TestReadOnlyIPSlicePusherLazy(t *testing.T) {
for _, tc := range []struct{ name, access string }{
{"index", `local ip = ips[1]`},
{"iterate", `local _, ip = ips()()`},
{"coroutine", `local co = coroutine.create(function() return ips[1] end); local ok, ip = coroutine.resume(co); assert(ok)`},
} {
t.Run(tc.name, func(t *testing.T) {
L := glua.NewState()
defer L.Close()
methods := 0
luar.GetConfig(L).MethodNames = func(_ reflect.Type, method reflect.Method) []string {
methods++
return []string{method.Name}
}
NewReadOnlySlicePusher[net.IP](L)(L, []net.IP{{192, 0, 2, 1}})
L.SetGlobal("ips", L.Get(-1))
L.Pop(1)
if err := L.DoString(`assert(#ips == 1); assert(not pcall(function() ips[1] = nil end))`); err != nil {
t.Fatal(err)
}
if methods != 0 {
t.Fatal("IP methods initialized without reading an element")
}
source := tc.access + `
assert(ip:String() == "192.0.2.1" and ip.AppendText == nil)
assert(not pcall(function() ip[1] = 0 end))
assert(not pcall(function() ip:To4()[1] = 0 end))
assert(not pcall(function() ip:To16()[1] = 0 end))
assert(not pcall(function() ip:DefaultMask()[1] = 0 end))
`
if err := L.DoString(source); err != nil {
t.Fatal(err)
}
initialized := methods
if initialized == 0 {
t.Fatal("IP access did not initialize its methods")
}
if err := L.DoString(source); err != nil {
t.Fatal(err)
}
if methods != initialized {
t.Fatal("IP methods initialized more than once")
}
})
}
}
func TestReadOnlyNestedSlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
values := [][][]byte{{{3, 5}}}
NewReadOnlySlicePusher[[][]byte](L)(L, values)
L.SetGlobal("values", L.Get(-1))
L.Pop(1)
ips := [][]net.IP{{{192, 0, 2, 1}}}
NewReadOnlySlicePusher[[]net.IP](L)(L, ips)
L.SetGlobal("ips", L.Get(-1))
L.Pop(1)
if err := L.DoString(`
local function readOnly(value)
assert(not pcall(function() value[1] = value[1] end))
assert(not pcall(function() return value + value[1] end))
end
assert(values[1][1][1] == 3 and values[1][1][2] == 5)
readOnly(values)
for _, group in values() do
readOnly(group)
for _, bytes in group() do readOnly(bytes) end
end
readOnly(ips)
readOnly(ips[1])
local ip = ips[1][1]
assert(ip:String() == "192.0.2.1")
readOnly(ip)
readOnly(ip:To4())
readOnly(ip:To16())
readOnly(ip:DefaultMask())
assert(ip.AppendText == nil and ip.appendText == nil)
`); err != nil {
t.Fatal(err)
}
readOnly := luar.MT(L, ips[0]).RawGetString("__newindex")
NewReadOnlySlicePusher[net.IP](L)
if luar.MT(L, ips[0]).RawGetString("__newindex") != readOnly {
t.Fatal("nested slice metatable was initialized again")
}
got := L.GetGlobal("values").(*glua.LUserData).Value.([][][]byte)
if &got[0][0][0] != &values[0][0][0] || values[0][0][0] != 3 || values[0][0][1] != 5 {
t.Fatal("read-only binding copied or modified the nested slice")
}
}
type recursiveReadOnlySlice []recursiveReadOnlySlice
func TestReadOnlyRecursiveSlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
values := make(recursiveReadOnlySlice, 1)
values[0] = values
NewReadOnlySlicePusher[recursiveReadOnlySlice](L)(L, values)
L.SetGlobal("values", L.Get(-1))
L.Pop(1)
if err := L.DoString(`
local nested = values[1][1][1]
assert(#nested == 1)
assert(not pcall(function() nested[1] = nested end))
assert(not pcall(function() return nested + nested end))
`); err != nil {
t.Fatal(err)
}
}
func TestSlicePusherMetatablePerState(t *testing.T) {
first := glua.NewState()
defer first.Close()
+117
View File
@@ -0,0 +1,117 @@
package lua
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"sync"
"github.com/xtls/xray-core/common/errors"
)
var privilegedKey struct {
sync.Once
publicKey ed25519.PublicKey
}
// SetPrivilegedPublicKey configures the trusted Ed25519 public key once at
// startup, before compiling scripts. An empty value disables authorization.
// Nonempty values must be unpadded Base64URL encodings of exactly 32 bytes.
func SetPrivilegedPublicKey(value string) error {
var publicKey ed25519.PublicKey
if value != "" {
decoded, err := decodePrivilegedValue(value, ed25519.PublicKeySize)
if err != nil {
return errors.New("invalid Lua privileged public key").Base(err)
}
publicKey = ed25519.PublicKey(decoded)
}
privilegedKey.Do(func() {
privilegedKey.publicKey = publicKey
})
return nil
}
func decodePrivilegedValue(value string, size int) ([]byte, error) {
encoding := base64.RawURLEncoding.Strict()
if len(value) != encoding.EncodedLen(size) {
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
}
decoded, err := encoding.DecodeString(value)
if err != nil || len(decoded) != size || encoding.EncodeToString(decoded) != value {
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
}
return decoded, nil
}
func verifyPrivilegedSignature(code []byte) (bool, error) {
_, end, value, found := privilegedSignatureLine(code)
if !found {
return false, nil
}
publicKey := privilegedKey.publicKey
if len(publicKey) == 0 {
return false, errors.New("Lua privileged signature requires --lua-privileged-pubkey")
}
signature, err := decodePrivilegedValue(string(bytes.TrimSpace(value)), ed25519.SignatureSize)
if err != nil {
return false, errors.New("invalid Lua privileged signature").Base(err)
}
digest := sha256.Sum256(bytes.TrimSpace(code[end:]))
if !ed25519.Verify(publicKey, digest[:], signature) {
return false, errors.New("Lua privileged signature verification failed")
}
return true, nil
}
// SignPrivilegedScript signs the script body, then prepends or replaces the signature line.
func SignPrivilegedScript(code []byte, seed string) ([]byte, error) {
decoded, err := decodePrivilegedValue(seed, ed25519.SeedSize)
if err != nil {
return nil, errors.New("invalid Ed25519 private key seed").Base(err)
}
start, end, _, found := privilegedSignatureLine(code)
body := code
if found {
body = code[end:]
}
digest := sha256.Sum256(bytes.TrimSpace(body))
signature := ed25519.Sign(ed25519.NewKeyFromSeed(decoded), digest[:])
header := []byte("-- @xray:privileged-signature=" + base64.RawURLEncoding.EncodeToString(signature))
if !found {
if newline := bytes.IndexByte(code, '\n'); newline > 0 && code[newline-1] == '\r' {
header = append(header, '\r')
}
header = append(header, '\n')
return append(header, code...), nil
}
signed := make([]byte, 0, start+len(header)+len(code)-end)
signed = append(signed, code[:start]...)
signed = append(signed, header...)
return append(signed, code[end:]...), nil
}
func privilegedSignatureLine(code []byte) (start, end int, value []byte, found bool) {
for rest := code; len(rest) != 0; {
start = len(code) - len(rest)
var line []byte
line, rest, _ = bytes.Cut(rest, []byte{'\n'})
line = bytes.TrimSuffix(line, []byte{'\r'})
if len(bytes.TrimSpace(line)) == 0 {
continue
}
end = start + len(line)
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
if !found {
return 0, 0, nil, false
}
value, found = bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
return start, end, value, found
}
return 0, 0, nil, false
}
+85
View File
@@ -0,0 +1,85 @@
package lua
// TODO: Re-enable these tests and their helper when Compile supports privileged signature authorization again.
/*
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/hex"
"strings"
"testing"
)
func privilegedSeed(t *testing.T) string {
t.Helper()
seed, err := hex.DecodeString(privilegedTestSeed)
if err != nil {
t.Fatal(err)
}
return base64.RawURLEncoding.EncodeToString(seed)
}
func TestSignPrivilegedScriptPreservesSource(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
seed := privilegedSeed(t)
for _, tc := range []struct {
name, prefix, header, body, newline string
}{
{name: "unsigned", body: privilegedFixtureBody, newline: "\n"},
{name: "unsigned boundary whitespace", body: " \t\n\n" + privilegedFixtureBody + " \t\n", newline: "\n"},
{name: "unsigned CRLF", body: strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n"), newline: "\r\n"},
{name: "replace invalid header", header: privilegedSignatureHeader + "%", body: "\n" + privilegedFixtureBody, newline: "\n"},
{name: "replace indented header", prefix: " \t\r\n\n", header: " \t--\t @xray:privileged-signature=% \t", body: "\r\n \t\r\n" + privilegedFixtureBody + "\r\n", newline: "\r\n"},
{name: "replace valid header", header: privilegedSignatureHeader + privilegedFixtureSignature, body: "\n" + privilegedFixtureBody, newline: "\n"},
{name: "empty script", newline: "\n"},
{name: "header without newline", header: privilegedSignatureHeader + "%"},
{name: "ordinary comment before marker", body: "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", newline: "\n"},
} {
t.Run(tc.name, func(t *testing.T) {
code := []byte(tc.prefix + tc.header + tc.body)
original := bytes.Clone(code)
signed, err := SignPrivilegedScript(code, seed)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(code, original) {
t.Fatal("signing modified the input buffer")
}
body := tc.body
if tc.header != "" {
body = strings.TrimPrefix(body, tc.newline)
}
header := privilegedSignatureHeader + testSignature(t, body)
want := header + tc.newline + tc.body
if tc.header != "" {
want = tc.prefix + header + tc.body
}
if string(signed) != want {
t.Fatalf("signed source = %q, want %q", signed, want)
}
program, err := Compile(writeTestScript(t, string(signed)))
if err != nil || program == nil || !program.privileged {
t.Fatalf("signed source Compile = %v, %v; want privileged program", program, err)
}
again, err := SignPrivilegedScript(signed, seed)
if err != nil || !bytes.Equal(again, signed) {
t.Fatalf("resigning changed the source: %q, %v", again, err)
}
})
}
}
func TestSignPrivilegedScriptRejectsInvalidSeed(t *testing.T) {
seed := privilegedSeed(t)
for _, value := range []string{
"", "%", seed + "=",
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SeedSize-1)),
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PrivateKeySize)),
} {
if signed, err := SignPrivilegedScript([]byte(privilegedFixtureBody), value); signed != nil || err == nil {
t.Fatalf("SignPrivilegedScript with invalid seed = %q, %v; want rejection", signed, err)
}
}
}
*/
+181 -8
View File
@@ -3,19 +3,23 @@ package lua
import (
"bytes"
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
glua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"
)
// Program holds immutable bytecode that can be run by independent LStates.
type Program struct {
proto *glua.FunctionProto
proto *glua.FunctionProto
privileged bool
}
// LStateFactory returns a fully initialized state or nil and an error.
@@ -27,10 +31,17 @@ func Compile(script string) (*Program, error) {
var code []byte
var err error
name := "[inline]"
privileged := false
if strings.HasPrefix(script, base64.GzipPrefix) || strings.HasPrefix(script, base64.Prefix) {
// TODO: Restore inline script support and re-enable the related tests.
return nil, errors.New("currently unavailable")
code, err = base64.Decode(script)
} else if name, err = platform.ResolveLuaFile(script); err == nil {
} else if name, err = filesystem.ResolveLuaFile(script); err == nil {
code, err = os.ReadFile(name)
// TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries.
// if err == nil {
// privileged, err = verifyPrivilegedSignature(code)
// }
}
if err != nil {
return nil, err
@@ -44,14 +55,26 @@ func Compile(script string) (*Program, error) {
if err != nil {
return nil, err
}
return &Program{proto: proto}, nil
return &Program{proto: proto, privileged: privileged}, nil
}
// NewState creates a state, runs register, executes the program under ctx, and
// runs validate. It removes the initialization context before returning a state
// owned by the caller.
func (p *Program) NewState(ctx context.Context, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
L := glua.NewState()
// Choose registrySize for the script type's expected stack usage, balancing
// per-state memory usage against the number of states retained in a pool.
func (p *Program) NewState(ctx context.Context, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
options := glua.Options{
RegistrySize: registrySize,
RegistryMaxSize: glua.RegistrySize,
RegistryGrowStep: registrySize,
}
var L *glua.LState
if p.privileged {
L = glua.NewState(options)
} else {
L = newSandboxedState(options)
}
valid := false
defer func() {
if !valid {
@@ -63,6 +86,10 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
if register != nil {
register(L)
}
if !p.privileged {
// Hide package internals to prevent data snooping and sandbox escape.
L.SetGlobal(glua.LoadLibName, glua.LNil)
}
L.Push(L.NewFunctionFromProto(p.proto))
// Execute the Lua script's top level.
if err := L.PCall(0, 0, nil); err != nil {
@@ -77,11 +104,157 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
return L, nil
}
func newSandboxedState(options glua.Options) *glua.LState {
options.SkipOpenLibs = true
L := glua.NewState(options)
for _, lib := range []struct {
name string
open glua.LGFunction
}{
{glua.LoadLibName, glua.OpenPackage},
{glua.BaseLibName, glua.OpenBase},
{glua.TabLibName, glua.OpenTable},
{glua.StringLibName, glua.OpenString},
{glua.MathLibName, glua.OpenMath},
{glua.OsLibName, glua.OpenOs},
} {
L.Push(L.NewFunction(lib.open))
L.Push(glua.LString(lib.name))
L.Call(1, 0)
}
for _, name := range []string{
"dofile", "loadfile", "load", "loadstring", "getfenv", "setfenv",
"getmetatable", "setmetatable", "rawget", "rawset",
"collectgarbage", "module", "newproxy", "print", "_printregs",
} {
L.SetGlobal(name, glua.LNil)
}
// Prevent printing memory addresses that reveal the memory layout.
toString := func(value glua.LValue) glua.LString {
result := "<opaque>"
switch value.Type() {
case glua.LTNil, glua.LTBool, glua.LTNumber, glua.LTString:
result = value.String()
case glua.LTUserData:
switch native := value.(*glua.LUserData).Value.(type) {
case error: // Errors can also implement fmt.Stringer; redact them first.
result = redactMemoryAddresses(native.Error())
case fmt.Stringer:
result = native.String()
}
}
return glua.LString(result)
}
L.SetGlobal("tostring", L.NewFunction(func(L *glua.LState) int {
L.Push(toString(L.CheckAny(1)))
return 1
}))
// Prevent printing memory addresses that reveal the memory layout.
stringLib := L.GetGlobal(glua.StringLibName).(*glua.LTable)
stringLib.RawSetString("dump", glua.LNil)
format := stringLib.RawGetString("format")
stringLib.RawSetString("format", L.NewFunction(func(L *glua.LState) int {
nargs := L.GetTop()
for i := 2; i <= nargs; i++ {
value := L.Get(i)
switch value.Type() {
case glua.LTBool, glua.LTNumber, glua.LTString:
continue
}
L.Replace(i, toString(value))
}
L.Insert(format, 1)
L.Call(nargs, 1)
return 1
}))
// Redact addresses before caught errors become readable by the script.
redactError := func(value glua.LValue) glua.LValue {
if message, ok := value.(glua.LString); ok {
if redacted := redactMemoryAddresses(string(message)); redacted != string(message) {
return glua.LString(redacted)
}
}
return value
}
for _, name := range []string{"pcall", "xpcall"} {
call := L.GetGlobal(name)
L.SetGlobal(name, L.NewFunction(func(L *glua.LState) int {
nargs := L.GetTop()
if name == "xpcall" {
L.CheckFunction(1)
handler := L.CheckFunction(2)
L.Replace(2, L.NewFunction(func(L *glua.LState) int {
L.Replace(1, redactError(L.Get(1)))
L.Insert(handler, 1)
L.Call(1, 1)
return 1
}))
}
L.Insert(call, 1)
L.Call(nargs, glua.MultRet)
if L.Get(1) == glua.LFalse {
L.Replace(2, redactError(L.Get(2)))
}
return L.GetTop()
}))
}
// Prevent seed output from compromising cryptographic security.
L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil)
// Keep only time functions, including when os is accessed through require.
osLib := L.GetGlobal(glua.OsLibName).(*glua.LTable)
for key, _ := osLib.Next(glua.LNil); key != glua.LNil; key, _ = osLib.Next(key) {
switch key {
case glua.LString("clock"), glua.LString("date"), glua.LString("difftime"), glua.LString("time"):
default:
osLib.RawSet(key, glua.LNil)
}
}
// Prevent module injection through the global package table.
module := L.GetGlobal(glua.LoadLibName).(*glua.LTable)
preload := module.RawGetString("preload").(*glua.LTable)
loaders := L.CreateTable(1, 0)
loaders.RawSetInt(1, L.NewFunction(func(L *glua.LState) int {
name := L.CheckString(1)
loader := preload.RawGetString(name)
if loader == glua.LNil {
loader = glua.LString("no preloaded module: " + name)
}
L.Push(loader)
return 1
}))
module.RawSetString("loaders", loaders)
L.SetField(L.Get(glua.RegistryIndex), "_LOADERS", loaders)
for _, name := range []string{"loadlib", "seeall", "path", "cpath", "config"} {
module.RawSetString(name, glua.LNil)
}
return L
}
var memoryAddressPattern = regexp.MustCompile(`0[xX][0-9a-fA-F]+`)
func redactMemoryAddresses(message string) string {
if !strings.Contains(message, "0x") && !strings.Contains(message, "0X") {
return message
}
return memoryAddressPattern.ReplaceAllString(message, "<address>")
}
// NewStateFactory returns a factory that gives each state an initialization timeout.
func (p *Program) NewStateFactory(initTimeout time.Duration, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
// Choose registrySize for the script type's expected stack usage, balancing
// per-state memory usage against the number of states retained in a pool.
func (p *Program) NewStateFactory(initTimeout time.Duration, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
return func(ctx context.Context) (*glua.LState, error) {
initCtx, cancel := context.WithTimeout(ctx, initTimeout)
defer cancel()
return p.NewState(initCtx, register, validate)
return p.NewState(initCtx, registrySize, register, validate)
}
}
File diff suppressed because it is too large Load Diff
+55 -44
View File
@@ -4,7 +4,7 @@ package net
import (
"bufio"
"encoding/hex"
"encoding/binary"
"fmt"
"net"
"os"
@@ -26,36 +26,38 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.")
}
var procFile string
var procFiles []string
switch network {
case "tcp":
if net.ParseIP(srcIP).To4() != nil {
procFile = "/proc/net/tcp"
procFiles = []string{"/proc/net/tcp"}
} else {
procFile = "/proc/net/tcp6"
procFiles = []string{"/proc/net/tcp6"}
}
case "udp":
if net.ParseIP(srcIP).To4() != nil {
procFile = "/proc/net/udp"
procFiles = []string{"/proc/net/udp", "/proc/net/udp6"}
} else {
procFile = "/proc/net/udp6"
procFiles = []string{"/proc/net/udp6", "/proc/net/udp"}
}
default:
panic("Unsupported network type for process lookup.")
}
targetHexAddr, err := formatLittleEndianString(net.ParseIP(srcIP), Port(srcPort))
if err != nil {
return 0, "", "", errors.New("failed to format address: ", err)
targetHexAddr := formatProcNetAddress(net.ParseIP(srcIP), Port(srcPort))
portFallback := ""
if network == "udp" {
portFallback = fmt.Sprintf(":%04X", srcPort)
}
inode, err := findInodeInFile(procFile, targetHexAddr)
inode, err := findInodeInFiles(procFiles, targetHexAddr, portFallback)
if err != nil {
return 0, "", "", errors.New("could not search in ", procFile).Base(err)
return 0, "", "", errors.New("could not search in ", strings.Join(procFiles, " and ")).Base(err)
}
if inode == "" {
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile)
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", strings.Join(procFiles, " or "))
}
pidStr, err := findPidByInode(inode)
@@ -82,51 +84,60 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return pid, procName, absPath, nil
}
func formatLittleEndianString(addr net.IP, port Port) (string, error) {
ip := addr
func formatProcNetAddress(addr net.IP, port Port) string {
var ipBytes []byte
if ip.To4() != nil {
ipBytes = ip.To4()
if ipv4 := addr.To4(); ipv4 != nil {
ipBytes = ipv4
} else {
ipBytes = ip.To16()
}
if ipBytes == nil {
return "", errors.New("invalid IP format for ", addr, ": ", ip)
ipBytes = addr.To16()
}
for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 {
ipBytes[i], ipBytes[j] = ipBytes[j], ipBytes[i]
var formatted strings.Builder
formatted.Grow(len(ipBytes)*2 + 5)
for i := 0; i < len(ipBytes); i += 4 {
fmt.Fprintf(&formatted, "%08X", binary.NativeEndian.Uint32(ipBytes[i:i+4]))
}
portHex := fmt.Sprintf("%04X", uint16(port))
ipHex := strings.ToUpper(hex.EncodeToString(ipBytes))
return fmt.Sprintf("%s:%s", ipHex, portHex), nil
fmt.Fprintf(&formatted, ":%04X", uint16(port))
return formatted.String()
}
func findInodeInFile(filePath, targetHexAddr string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) {
onlyMatchedPort := ""
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 10 {
continue
for i, filePath := range filePaths {
file, err := os.Open(filePath)
if err != nil {
if i > 0 && os.IsNotExist(err) {
continue
}
return "", err
}
defer file.Close()
localAddress := fields[1]
if localAddress == targetHexAddr {
inode := fields[9]
return inode, nil
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 10 {
continue
}
localAddress := fields[1]
if localAddress == targetHexAddr {
return fields[9], nil
}
if portFallback != "" && strings.HasSuffix(localAddress, portFallback) {
wildcardIP := strings.TrimSuffix(localAddress, portFallback)
if (len(wildcardIP) == 8 || len(wildcardIP) == 32) && strings.Trim(wildcardIP, "0") == "" {
onlyMatchedPort = fields[9]
}
}
}
if err := scanner.Err(); err != nil {
return "", err
}
}
return "", scanner.Err()
return onlyMatchedPort, nil
}
func findPidByInode(inode string) (string, error) {
+29 -28
View File
@@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/platform"
@@ -26,25 +27,13 @@ func ReadFile(path string) ([]byte, error) {
return buf.ReadAllToBytes(reader)
}
func ReadAsset(file string) ([]byte, error) {
path, _, err := getAssetFileLocation(file)
func OpenAsset(file string) (io.ReadCloser, int64, error) {
path, info, err := getAssetFileLocation(file)
if err != nil {
return nil, err
return nil, 0, err
}
return ReadFile(path)
}
func OpenAsset(file string) (io.ReadCloser, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
r, err := NewFileReader(path)
return r, info.Size(), err
}
func ResolveAsset(file string) (string, error) {
@@ -78,17 +67,29 @@ func ReadCert(file string) ([]byte, error) {
return ReadFile(platform.GetCertLocation(file))
}
func CopyFile(dst string, src string) error {
bytes, err := ReadFile(src)
if err != nil {
return err
// ResolveLuaFile finds a local .lua script and returns its absolute path.
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
func ResolveLuaFile(path string) (string, error) {
if !strings.EqualFold(filepath.Ext(path), ".lua") {
return "", errors.New("Lua file extension must be .lua")
}
f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
if filepath.IsAbs(path) {
return path, nil
}
defer f.Close()
_, err = f.Write(bytes)
return err
for _, dir := range []string{
platform.GetConfDirPath(),
platform.NewEnvFlag(platform.ConfigLocation).GetValue(func() string { return "" }),
".",
platform.GetExecutableDir(),
} {
if dir != "" {
resolved := filepath.Join(dir, path)
if _, err := os.Stat(resolved); err == nil {
return filepath.Abs(resolved)
} else if !os.IsNotExist(err) {
return "", err
}
}
}
return "", os.ErrNotExist
}
+47 -2
View File
@@ -1,13 +1,17 @@
package filesystem_test
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/platform"
. "github.com/xtls/xray-core/common/platform/filesystem"
)
func TestStatAssetRejectsInvalidPath(t *testing.T) {
func TestResolveAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{
"",
".",
@@ -25,8 +29,49 @@ func TestStatAssetRejectsInvalidPath(t *testing.T) {
`nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"),
} {
if _, err := StatAsset(file); err == nil {
if _, err := ResolveAsset(file); err == nil {
t.Fatalf("expected error for %q", file)
}
}
}
func TestResolveLuaFile(t *testing.T) {
t.Chdir(t.TempDir())
workingDir, err := os.Getwd()
common.Must(err)
executable, err := os.Executable()
common.Must(err)
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
common.Must(err)
common.Must(file.Close())
defer os.Remove(file.Name())
name := filepath.Base(file.Name())
paths := []string{
filepath.Join(t.TempDir(), name),
filepath.Join(t.TempDir(), name),
filepath.Join(workingDir, name),
file.Name(),
}
t.Setenv(platform.ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(platform.ConfigLocation, filepath.Dir(paths[1]))
for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600))
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
}
for i, want := range paths {
if i == 2 {
t.Setenv(platform.ConfdirLocation, "")
t.Setenv(platform.ConfigLocation, "")
}
if got, err := ResolveLuaFile(name); err != nil || got != want {
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
}
common.Must(os.Remove(want))
}
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing file error = %v", err)
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ func LineSeparator() string {
// GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations
func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir)
assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir)
defPath := filepath.Join(assetPath, file)
for _, p := range []string{
defPath,
@@ -36,6 +36,6 @@ func GetAssetLocation(file string) string {
// GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir)
certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir)
return filepath.Join(certPath, file)
}
+2 -50
View File
@@ -1,8 +1,6 @@
package platform // import "github.com/xtls/xray-core/common/platform"
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
@@ -74,7 +72,7 @@ func NormalizeEnvName(name string) string {
return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_")
}
func getExecutableDir() string {
func GetExecutableDir() string {
exec, err := os.Executable()
if err != nil {
return ""
@@ -83,7 +81,7 @@ func getExecutableDir() string {
}
func GetConfigurationPath() string {
configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir)
configPath := NewEnvFlag(ConfigLocation).GetValue(GetExecutableDir)
return filepath.Join(configPath, "config.json")
}
@@ -92,49 +90,3 @@ func GetConfDirPath() string {
configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" })
return configPath
}
// ResolveLuaFile finds a local Lua script and returns its absolute path.
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
func ResolveLuaFile(path string) (string, error) {
if path == "" {
return "", errors.New("Lua file path is empty")
}
paths := []string{path}
if !filepath.IsAbs(path) {
paths = nil
for _, dir := range []string{
GetConfDirPath(),
NewEnvFlag(ConfigLocation).GetValue(func() string { return "" }),
".",
getExecutableDir(),
} {
if dir != "" {
paths = append(paths, filepath.Join(dir, path))
}
}
}
return resolveFile(paths)
}
func resolveFile(paths []string) (string, error) {
var tried []string
for _, path := range paths {
path, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to resolve file path: %w", err)
}
tried = append(tried, path)
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return "", fmt.Errorf("failed to inspect file %q: %w", path, err)
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("file is not a regular file: %s", path)
}
return path, nil
}
return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
}
-51
View File
@@ -1,7 +1,6 @@
package platform_test
import (
"errors"
"os"
"path/filepath"
"runtime"
@@ -65,53 +64,3 @@ func TestGetAssetLocation(t *testing.T) {
}
}
}
func TestResolveLuaFile(t *testing.T) {
workingDir := t.TempDir()
t.Chdir(workingDir)
executable, err := os.Executable()
common.Must(err)
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
common.Must(err)
common.Must(file.Close())
defer os.Remove(file.Name())
name := filepath.Base(file.Name())
paths := []string{
filepath.Join(t.TempDir(), name),
filepath.Join(t.TempDir(), name),
filepath.Join(workingDir, name),
file.Name(),
}
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600))
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
}
for i, want := range paths {
if i == 2 {
t.Setenv(ConfdirLocation, "")
t.Setenv(ConfigLocation, "")
}
if got, err := ResolveLuaFile(name); err != nil || got != want {
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
}
common.Must(os.Remove(want))
}
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing file error = %v", err)
}
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
common.Must(os.Mkdir(paths[0], 0o700))
common.Must(os.WriteFile(paths[1], nil, 0o600))
for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} {
if _, err := ResolveLuaFile(path); err == nil {
t.Fatalf("accepted invalid path %q", path)
}
}
}
+2 -2
View File
@@ -11,12 +11,12 @@ func LineSeparator() string {
// GetAssetLocation searches for `file` in the env dir and the executable dir
func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir)
assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir)
return filepath.Join(assetPath, file)
}
// GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir)
certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir)
return filepath.Join(certPath, file)
}
+6 -2
View File
@@ -175,9 +175,13 @@ func init() {
switch v := input.(type) {
case cmdarg.Arg:
r, err := confloader.LoadConfig(v[0])
common.Must(err)
if err != nil {
return nil, err
}
data, err := buf.ReadAllToBytes(r)
common.Must(err)
if err != nil {
return nil, err
}
return loadProtobufConfig(data)
case io.Reader:
data, err := buf.ReadAllToBytes(v)
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 10
Version_z byte = 10
Version_z byte = 29
)
var (
-2
View File
@@ -1,5 +1,3 @@
package core
//go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
//go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
//go:generate go run ../infra/vprotogen/main.go -pwd ./..
+10
View File
@@ -2,6 +2,7 @@ package localdns
import (
"context"
"net/netip"
"syscall"
"time"
@@ -100,6 +101,15 @@ func New() *Client {
if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address)
}
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return d.DialContext(ctx, network, address)
},
}
+21 -17
View File
@@ -4,37 +4,36 @@ go 1.27
require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/bufbuild/protocompile v0.14.1
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0
github.com/klauspost/cpuid/v2 v2.4.1
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
github.com/miekg/dns v1.1.73
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.15.0
github.com/pires/go-proxyproto v0.16.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.57.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
golang.org/x/crypto v0.58.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.60.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
google.golang.org/protobuf v1.36.11
h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11
lukechampine.com/blake3 v1.4.1
@@ -42,13 +41,12 @@ require (
)
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/andybalholm/brotli v1.2.3 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
@@ -58,9 +56,15 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
golang.org/x/tools v0.51.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
)
tool (
google.golang.org/grpc/cmd/protoc-gen-go-grpc
google.golang.org/protobuf/cmd/protoc-gen-go
)
+32 -26
View File
@@ -1,12 +1,12 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
@@ -33,10 +33,10 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U=
github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -61,8 +61,8 @@ github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/pires/go-proxyproto v0.16.0 h1:9KYT/fM3wpjT8bdMEj/7FBcoiUG1KyzEjRYnPu+vz7A=
github.com/pires/go-proxyproto v0.16.0/go.mod h1:n7yhg8yIfNl3KgYDCwBzmeUHNpKTVnUhcWpc7rV4RW4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -81,8 +81,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
@@ -95,10 +97,10 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a h1:V5oicZXS0oqZx30gY/apgSVD7YvNBfrIOdcMnlelJY4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -107,12 +109,12 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,22 +123,22 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -152,6 +154,10 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+2
View File
@@ -91,6 +91,7 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
}
if c.Fragment != nil {
errors.PrintDeprecatedFeatureWarning(`"fragment" in Freedom`, `"fragment" in "streamSettings.finalmask.tcp"`)
config.Fragment = new(freedom.Fragment)
switch strings.ToLower(c.Fragment.Packets) {
@@ -147,6 +148,7 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
}
if c.Noises != nil {
errors.PrintDeprecatedFeatureWarning(`"noises" in Freedom`, `"noise" in "streamSettings.finalmask.udp"`)
for _, n := range c.Noises {
NConfig, err := ParseNoise(n)
if err != nil {
-4
View File
@@ -6,7 +6,6 @@ import (
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
@@ -19,9 +18,6 @@ func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
if err := validateHTTPS(c.URL); err != nil {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
}
if _, err := filesystem.StatAsset(c.File); err != nil {
return nil, errors.New("invalid geodata asset file: ", c.File).Base(err)
}
return &geodata.Asset{
Url: c.URL,
File: c.File,
-23
View File
@@ -1,7 +1,6 @@
package conf_test
import (
"path/filepath"
"testing"
"github.com/xtls/xray-core/app/geodata"
@@ -9,8 +8,6 @@ import (
)
func TestGeodataConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
creator := func() Buildable {
return new(GeodataConfig)
}
@@ -38,27 +35,7 @@ func TestGeodataConfig(t *testing.T) {
})
}
func TestGeodataAssetConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "geoip.dat",
}).Build(); err != nil {
t.Fatal(err)
}
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "missing.dat",
}).Build(); err == nil {
t.Fatal("expected error")
}
}
func TestGeodataAssetConfigInvalidURL(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, rawURL := range []string{
"",
"http://example.com/geoip.dat",
+161 -10
View File
@@ -9,12 +9,17 @@ import (
"fmt"
"net/netip"
"net/url"
"os"
"regexp"
"strings"
googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -28,6 +33,7 @@ import (
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
@@ -70,6 +76,7 @@ var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) },
@@ -77,6 +84,7 @@ var (
}, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomUDP) },
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
"noise": func() interface{} { return new(NoiseMask) },
@@ -89,6 +97,23 @@ var (
}, "type", "settings")
)
type Filter struct {
BlockedSourceIPs []string `json:"blockedSourceIPs"`
AllowedSourceIPs []string `json:"allowedSourceIPs"`
}
func (c *Filter) Build() (proto.Message, error) {
rules, err := geodata.ParseIPRules(c.BlockedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "blockedSourceIPs"`).Base(err)
}
allowed, err := geodata.ParseIPRules(c.AllowedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "allowedSourceIPs"`).Base(err)
}
return &filter.Config{BlockedSourceIps: rules, AllowedSourceIps: allowed}, nil
}
type TCPItem struct {
Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"`
@@ -304,11 +329,26 @@ type NoiseItem struct {
}
type NoiseMask struct {
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
Script string `json:"script"`
}
func (c *NoiseMask) Build() (proto.Message, error) {
if c.Script != "" {
if len(c.Noise) > 0 {
return nil, errors.New(`"script" and "noise" can't be used together`)
}
if _, err := xlua.Compile(c.Script); err != nil {
return nil, errors.New("failed to compile the noise script").Base(err)
}
return &noise.Config{
ResetMin: int64(c.Reset.From),
ResetMax: int64(c.Reset.To),
Script: c.Script,
}, nil
}
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
for _, item := range c.Noise {
if len(item.Packet) > 0 && item.Rand.To > 0 {
@@ -799,7 +839,10 @@ type XDNSDomain struct {
}
type XDNSResolver struct {
Addrs []string `json:"addrs"`
Addrs []string `json:"addrs"`
Worker int32 `json:"worker"`
TlsConfig *TLSConfig `json:"tlsConfig"`
QuicConfig *QuicParamsConfig `json:"quicConfig"`
}
type XDNS struct {
@@ -834,6 +877,26 @@ func (c *XDNS) Build() (proto.Message, error) {
}
}
for i := range c.Resolvers {
worker := c.Resolvers[i].Worker
if worker == 0 {
worker = 32
}
var tlsConfig *tls.Config
var quicConfig *internet.QuicParams
if c.Resolvers[i].TlsConfig != nil {
proto, err := c.Resolvers[i].TlsConfig.Build()
if err != nil {
return nil, err
}
tlsConfig = proto.(*tls.Config)
}
if c.Resolvers[i].QuicConfig != nil {
proto, err := c.Resolvers[i].QuicConfig.Build()
if err != nil {
return nil, err
}
quicConfig = proto.(*internet.QuicParams)
}
for j := range c.Resolvers[i].Addrs {
var u *url.URL
var e error
@@ -845,18 +908,21 @@ func (c *XDNS) Build() (proto.Message, error) {
if e != nil {
return nil, e
}
var addr string
switch u.Scheme {
case "tcp", "udp":
host := u.Hostname()
port := u.Port()
if port == "" {
port = "53"
}
addr = net.JoinHostPort(host, port)
case "https":
addr = u.String()
default:
return nil, errors.New("invalid protocol")
}
var host, port string
host = u.Hostname()
port = u.Port()
if port == "" {
port = "53"
}
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: net.JoinHostPort(host, port)})
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: addr, Worker: worker, TlsConfig: tlsConfig, QuicConfig: quicConfig})
}
}
if c.ExtraPoll < 0 || c.ExtraPoll > 3 {
@@ -1149,6 +1215,91 @@ type QuicParamsConfig struct {
DisableStatelessReset bool `json:"disableStatelessReset"`
}
func (config *QuicParamsConfig) Build() (proto.Message, error) {
profile := strings.ToLower(config.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := config.BrutalUp.Bps()
if err != nil {
return nil, err
}
down, err := config.BrutalDown.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
config.Congestion = strings.ToLower(config.Congestion)
switch config.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", config.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if config.InitStreamReceiveWindow > 0 && config.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if config.MaxStreamReceiveWindow > 0 && config.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if config.InitConnectionReceiveWindow > 0 && config.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if config.MaxConnectionReceiveWindow > 0 && config.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if config.MaxIdleTimeout != 0 && (config.MaxIdleTimeout < 4 || config.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if config.KeepAlivePeriod != 0 && (config.KeepAlivePeriod < 2 || config.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if config.MaxIncomingStreams != 0 && config.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if config.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
return &internet.QuicParams{
Congestion: config.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: config.BrutalDisableLossCompensation,
InitStreamReceiveWindow: config.InitStreamReceiveWindow,
MaxStreamReceiveWindow: config.MaxStreamReceiveWindow,
InitConnReceiveWindow: config.InitConnectionReceiveWindow,
MaxConnReceiveWindow: config.MaxConnectionReceiveWindow,
MaxIdleTimeout: config.MaxIdleTimeout,
KeepAlivePeriod: config.KeepAlivePeriod,
DisablePathMtuDiscovery: config.DisablePathMTUDiscovery,
DisableChromeParrot: config.DisableChromeParrot,
DisableGSO: config.DisableGSO,
MaxIncomingStreams: config.MaxIncomingStreams,
DisableStatelessReset: config.DisableStatelessReset,
}, nil
}
type FinalMask struct {
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
@@ -0,0 +1,79 @@
package conf_test
import (
"encoding/json"
"fmt"
"net"
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
)
func TestFinalMaskFilter(t *testing.T) {
for _, network := range []string{"tcp", "udp"} {
for _, test := range []struct {
name, settings string
blocked, allowed int
}{
{"blacklist", `{"blockedSourceIPs":["192.0.2.0/24"]}`, 1, 0},
{"whitelist", `{"allowedSourceIPs":["192.0.2.0/24"]}`, 0, 1},
{"both", `{"blockedSourceIPs":["192.0.2.0/24"],"allowedSourceIPs":["192.0.2.0/24"]}`, 1, 1},
{"empty_whitelist", `{"allowedSourceIPs":[]}`, 0, 0},
{"empty_blacklist", `{"blockedSourceIPs":[]}`, 0, 0},
} {
t.Run(network+"/"+test.name, func(t *testing.T) {
input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"filter","settings":%s}]}}`, network, test.settings)
var config StreamConfig
if err := json.Unmarshal([]byte(input), &config); err != nil {
t.Fatal(err)
}
built, err := config.Build()
if err != nil {
t.Fatal(err)
}
masks := built.Tcpmasks
if network == "udp" {
masks = built.Udpmasks
}
if len(masks) != 1 {
t.Fatalf("expected one mask, got %d", len(masks))
}
instance, err := masks[0].GetInstance()
if err != nil {
t.Fatal(err)
}
filter, ok := instance.(*filter.Config)
if !ok {
t.Fatalf("unexpected mask type: %T", instance)
}
if len(filter.BlockedSourceIps) != test.blocked || len(filter.AllowedSourceIps) != test.allowed {
t.Fatalf("unexpected filter policy: %v", filter)
}
for _, rules := range [][]*geodata.IPRule{filter.BlockedSourceIps, filter.AllowedSourceIps} {
for _, rule := range rules {
cidr := rule.GetCustom().GetCidr()
if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) {
t.Fatalf("unexpected IP rule: %v", cidr)
}
}
}
if _, err := internet.ToMemoryStreamConfig(built); err != nil {
t.Fatalf("failed to initialize runtime finalmask: %v", err)
}
})
}
}
for _, field := range []string{"blockedSourceIPs", "allowedSourceIPs"} {
var config Filter
if err := json.Unmarshal([]byte(fmt.Sprintf(`{"%s":["192.0.2.0/33"]}`, field)), &config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), field) {
t.Errorf("expected invalid %s, got %v", field, err)
}
}
}
@@ -2,6 +2,9 @@ package conf
import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
@@ -134,3 +137,68 @@ func TestNoiseExpFromJSON(t *testing.T) {
t.Errorf("item 3 = %+v", items[3])
}
}
const noiseScript = `function HandleNoise(plan, address) plan:Add("n") end
`
func writeNoiseScript(t *testing.T, script string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "noise.lua")
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
t.Fatal(err)
}
return path
}
func buildNoiseMask(t *testing.T, mask *NoiseMask) *noise.Config {
t.Helper()
msg, err := mask.Build()
if err != nil {
t.Fatal(err)
}
return msg.(*noise.Config)
}
func TestNoiseScriptFromFile(t *testing.T) {
path := writeNoiseScript(t, noiseScript)
cfg := buildNoiseMask(t, &NoiseMask{Script: path, Reset: Int32Range{From: 30, To: 60}})
if cfg.Script != path {
t.Errorf("got script %q, want the path", cfg.Script)
}
if len(cfg.Items) != 0 {
t.Errorf("got %d items, want none", len(cfg.Items))
}
if cfg.ResetMin != 30 || cfg.ResetMax != 60 {
t.Errorf("got reset %d-%d, want 30-60", cfg.ResetMin, cfg.ResetMax)
}
}
func TestNoiseScriptFromJSON(t *testing.T) {
path := writeNoiseScript(t, noiseScript)
input := `{"script": ` + strconv.Quote(path) + `, "reset": "30-60"}`
var mask NoiseMask
if err := json.Unmarshal([]byte(input), &mask); err != nil {
t.Fatal(err)
}
if cfg := buildNoiseMask(t, &mask); cfg.Script != path {
t.Errorf("got script %q, want the path", cfg.Script)
}
}
func TestNoiseScriptRejected(t *testing.T) {
path := writeNoiseScript(t, noiseScript)
invalid := writeNoiseScript(t, "function HandleNoise(")
for name, mask := range map[string]*NoiseMask{
"script with noise": {Script: path, Noise: []NoiseItem{{Rand: Int32Range{From: 10, To: 20}}}},
"missing file": {Script: filepath.Join(t.TempDir(), "absent.lua")},
"not lua": {Script: invalid},
} {
t.Run(name, func(t *testing.T) {
if _, err := mask.Build(); err == nil {
t.Error("expected an error")
}
})
}
}
+33 -82
View File
@@ -1,13 +1,15 @@
package conf
import (
"os"
"context"
"slices"
"strings"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
"github.com/xtls/xray-core/transport/internet/tls"
)
type TransportProtocol string
@@ -88,6 +90,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config.ProtocolName = protocol
}
var xdriveH3 bool
switch strings.ToLower(c.Security) {
case "", "none":
case "tls":
@@ -102,6 +105,15 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type
xdriveH3 = config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"h3"})
if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" {
// a fingerprint puts its own ALPN into the hello, except Go TLS and the
// randomized ones, which take it from the config or send none
if fp := tls.GetFingerprint(strings.ToLower(tlsSettings.Fingerprint)); fp != nil && fp.Client != utls.HelloGolang.Client &&
!strings.HasPrefix(fp.Client, utls.HelloRandomized.Client) {
errors.LogWarning(context.Background(), `XDRIVE: "alpn" ["http/1.1"] reaches the wire only inside ECH, without "echConfigList" the hello carries the ALPN of the fingerprint and the server may pick h2`)
}
}
case "reality":
if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" {
return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.")
@@ -209,6 +221,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
})
}
if c.XDRIVESettings != nil {
c.XDRIVESettings.h3 = xdriveH3
xs, err := c.XDRIVESettings.Build()
if err != nil {
return nil, errors.New("Failed to build XDRIVE config.").Base(err)
@@ -228,103 +241,41 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
if c.FinalMask != nil {
for _, mask := range c.FinalMask.Tcp {
u, err := mask.Build(true)
proto, err := mask.Build(true)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u))
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(proto))
}
for _, mask := range c.FinalMask.Udp {
u, err := mask.Build(false)
proto, err := mask.Build(false)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u))
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(proto))
}
if c.FinalMask.QuicParams != nil {
profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := c.FinalMask.QuicParams.BrutalUp.Bps()
if err != nil {
return nil, err
}
down, err := c.FinalMask.QuicParams.BrutalDown.Bps()
proto, err := c.FinalMask.QuicParams.Build()
if err != nil {
return nil, err
}
config.QuicParams = proto.(*internet.QuicParams)
}
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion)
switch c.FinalMask.QuicParams.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
if xdriveH3 && c.FinalMask != nil {
if c.XDRIVESettings != nil && c.XDRIVESettings.Service == "google-drive" {
// noise only sends datagrams of its own ahead of the QUIC ones, the
// other masks change those or need a peer that undoes them
for _, mask := range c.FinalMask.Udp {
if strings.ToLower(mask.Type) != "noise" {
return nil, errors.New(`XDRIVE: over h3 to Google Drive the only "udp" mask that works is "noise", not "`, mask.Type, `"`)
}
default:
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if c.FinalMask.QuicParams.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
}
}
if config.QuicParams != nil && (config.QuicParams.Congestion == "brutal" || config.QuicParams.Congestion == "force-brutal") {
return nil, errors.New(`XDRIVE: brutal is for Hysteria servers, over h3 "congestion" is "bbr" or "reno", not "`, config.QuicParams.Congestion, `"`)
}
}
return config, nil
+55 -1
View File
@@ -997,6 +997,18 @@ type XDriveConfig struct {
EagerWindowMs uint32 `json:"eagerWindowMs"`
HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
Template json.RawMessage `json:"template"`
S3 *XDriveS3Config `json:"s3"`
Xmux XmuxConfig `json:"xmux"`
// set by StreamConfig when "alpn" is ["h3"]
h3 bool
}
type XDriveS3Config struct {
Endpoint string `json:"endpoint"`
Region string `json:"region"`
Bucket string `json:"bucket"`
PathStyle *bool `json:"pathStyle"`
}
// Build implements Buildable.
@@ -1004,6 +1016,10 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
switch c.Service {
case "local":
case "Google Drive":
errors.PrintDeprecatedFeatureWarning(`"service": "Google Drive"`, `"service": "google-drive"`)
c.Service = "google-drive"
fallthrough
case "google-drive":
if len(c.Secrets) != 3 {
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
}
@@ -1011,8 +1027,27 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
if len(c.Template) == 0 {
return nil, errors.New(`service "template" needs a "template" object`)
}
case "s3":
if c.S3 == nil {
return nil, errors.New(`service "s3" needs an "s3" object`)
}
default:
return nil, errors.New("unsupported service")
return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template", "s3" or "local"`)
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 1
c.Xmux.MaxConnections.To = 1
if c.Service == "google-drive" && !c.h3 {
// Google gives an h2 connection 1 MiB of window, which a few sessions on
// a long rtt would share, an h3 connection has no such limit
c.Xmux.MaxConnections.From = 4
c.Xmux.MaxConnections.To = 4
}
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
}
config := &xdrive.Config{
RemoteFolder: c.RemoteFolder,
@@ -1027,6 +1062,25 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
EagerWindowMs: c.EagerWindowMs,
HoleTimeoutMs: c.HoleTimeoutMs,
Template: string(c.Template),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
},
}
if c.Service == "s3" {
config.S3 = &xdrive.S3Config{
Endpoint: c.S3.Endpoint,
Region: c.S3.Region,
Bucket: c.S3.Bucket,
PathStyle: c.S3.PathStyle == nil || *c.S3.PathStyle,
}
if err := xdrive.ValidateS3Config(config); err != nil {
return nil, err
}
}
return config, nil
}
+274 -2
View File
@@ -5,9 +5,12 @@ import (
"strings"
"testing"
"github.com/xtls/xray-core/common/log"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/xdrive"
"google.golang.org/protobuf/proto"
)
@@ -321,8 +324,25 @@ func TestXDriveRejectsUnknownService(t *testing.T) {
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); err == nil {
t.Fatal("Build accepted an unsupported service")
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template", "s3" or "local"`) {
t.Fatalf("expected an unknown service error, got %v", err)
}
}
func TestXDriveGoogleDriveName(t *testing.T) {
for _, service := range []string{"google-drive", "Google Drive"} {
config := &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b", "c"}}
built, err := config.Build()
if err != nil {
t.Fatalf("%s: %v", service, err)
}
if got := built.(*xdrive.Config).Service; got != "google-drive" {
t.Errorf("%s: service is %q, want google-drive", service, got)
}
config = &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b"}}
if _, err := config.Build(); err == nil {
t.Errorf("%s: accepted two secrets", service)
}
}
}
@@ -355,6 +375,131 @@ func TestXDriveTemplateStreamConfig(t *testing.T) {
}
}
type logLines []string
func (l *logLines) Handle(msg log.Message) {
*l = append(*l, msg.String())
}
// "alpn" ["http/1.1"] is warned about where the hello does not carry it, which
// is outside ECH with a fingerprint that has an ALPN of its own
func TestXDriveHTTP1ALPNWarning(t *testing.T) {
var lines logLines
log.RegisterHandler(&lines)
for _, c := range []struct {
method, tlsSettings string
warn bool
}{
{"xdrive", `{}`, false},
{"xdrive", `{"alpn": ["http/1.1"]}`, true},
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "Firefox"}`, true},
{"raw", `{"alpn": ["http/1.1"]}`, false},
{"xdrive", `{"alpn": ["h2", "http/1.1"]}`, false},
{"xdrive", `{"alpn": ["http/1.1"], "echConfigList": "udp://1.1.1.1"}`, false},
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "unsafe"}`, false},
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellogolang"}`, false},
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "randomized"}`, false},
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellorandomizednoalpn"}`, false},
} {
lines = nil
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
"method": "`+c.method+`",
"security": "tls",
"tlsSettings": `+c.tlsSettings+`,
"xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); err != nil {
t.Fatalf("%s %s: %v", c.method, c.tlsSettings, err)
}
warned := false
for _, line := range lines {
warned = warned || strings.HasPrefix(line, "[Warning]") && strings.Contains(line, `"alpn"`)
}
if warned != c.warn {
t.Errorf("%s %s: warned %v, want %v", c.method, c.tlsSettings, warned, c.warn)
}
}
}
// over h3 Google Drive takes only the noise mask, and no service takes brutal
func TestXDriveH3Checks(t *testing.T) {
const (
drive = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]}`
local = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}`
noise = `{"type": "Noise", "settings": {"noise": [{"rand": "50-100"}]}}`
salamander = `{"type": "salamander", "settings": {"password": "p"}}`
forceBrutal = `{"quicParams": {"congestion": "force-brutal", "brutalUp": "10 mbps"}}`
)
for _, c := range []struct {
alpn, transport, finalmask string
ok bool
}{
{`["h3"]`, drive, `{"udp": [` + noise + `]}`, true},
{`["h3"]`, drive, `{"udp": [` + salamander + `]}`, false},
{`["h3"]`, drive, `{"udp": [` + noise + `, ` + salamander + `]}`, false},
{`["h3"]`, local, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, `"method": "xdrive"`, `{"udp": [` + salamander + `]}`, true},
{`["h2"]`, drive, `{"udp": [` + salamander + `]}`, true},
{`null`, drive, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, drive, `null`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "bbr"}}`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, false},
{`["h3"]`, local, forceBrutal, false},
{`["h3", "h2"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, true},
{`["h3"]`, `"method": "xhttp"`, forceBrutal, true},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
`+c.transport+`,
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`},
"finalmask": `+c.finalmask+`
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); (err == nil) != c.ok {
t.Errorf("alpn %s, %s, finalmask %s: %v", c.alpn, c.transport, c.finalmask, err)
}
}
}
// one h3 connection is not held back by a window like h2, so h3 takes one by default
func TestXDriveH3Connections(t *testing.T) {
for _, c := range []struct {
alpn string
want int32
}{
{`["h3"]`, 1},
{`["h2"]`, 4},
{`null`, 4},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
"method": "xdrive",
"xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]},
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`}
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
xs, err := built.TransportSettings[0].Settings.GetInstance()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
if got := xs.(*xdrive.Config).Xmux.MaxConnections; got.From != c.want || got.To != c.want {
t.Errorf("alpn %s: maxConnections is %d-%d, want %d", c.alpn, got.From, got.To, c.want)
}
}
}
func TestXDriveTemplateNeedsTemplate(t *testing.T) {
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
@@ -364,3 +509,130 @@ func TestXDriveTemplateNeedsTemplate(t *testing.T) {
t.Fatal("Build accepted a template service without a template")
}
}
const (
xdriveStream = `"streamSettings": {"network": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "local"}}`
xdriveID = `"id": "27848739-7e62-4138-9fd3-098a63964b6b"`
xdriveServer = `"address": "127.0.0.1", "port": 443, ` + xdriveID
xdriveEnc = `"encryption": "mlkem768x25519plus.native.0rtt.XKbaq1BuzJKKKPxZbwgRpn-gYQbnhrYketgezASCL1I"`
xdriveDec = `"decryption": "mlkem768x25519plus.native.600s.UKfUEMDdjlEnycTMBP0a08acky8vRRKSrM1MRnaYk3s"`
)
func TestXDriveOutboundConfig(t *testing.T) {
build := func(s string) error {
c := new(OutboundDetourConfig)
if err := json.Unmarshal([]byte(s), c); err != nil {
return err
}
_, err := c.Build()
return err
}
for _, input := range []string{
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`,
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": 0, "xudpConcurrency": 8, "xudpProxyUDP443": "allow"}}`,
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "reverse": {"tag": "r"}}, ` + xdriveStream + `}`,
`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `}]}]}, ` + xdriveStream + `, "mux": {"enabled": true}}`,
} {
if err := build(input); err != nil {
t.Errorf("%s: %v", input, err)
}
}
for _, c := range []struct{ input, err string }{
{`{"protocol": "vless", "settings": {` + xdriveServer + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`},
{`{"protocol": "vless", "settings": {"address": "example.com", "port": 443, ` + xdriveID + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `}`, `"mux"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": false}}`, `"mux"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"mux"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpConcurrency": -1}}`, `"mux"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpProxyUDP443": "skip"}}`, `"mux"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`},
{`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}]}]}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`},
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "testpre": 1}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"testpre"`},
{`{"protocol": "vmess", "settings": {` + xdriveServer + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"},
{`{"protocol": "trojan", "settings": {"address": "example.com", "port": 443, "password": "p"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"},
{`{"protocol": "freedom", ` + xdriveStream + `}`, "VLESS"},
} {
if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) {
t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err)
}
}
}
func TestXDriveInboundConfig(t *testing.T) {
build := func(s string) error {
c := new(InboundDetourConfig)
if err := json.Unmarshal([]byte(s), c); err != nil {
return err
}
_, err := c.Build()
return err
}
for _, input := range []string{
`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`,
`{"protocol": "vless", "listen": "@xdrive", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`,
} {
if err := build(input); err != nil {
t.Errorf("%s: %v", input, err)
}
}
for _, c := range []struct{ input, err string }{
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], "decryption": "none"}, ` + xdriveStream + `}`, `"decryption"`},
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `}`, `"flow"`},
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `, "flow": "xtls-rprx-vision"}], ` + xdriveDec + `}, ` + xdriveStream + `}`, `"flow"`},
{`{"protocol": "vmess", "port": 443, "settings": {"clients": [{` + xdriveID + `}]}, ` + xdriveStream + `}`, "VLESS"},
{`{"protocol": "vless", "port": "10800-10803", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`, "one port"},
} {
if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) {
t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err)
}
}
}
func TestXDriveXmux(t *testing.T) {
defaults := func(connections int32) *splithttp.XmuxConfig {
return &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{},
MaxConnections: &splithttp.RangeConfig{From: connections, To: connections},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{From: 1800, To: 3000},
}
}
for _, c := range []struct {
input string
want *splithttp.XmuxConfig
}{
{`{"service": "google-drive", "secrets": ["a", "b", "c"]}`, defaults(4)},
{`{"service": "template", "template": {}}`, defaults(1)},
{`{"service": "google-drive", "secrets": ["a", "b", "c"], "xmux": {"maxConcurrency": "1-2", "hMaxRequestTimes": 100, "hKeepAlivePeriod": -1}}`, &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 2},
MaxConnections: &splithttp.RangeConfig{},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{From: 100, To: 100},
HMaxReusableSecs: &splithttp.RangeConfig{},
HKeepAlivePeriod: -1,
}},
} {
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(c.input), config); err != nil {
t.Fatalf("%s: Unmarshal: %v", c.input, err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("%s: Build: %v", c.input, err)
}
if got := built.(*xdrive.Config).Xmux; !proto.Equal(got, c.want) {
t.Errorf("%s: xmux is %v, want %v", c.input, got, c.want)
}
}
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"service": "template", "template": {}, "xmux": {"maxConnections": 2, "maxConcurrency": 8}}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); err == nil {
t.Error("Build accepted maxConnections with maxConcurrency")
}
}
+10 -15
View File
@@ -6,9 +6,7 @@ import (
"math/big"
"net"
"runtime"
"slices"
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/proxy/tun"
@@ -39,25 +37,22 @@ func (v *TunConfig) Build() (proto.Message, error) {
AutoSystemRoutingTable: v.AutoSystemRoutingTable,
AutoSystemDnsToGateway: v.AutoSystemDnsToGateway,
}
for _, leak := range v.AutoSystemWfpBlockLeak {
switch leak := strings.ToLower(leak); leak {
case "dns", "misconfigtun":
config.AutoSystemWfpBlockLeak = append(config.AutoSystemWfpBlockLeak, leak)
default:
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", leak)
}
leaks, err := tun.ExpandWfpBlockLeak(v.AutoSystemWfpBlockLeak)
if err != nil {
return nil, err
}
config.AutoSystemWfpBlockLeak = leaks
// Each option needs other settings on the system it takes effect on: the
// filters go along with the routes of autoSystemRoutingTable, "dns" lets
// DNS through the TUN only, and autoSystemDnsToGateway points the system
// DNS at the gateway.
// filters go along with the routes of autoSystemRoutingTable, keeping the
// system's DNS inside the TUN needs DNS servers on it, and
// autoSystemDnsToGateway points the system DNS at the gateway.
switch runtime.GOOS {
case "windows":
if len(config.AutoSystemWfpBlockLeak) > 0 && len(v.AutoSystemRoutingTable) == 0 {
if len(leaks) > 0 && len(v.AutoSystemRoutingTable) == 0 {
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
}
if slices.Contains(config.AutoSystemWfpBlockLeak, "dns") && len(v.DNS) == 0 {
return nil, errors.New(`autoSystemWfpBlockLeak "dns" needs dns to be set`)
if tun.BlocksSystemDNS(leaks) && len(v.DNS) == 0 {
return nil, errors.New(`autoSystemWfpBlockLeak "dns", "udp/53", "tcp/53" or "windnsclient" needs dns to be set`)
}
case "linux", "freebsd":
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
+18 -8
View File
@@ -29,12 +29,17 @@ func TestTunConfigAutoSystem(t *testing.T) {
{
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`,
Parser: loadJSON(creator),
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns", "misconfigtun"}},
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853", "misconfigtun"}},
},
{
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`,
Parser: loadJSON(creator),
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns"}},
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}},
},
{
Input: `{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["TCP/853", "udp/853", "tcp/0853"]}`,
Parser: loadJSON(creator),
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"tcp/853", "udp/853"}},
},
})
}
@@ -49,6 +54,9 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, nil},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp/53"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["windnsclient"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/853", "udp/853"]}`, nil},
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
} {
config := new(TunConfig)
@@ -62,11 +70,13 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
}
func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) {
config := new(TunConfig)
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "ip"]}`), config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err == nil {
t.Error("an unknown autoSystemWfpBlockLeak value was accepted")
for _, value := range []string{"ip", "tcp/0", "icmp/1"} {
config := new(TunConfig)
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "`+value+`"]}`), config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err == nil {
t.Errorf("autoSystemWfpBlockLeak value %q was accepted", value)
}
}
}
+64 -3
View File
@@ -17,7 +17,11 @@ import (
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/proxy/masque"
"github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vless/inbound"
"github.com/xtls/xray-core/proxy/vless/outbound"
"github.com/xtls/xray-core/transport/internet"
"google.golang.org/protobuf/proto"
)
var (
@@ -209,6 +213,9 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
if _, ok := ts.(*masque.ServerConfig); !ok && receiverSettings.StreamSettings != nil && receiverSettings.StreamSettings.ProtocolName == "masque" {
return nil, errors.New("the masque transport can only be used by the masque inbound")
}
if err := validateXDrive(ts, receiverSettings.StreamSettings, nil, receiverSettings.PortList); err != nil {
return nil, err
}
return &core.InboundHandlerConfig{
Tag: c.Tag,
@@ -230,10 +237,11 @@ type OutboundDetourConfig struct {
func requiresTransportSecurity(address *Address) bool {
if address == nil || address.Address == nil {
return false
return true
}
if address.Family().IsIP() {
return !geodata.GetPrivateIPMatcher().Match(address.IP())
ip := address.IP()
return !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
}
domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".")
return !geodata.GetPrivateDomainMatcher().MatchAny(domain)
@@ -262,6 +270,54 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
return nil
}
// validateXDrive allows only VLESS with encryption on xdrive, and on the client Mux.Cool for every connection
func validateXDrive(ts proto.Message, ss *internet.StreamConfig, ms *proxyman.MultiplexingConfig, pl *net.PortList) error {
if ss == nil || ss.ProtocolName != "xdrive" {
return nil
}
switch c := ts.(type) {
case *outbound.Config:
a, err := c.Vnext.User.Account.GetInstance()
if err != nil {
return err
}
account := a.(*vless.Account)
if account.Flow != "" {
return errors.New(`the xdrive transport doesn't support VLESS "flow"`)
}
if account.Encryption == "none" {
return errors.New(`the xdrive transport needs VLESS "encryption", run "xray vlessenc" to get one`)
}
// reverse opens its own mux
if account.Reverse == nil && (ms == nil || !ms.Enabled || ms.Concurrency < 0 || ms.XudpConcurrency < 0 || ms.XudpProxyUDP443 == "skip") {
return errors.New(`the xdrive transport needs "mux" enabled, with no negative "concurrency" or "xudpConcurrency" and no "xudpProxyUDP443":"skip"`)
}
if account.Testpre != 0 {
return errors.New(`the xdrive transport doesn't support VLESS "testpre"`)
}
case *inbound.Config:
if c.Decryption == "none" {
return errors.New(`the xdrive transport needs VLESS "decryption", run "xray vlessenc" to get one`)
}
for _, user := range c.Users {
a, err := user.Account.GetInstance()
if err != nil {
return err
}
if a.(*vless.Account).Flow != "" {
return errors.New(`the xdrive transport doesn't support VLESS "flow"`)
}
}
// every port gets its own listener on the same folder, and each of them accepts the same session
if pl != nil && len(pl.Ports()) != 1 {
return errors.New("the xdrive transport can only listen on one port")
}
default:
return errors.New("the xdrive transport can only be used by VLESS")
}
return nil
}
// Build implements Buildable.
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.ProxySettings != nil {
@@ -340,6 +396,9 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if err != nil {
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
}
if err := validateXDrive(ts, senderSettings.StreamSettings, senderSettings.MultiplexSettings, nil); err != nil {
return nil, err
}
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
@@ -550,7 +609,6 @@ func (c *Config) Build() (*core.Config, error) {
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
},
}
@@ -660,6 +718,9 @@ func (c *Config) Build() (*core.Config, error) {
config.App = append(config.App, serial.ToTypedMessage(r))
}
// Initialize the inbound module last to ensure inbound connections are handled correctly.
config.App = append(config.App, serial.ToTypedMessage(&proxyman.InboundConfig{}))
var inbounds []InboundDetourConfig
if len(c.InboundConfigs) > 0 {
+1 -1
View File
@@ -90,7 +90,6 @@ func TestXrayConfig(t *testing.T) {
AccessLogPath: "/var/log/xray/access.log",
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&router.Config{
DomainStrategy: router.Config_AsIs,
@@ -111,6 +110,7 @@ func TestXrayConfig(t *testing.T) {
},
},
}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
},
Inbound: []*core.InboundHandlerConfig{
{
+176 -197
View File
@@ -1,149 +1,87 @@
package main
import (
"errors"
"bytes"
"context"
"flag"
"fmt"
"go/build"
"io"
"net/http"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/bufbuild/protocompile"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/pluginpb"
)
var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
var (
directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
mode = flag.String("mode", "generate", "Execution mode. 'generate' (default) compiles and saves changes. 'check' reports out-of-date files and exits with non-zero.")
)
// envFile returns the name of the Go environment configuration file.
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
func envFile() (string, error) {
if file := os.Getenv("GOENV"); file != "" {
if file == "off" {
return "", errors.New("GOENV=off")
// topoSort sorts file descriptors so each dependency appears before files importing it.
func topoSort(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto {
fileMap := make(map[string]protoreflect.FileDescriptor)
for _, f := range files {
fileMap[f.Path()] = f
}
visited := make(map[string]bool)
var sorted []*descriptorpb.FileDescriptorProto
var visit func(fd protoreflect.FileDescriptor)
visit = func(fd protoreflect.FileDescriptor) {
if visited[fd.Path()] {
return
}
return file, nil
}
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
if dir == "" {
return "", errors.New("missing user-config dir")
}
return filepath.Join(dir, "go", "env"), nil
}
visited[fd.Path()] = true
// GetRuntimeEnv returns the value of runtime environment variable,
// that is set by running following command: `go env -w key=value`.
func GetRuntimeEnv(key string) (string, error) {
file, err := envFile()
if err != nil {
return "", err
}
if file == "" {
return "", errors.New("missing runtime env file")
}
var data []byte
var runtimeEnv string
data, readErr := os.ReadFile(file)
if readErr != nil {
return "", readErr
}
envStrings := strings.Split(string(data), "\n")
for _, envItem := range envStrings {
envItem = strings.TrimSuffix(envItem, "\r")
envKeyValue := strings.Split(envItem, "=")
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
runtimeEnv = strings.TrimSpace(envKeyValue[1])
imports := fd.Imports()
for i := 0; i < imports.Len(); i++ {
if dep, ok := fileMap[imports.Get(i).Path()]; ok {
visit(dep)
}
}
sorted = append(sorted, protodesc.ToFileDescriptorProto(fd))
}
return runtimeEnv, nil
for _, f := range files {
visit(f)
}
return sorted
}
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
func GetGOBIN() string {
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
GOBIN := os.Getenv("GOBIN")
if GOBIN == "" {
var err error
// The one set by user by running `go env -w GOBIN=/path`
GOBIN, err = GetRuntimeEnv("GOBIN")
if err != nil {
// The default one that Golang uses
return filepath.Join(build.Default.GOPATH, "bin")
}
if GOBIN == "" {
return filepath.Join(build.Default.GOPATH, "bin")
}
return GOBIN
}
return GOBIN
}
func whichProtoc(suffix, targetedVersion string) (string, error) {
protoc := "protoc" + suffix
path, err := exec.LookPath(protoc)
func runPlugin(pluginName, pwd string, req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) {
reqBytes, err := proto.Marshal(req)
if err != nil {
return "", fmt.Errorf(`
Command "%s" not found.
Make sure that %s is in your system path or current path.
Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases
`, protoc, protoc, protoc, targetedVersion)
return nil, err
}
return path, nil
}
func getProjectProtocVersion(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", errors.New("can not get the version of protobuf used in xray project")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("can not read from body")
}
versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v\d+\.(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(body))
return matched[1], nil
}
cmd := exec.Command("go", "tool", pluginName)
cmd.Dir = pwd
cmd.Stdin = bytes.NewReader(reqBytes)
cmd.Env = os.Environ()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
func getInstalledProtocVersion(protocPath string) (string, error) {
cmd := exec.Command(protocPath, "--version")
cmd.Env = append(cmd.Env, os.Environ()...)
output, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return "", cmdErr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("%v: %s", err, stderr.String())
}
versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(output))
return matched[1], nil
}
func parseVersion(s string, width int) int64 {
strList := strings.Split(s, ".")
format := fmt.Sprintf("%%s%%0%ds", width)
v := ""
for _, value := range strList {
v = fmt.Sprintf(format, v, value)
var resp pluginpb.CodeGeneratorResponse
if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
var result int64
var err error
if result, err = strconv.ParseInt(v, 10, 64); err != nil {
return 0
if resp.GetError() != "" {
return nil, fmt.Errorf("%s", resp.GetError())
}
return result
}
func needToUpdate(targetedVersion, installedVersion string) bool {
vt := parseVersion(targetedVersion, 4)
vi := parseVersion(installedVersion, 4)
return vt > vi
return &resp, nil
}
func main() {
@@ -154,106 +92,147 @@ func main() {
flag.Parse()
if !filepath.IsAbs(*directory) {
pwd, wdErr := os.Getwd()
if wdErr != nil {
pwd, err := os.Getwd()
if err != nil {
fmt.Println("Can not get current working directory.")
os.Exit(1)
}
*directory = filepath.Join(pwd, *directory)
}
pwd := *directory
GOBIN := GetGOBIN()
binPath := os.Getenv("PATH")
pathSlice := []string{pwd, GOBIN, binPath}
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
os.Setenv("PATH", binPath)
suffix := ""
if runtime.GOOS == "windows" {
suffix = ".exe"
}
/*
targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
*/
targetedVersion := ""
protoc, err := whichProtoc(suffix, targetedVersion)
if err != nil {
fmt.Println(err)
isCheck := false
switch *mode {
case "generate", "":
isCheck = false
case "check":
isCheck = true
default:
fmt.Fprintf(os.Stderr, "Unrecognized 'mode': %s. Use 'generate' or 'check'.\n", *mode)
os.Exit(1)
}
installedVersion, err := getInstalledProtocVersion(protoc)
if err != nil {
fmt.Println(err)
os.Exit(1)
if isCheck {
fmt.Println("Checking for out-of-date Protobuf generated files...")
}
if needToUpdate(targetedVersion, installedVersion) {
fmt.Printf(`
You are using an old protobuf version, please update to v%s or later.
Download it from https://github.com/protocolbuffers/protobuf/releases
* Protobuf version used in xray project: v%s
* Protobuf version you have installed: v%s
`, targetedVersion, targetedVersion, installedVersion)
os.Exit(1)
}
protoFilesMap := make(map[string][]string)
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println(err)
var protoFiles []string
var pbCount int
err := filepath.WalkDir(pwd, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
if info.IsDir() {
return nil
if strings.HasSuffix(d.Name(), ".proto") {
rel, _ := filepath.Rel(pwd, path)
protoFiles = append(protoFiles, filepath.ToSlash(rel))
}
dir := filepath.Dir(path)
filename := filepath.Base(path)
if strings.HasSuffix(filename, ".proto") {
path = path[len(pwd)+1:]
protoFilesMap[dir] = append(protoFilesMap[dir], path)
if strings.HasSuffix(d.Name(), ".pb.go") {
pbCount++
}
return nil
})
if walkErr != nil {
fmt.Println(walkErr)
if err != nil {
fmt.Println("Walk error:", err)
os.Exit(1)
}
if len(protoFiles) == 0 {
if isCheck && pbCount > 0 {
fmt.Fprintf(os.Stderr, "Found %d .pb.go files without any .proto files. Remove these orphaned files.\n", pbCount)
os.Exit(1)
}
return
}
compiler := protocompile.Compiler{
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{pwd},
}),
SourceInfoMode: protocompile.SourceInfoStandard,
}
linkedFiles, err := compiler.Compile(context.Background(), protoFiles...)
if err != nil {
fmt.Fprintf(os.Stderr, "Protobuf compilation failed:\n%v\n", err)
os.Exit(1)
}
for _, files := range protoFilesMap {
for _, relProtoFile := range files {
args := []string{
"--go_out", pwd,
"--go_opt", "paths=source_relative",
"--go-grpc_out", pwd,
"--go-grpc_opt", "paths=source_relative",
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
var fdList []protoreflect.FileDescriptor
for _, f := range linkedFiles {
fdList = append(fdList, f)
}
req := &pluginpb.CodeGeneratorRequest{
FileToGenerate: protoFiles,
Parameter: proto.String("paths=source_relative"),
ProtoFile: topoSort(fdList),
// TODO: replace with unknown in the future
CompilerVersion: &pluginpb.Version{
Major: proto.Int32(6),
Minor: proto.Int32(33),
Patch: proto.Int32(5),
},
}
var diffCount int
generatedFiles := make(map[string]struct{})
for _, plugin := range []string{"protoc-gen-go", "protoc-gen-go-grpc"} {
resp, err := runPlugin(plugin, pwd, req)
if err != nil {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", plugin, err)
os.Exit(1)
}
for _, f := range resp.File {
targetPath := filepath.Join(pwd, filepath.FromSlash(f.GetName()))
generatedFiles[targetPath] = struct{}{}
content := []byte(f.GetContent())
hasDiff := false
existing, err := os.ReadFile(targetPath)
if err != nil {
hasDiff = true
} else {
oldNorm := bytes.ReplaceAll(existing, []byte("\r\n"), []byte("\n"))
newNorm := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
if !bytes.Equal(oldNorm, newNorm) {
hasDiff = true
}
}
args = append(args, relProtoFile)
cmd := exec.Command(protoc, args...)
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Dir = pwd
output, cmdErr := cmd.CombinedOutput()
if len(output) > 0 {
fmt.Println(string(output))
}
if cmdErr != nil {
fmt.Println(cmdErr)
os.Exit(1)
if hasDiff {
diffCount++
fmt.Println(targetPath)
if isCheck {
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
os.Exit(1)
}
mode := os.FileMode(0o644)
if stat, err := os.Stat(targetPath); err == nil {
mode = stat.Mode().Perm()
}
if err := os.WriteFile(targetPath, content, mode); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", targetPath, err)
os.Exit(1)
}
}
}
}
if isCheck {
if pbCount != len(generatedFiles) {
fmt.Printf("Protobuf file count mismatch: found %d, expected %d. Remove orphaned files and regenerate.\n", pbCount, len(generatedFiles))
diffCount++
}
if diffCount > 0 {
fmt.Println("Protobuf generated file problem(s) found.")
fmt.Println("Please run 'go run ./infra/vprotogen' to regenerate them.")
os.Exit(1)
}
fmt.Println("All Protobuf generated file check has been passed.")
}
}
+2
View File
@@ -92,6 +92,8 @@ func executeAddRules(cmd *base.Command, args []string) {
if err != nil {
base.Fatalf("failed to perform AddRule: %s", err)
}
// if multiple files are specified, use append to merge them instead of overriding previous one
shouldAppend = true
showJSONResponse(resp)
}
}
+4
View File
@@ -4,6 +4,8 @@ import (
"github.com/xtls/xray-core/main/commands/all/api"
"github.com/xtls/xray-core/main/commands/all/base64"
"github.com/xtls/xray-core/main/commands/all/convert"
// TODO: Restore this import and lua.CmdLua when Lua privileged signature authorization is re-enabled.
// "github.com/xtls/xray-core/main/commands/all/lua"
"github.com/xtls/xray-core/main/commands/all/tls"
"github.com/xtls/xray-core/main/commands/base"
)
@@ -14,9 +16,11 @@ func init() {
api.CmdAPI,
base64.CmdBase64,
convert.CmdConvert,
// lua.CmdLua,
tls.CmdTLS,
cmdUUID,
cmdX25519,
// cmdEd25519,
cmdWG,
cmdMLDSA65,
cmdMLKEM768,
+53
View File
@@ -0,0 +1,53 @@
package all
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdEd25519 = &base.Command{
UsageLine: `{{.Exec}} ed25519 [-i "private key seed (Base64URL)"]`,
Short: "Generate an Ed25519 key pair",
Long: `
Generate an Ed25519 key pair, or restore it from a 32-byte private key seed
with -i. PrivateKey (seed) and PublicKey use unpadded Base64URL.
`,
}
var inputEd25519 = cmdEd25519.Flag.String("i", "", "Ed25519 private key seed in unpadded Base64URL.")
func init() {
cmdEd25519.Run = executeEd25519
}
func executeEd25519(cmd *base.Command, args []string) {
if len(args) != 0 {
cmd.Usage()
}
var privateKey ed25519.PrivateKey
if *inputEd25519 == "" {
_, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
base.Fatalf("failed to generate Ed25519 key: %s", err)
}
privateKey = key
} else {
encoding := base64.RawURLEncoding.Strict()
seed, err := encoding.DecodeString(*inputEd25519)
if err != nil || len(seed) != ed25519.SeedSize || encoding.EncodeToString(seed) != *inputEd25519 {
base.Fatalf("invalid Ed25519 private key seed: expected 32 bytes in unpadded Base64URL")
}
privateKey = ed25519.NewKeyFromSeed(seed)
}
if _, err := fmt.Printf("PrivateKey: %s\nPublicKey: %s\n",
base64.RawURLEncoding.EncodeToString(privateKey.Seed()),
base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))); err != nil {
base.Fatalf("failed to write Ed25519 keys: %s", err)
}
}
+13
View File
@@ -0,0 +1,13 @@
package lua
import "github.com/xtls/xray-core/main/commands/base"
// CmdLua holds the Lua script commands.
var CmdLua = &base.Command{
UsageLine: "{{.Exec}} lua",
Short: "Manage Lua scripts",
Long: "{{.Exec}} {{.LongName}} provides tools for local Lua scripts.",
Commands: []*base.Command{
cmdSign,
},
}
+91
View File
@@ -0,0 +1,91 @@
package lua
import (
"os"
"path/filepath"
"strings"
xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdSign = &base.Command{
UsageLine: "{{.Exec}} lua sign [-key seed] [-o output-file] <input-file>",
Short: "Sign a local Lua script for privileged execution",
Long: `
Sign a local Lua file using a 32-byte Ed25519 private key seed in unpadded
Base64URL. Use {{.Exec}} ed25519 to generate a key pair.
Write back to the input file, or use -o to write another file (- for stdout).
`,
}
var (
signKey = cmdSign.Flag.String("key", "", "Ed25519 private key seed in unpadded Base64URL.")
signOutput = cmdSign.Flag.String("o", "", "Output file, or - for stdout. Default: input file.")
)
func init() {
cmdSign.Run = executeSign
}
func executeSign(cmd *base.Command, args []string) {
if len(args) != 1 {
cmd.Usage()
}
if !strings.EqualFold(filepath.Ext(args[0]), ".lua") {
base.Fatalf("Lua file extension must be .lua")
}
info, err := os.Stat(args[0])
if err != nil {
base.Fatalf("failed to inspect Lua file: %s", err)
}
code, err := os.ReadFile(args[0])
if err != nil {
base.Fatalf("failed to read Lua file: %s", err)
}
signed, err := xlua.SignPrivilegedScript(code, *signKey)
if err != nil {
base.Fatalf("failed to sign Lua file: %s", err)
}
if *signOutput == "-" {
if _, err := os.Stdout.Write(signed); err != nil {
base.Fatalf("failed to write signed Lua file: %s", err)
}
return
}
output := *signOutput
if output == "" {
output = args[0]
}
if err := writeSignedFile(output, signed, info.Mode().Perm()); err != nil {
base.Fatalf("failed to write signed Lua file: %s", err)
}
}
func writeSignedFile(path string, code []byte, mode os.FileMode) error {
if info, err := os.Stat(path); err == nil {
mode = info.Mode().Perm()
} else if !os.IsNotExist(err) {
return err
}
file, err := os.CreateTemp(filepath.Dir(path), ".xray-lua-sign-*")
if err != nil {
return err
}
defer os.Remove(file.Name())
defer file.Close()
if _, err := file.Write(code); err != nil {
return err
}
if err := file.Chmod(mode); err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
return os.Rename(file.Name(), path)
}
+3 -3
View File
@@ -1,11 +1,11 @@
package all
import (
"crypto/mldsa"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/xray-core/main/commands/base"
)
@@ -39,8 +39,8 @@ func executeMLDSA65(cmd *base.Command, args []string) {
} else {
rand.Read(seed[:])
}
pub, _ := mldsa65.NewKeyFromSeed(&seed)
key, _ := mldsa.NewPrivateKey(mldsa.MLDSA65(), seed[:])
fmt.Printf("Seed: %v\nVerify: %v\n",
base64.RawURLEncoding.EncodeToString(seed[:]),
base64.RawURLEncoding.EncodeToString(pub.Bytes()))
base64.RawURLEncoding.EncodeToString(key.PublicKey().Bytes()))
}
+2
View File
@@ -4,6 +4,8 @@ package base
type CommandEnvHolder struct {
// Executable name of current binary
Exec string
// Full name of the command being executed
CommandName string
// commands column width of current command
CommandsWidth int
}
+1
View File
@@ -61,6 +61,7 @@ BigCmdLoop:
}
buildCommandText(cmd)
CommandEnv.CommandName = cmdName
cmd.Run(cmd, args)
Exit()
return
+27 -1
View File
@@ -8,12 +8,14 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/main/commands/base"
"github.com/xtls/xray-core/main/confloader"
)
@@ -31,7 +33,12 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
data, err = io.ReadAll(os.Stdin)
default:
data, err = os.ReadFile(arg)
file, openErr := os.Open(arg)
if openErr != nil {
return nil, openErr
}
defer file.Close()
data, err = readConfigFile(file)
}
if err != nil {
@@ -41,6 +48,25 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
return
}
func readConfigFile(file *os.File) ([]byte, error) {
if base.CommandEnv.CommandName != "run" {
return io.ReadAll(file)
}
info, err := file.Stat()
if err != nil {
return nil, err
}
if info.Mode().IsRegular() {
switch strings.ToLower(filepath.Ext(file.Name())) {
case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf":
default:
return nil, errors.New("unsupported config file extension: ", file.Name())
}
}
return io.ReadAll(file)
}
// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL
// or a Unix socket HTTP endpoint.
//
+10 -4
View File
@@ -36,10 +36,6 @@ The -confdir=dir flag sets a dir with multiple json config
The -format=json flag sets the format of config files.
Default "auto".
The config root env object sets process environment variables after all config
files are parsed. Variables needed to locate or parse config files must be set
in the process environment before Xray starts.
The -test flag tells Xray to test config files only,
without launching the server.
@@ -47,6 +43,9 @@ The -dump flag tells Xray to print the merged config.
`,
}
// The --lua-privileged-pubkey=key flag sets the trusted Ed25519 public key
// for privileged local Lua scripts, encoded as unpadded Base64URL.
func init() {
cmdRun.Run = executeRun // break init loop
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
@@ -58,6 +57,8 @@ var (
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
// TODO: Restore this flag and its help text when Lua privileged signature authorization is re-enabled.
// luaPrivilegedPublicKey = cmdRun.Flag.String("lua-privileged-pubkey", "", "Unpadded Base64URL Ed25519 public key for privileged local Lua scripts.")
/* We have to do this here because Golang's Test will also need to parse flag, before
* main func in this file is run.
@@ -214,6 +215,11 @@ func getConfigFormat() string {
}
func startXray() (core.Server, error) {
// TODO: Restore public key initialization when Lua privileged signature authorization is re-enabled.
// if err := lua.SetPrivilegedPublicKey(*luaPrivilegedPublicKey); err != nil {
// return nil, errors.New("failed to configure Lua privileged authorization").Base(err)
// }
configFiles := getConfigFilePath(true)
c, err := core.LoadConfig(getConfigFormat(), configFiles)
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"context"
"flag"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func TestRunConfigCommands(t *testing.T) {
if os.Getenv("XRAY_TEST_CONFIG_COMMAND") == "1" {
os.Args = append([]string{os.Args[0]}, flag.Args()...)
main()
return
}
dir := t.TempDir()
config := filepath.Join(dir, "config.yaml")
script := filepath.Join(dir, "config.lua")
binary := filepath.Join(dir, "config.pb")
message := filepath.Join(dir, "user.tmsg")
for path, body := range map[string]string{
config: "{}",
script: "{}",
binary: "",
message: `{"type":"xray.app.dispatcher.Config","value":""}`,
} {
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
file, err := os.Open(script)
if err != nil {
t.Fatal(err)
}
defer file.Close()
for _, tc := range []struct {
name string
args []string
code int
input io.Reader
}{
{"format override", []string{"run", "-test", "-format", "json", "-c", config}, 0, nil},
{"invalid extension", []string{"run", "-test", "-format", "json", "-c", script}, 23, nil},
{"default command", []string{"-test", "-format", "json", "-c", script}, 23, nil},
{"protobuf config", []string{"run", "-test", "-c", binary}, 0, nil},
{"protobuf error", []string{"run", "-test", "-format", "protobuf", "-c", script}, 23, nil},
{"merged source", []string{"run", "-test", "-format", "json", "-c", config, "-c", script}, 23, nil},
{"dump", []string{"run", "-dump", "-c", config}, 0, nil},
{"stdin pipe", []string{"run", "-test", "-c", "stdin:"}, 0, nil},
{"stdin file", []string{"run", "-test", "-c", "stdin:"}, 0, file},
{"convert json", []string{"convert", "json", message}, 0, nil},
{"convert pb", []string{"convert", "pb", "-debug", config}, 0, nil},
{"convert pb stdin", []string{"convert", "pb", "-debug", "stdin:"}, 0, file},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
args := append([]string{"-test.run=^TestRunConfigCommands$", "--"}, tc.args...)
cmd := exec.CommandContext(ctx, os.Args[0], args...)
cmd.Env = append(os.Environ(), "XRAY_TEST_CONFIG_COMMAND=1")
cmd.Stdin = strings.NewReader("{}")
if tc.input != nil {
if _, err := file.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
cmd.Stdin = tc.input
}
output, err := cmd.CombinedOutput()
if cmd.ProcessState == nil {
t.Fatal(err)
}
if code := cmd.ProcessState.ExitCode(); code != tc.code {
t.Fatalf("exit code = %d, want %d: %v\n%s", code, tc.code, err, output)
}
if tc.code == 23 && !strings.Contains(string(output), "failed to load config files") {
t.Fatalf("missing config error:\n%s", output)
}
})
}
}
+7
View File
@@ -381,6 +381,13 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
errors.LogInfoInner(context.Background(), err, "pack message")
b.Release()
timer.SetTimeout(0)
return
}
if len(msgBytes) > buf.Size {
b.Release()
timer.SetTimeout(0)
errors.LogError(context.Background(), "DNS response message too large")
return
}
b.Resize(0, int32(len(msgBytes)))
+22 -22
View File
@@ -23,14 +23,14 @@ const (
)
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
state protoimpl.MessageState `protogen:"open.v1"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -65,6 +65,13 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetRewriteAddress() *net.IPOrDomain {
if x != nil {
return x.RewriteAddress
@@ -86,13 +93,6 @@ func (x *Config) GetPortMap() map[string]string {
return nil
}
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetFollowRedirect() bool {
if x != nil {
return x.FollowRedirect
@@ -112,11 +112,11 @@ var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor
const file_proxy_dokodemo_config_proto_rawDesc = "" +
"\n" +
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" +
"\x06Config\x12D\n" +
"\x06Config\x12C\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12D\n" +
"\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" +
"\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" +
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12'\n" +
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12'\n" +
"\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" +
"\n" +
"user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" +
@@ -141,13 +141,13 @@ var file_proxy_dokodemo_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_dokodemo_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.dokodemo.Config
nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry
(*net.IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain
(net.Network)(0), // 3: xray.common.net.Network
(net.Network)(0), // 2: xray.common.net.Network
(*net.IPOrDomain)(nil), // 3: xray.common.net.IPOrDomain
}
var file_proxy_dokodemo_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
2, // 0: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
3, // 1: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
1, // 2: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
+10 -5
View File
@@ -229,14 +229,19 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
delay := h.blockDelay(rule)
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() {
defer common.Close(output)
done := make(chan struct{})
go func() { // a DispatchLink inbound's reader ignores Interrupt, so don't wait for this copy
_ = buf.Copy(input, buf.Discard)
close(done)
}()
select {
case <-done:
case <-time.After(delay):
common.Interrupt(input)
common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
})
defer timer.Stop()
defer common.Close(output)
_ = buf.Copy(input, buf.Discard)
}
return nil
}
+30 -2
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"sync"
"text/template"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
@@ -86,7 +87,13 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
user := server.User
var conn stat.Connection
mbuf, _ := link.Reader.ReadMultiBuffer()
var mbuf buf.MultiBuffer
if reader, ok := link.Reader.(buf.TimeoutReader); ok {
// Don't wait forever: server-first protocols (SMTP, FTP, MySQL...) send nothing until the server greets.
mbuf, _ = reader.ReadMultiBufferTimeout(time.Millisecond * 500)
} else {
mbuf, _ = link.Reader.ReadMultiBuffer()
}
len := mbuf.Len()
firstPayload := bytespool.Alloc(len)
mbuf, _ = buf.SplitBytes(mbuf, firstPayload)
@@ -234,7 +241,8 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
reader := bufio.NewReader(rawConn)
resp, err := http.ReadResponse(reader, req)
if err != nil {
rawConn.Close()
return nil, err
@@ -245,6 +253,11 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
}
// Tunneled bytes (e.g. a server-first greeting) may arrive in the same read as the 200 response.
if n := reader.Buffered(); n > 0 {
early, _ := reader.Peek(n)
return &earlyDataConn{Conn: rawConn, early: early}, nil
}
return rawConn, nil
}
@@ -375,6 +388,21 @@ func (h *http2Conn) Close() error {
return h.out.Close()
}
// earlyDataConn returns the bytes read ahead together with the CONNECT response before reading from Conn.
type earlyDataConn struct {
net.Conn
early []byte
}
func (c *earlyDataConn) Read(p []byte) (int, error) {
if len(c.early) > 0 {
n := copy(p, c.early)
c.early = c.early[n:]
return n, nil
}
return c.Conn.Read(p)
}
func init() {
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewClient(ctx, config.(*ClientConfig))
+1 -1
View File
@@ -327,7 +327,7 @@ func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr,
dns = remoteDNS
}
dev, tnet, _, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, true)
dev, tnet, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, nil)
if err != nil {
return nil, err
}
+13 -9
View File
@@ -22,6 +22,7 @@ import (
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
xtun "github.com/xtls/xray-core/proxy/tun"
"github.com/xtls/xray-core/proxy/wireguard"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
@@ -166,17 +167,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if mtu == 0 {
mtu = masque.MinPacketSize
}
dev, _, gstack, err := wireguard.CreateNetTUN(local, nil, mtu, false)
if err != nil {
return nil, err
}
s := &Server{
validator: users,
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
ctx: core.ToBackgroundDetachedContext(ctx),
mtu: mtu,
dev: dev,
pools: pools,
local: local,
tunnels: make(map[netip.Addr]*serverTunnel),
@@ -187,7 +182,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if content := session.ContentFromContext(ctx); content != nil {
s.sniffing = content.SniffingRequest
}
wireguard.CreateForwarder(gstack, s.handleConnection)
dev, _, err := wireguard.CreateNetTUN(local, nil, mtu, s)
if err != nil {
return nil, err
}
s.dev = dev
return s, nil
}
@@ -500,7 +499,8 @@ func packetDestination(packet []byte) (netip.Addr, bool) {
return netip.Addr{}, false
}
func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
// HandleConnection implements tun.ConnectionHandler
func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
defer conn.Close()
source := net.DestinationFromAddr(conn.RemoteAddr())
addr, _ := netip.AddrFromSlice(source.Address.IP())
@@ -534,9 +534,13 @@ func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
})
errors.LogInfo(ctx, "processing from ", source, " to ", dest)
writer := buf.NewWriter(conn)
if dest.Network == net.Network_TCP {
writer = &xtun.TCPWriter{Writer: writer}
}
link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: buf.NewWriter(conn),
Writer: writer,
}
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err))
+57
View File
@@ -7,13 +7,17 @@ import (
"net/netip"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
"golang.zx2c4.com/wireguard/tun"
)
@@ -326,3 +330,56 @@ func TestPacketDestination(t *testing.T) {
require.False(t, ok)
}
}
// stackConn is a TCP connection as the stack hands it over: it is its own buf.Writer.
type stackConn struct {
net.Conn
closed atomic.Int32
}
func (c *stackConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c *stackConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
buf.ReleaseMulti(mb)
return nil
}
func (c *stackConn) Close() error {
c.closed.Add(1)
return nil
}
func (c *stackConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IP{10, 13, 0, 2}, Port: 1234}
}
// closingDispatcher does what an outbound does when the other side has no more to send.
type closingDispatcher struct {
routing.Dispatcher
conn *stackConn
closed int32
}
func (d *closingDispatcher) DispatchLink(_ context.Context, _ net.Destination, link *transport.Link) error {
common.Close(link.Writer)
d.closed = d.conn.closed.Load()
return nil
}
func TestServerKeepsConnectionOpenWhenWriterCloses(t *testing.T) {
conn := &stackConn{}
dispatcher := &closingDispatcher{conn: conn}
s := &Server{
ctx: context.Background(),
dispatcher: dispatcher,
tunnels: map[netip.Addr]*serverTunnel{netip.MustParseAddr("10.13.0.2"): newServerTunnel(nil, &protocol.MemoryUser{})},
}
s.HandleConnection(conn, net.TCPDestination(net.ParseAddress("192.0.2.1"), 443))
if dispatcher.closed != 0 {
t.Fatal("the connection was closed with its writer, while the client may still be sending")
}
if got := conn.closed.Load(); got != 1 {
t.Fatalf("connection closed %d times, want 1", got)
}
}
+9 -6
View File
@@ -180,14 +180,14 @@ type VisionReader struct {
isUplink bool
conn net.Conn
input *bytes.Reader
rawInput *bytes.Buffer
rawInput **bytes.Buffer
ob *session.Outbound
// internal
directReadCounter stats.Counter
}
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, ob *session.Outbound) *VisionReader {
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput **bytes.Buffer, ob *session.Outbound) *VisionReader {
return &VisionReader{
Reader: reader,
trafficState: trafficState,
@@ -261,12 +261,15 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, inputBuffer)
}
if rawInputBuffer, err := buf.ReadFrom(w.rawInput); err == nil && !rawInputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
}
*w.input = bytes.Reader{} // release memory
w.input = nil
*w.rawInput = bytes.Buffer{} // release memory
if raw := *w.rawInput; raw != nil {
if rawInputBuffer, err := buf.ReadFrom(raw); err == nil && !rawInputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
}
// *raw = bytes.Buffer{} // may not be suitable for Go 1.27.3+
}
w.rawInput = nil
if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil {
+11 -5
View File
@@ -169,11 +169,17 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
}
sessionItem.Unlock()
}
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
})
if err != nil {
continue
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
})
if err != nil {
continue
}
}
payloadBuf := buf.New()
+11 -5
View File
@@ -336,11 +336,17 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
}
sessionItem.Unlock()
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
})
if err != nil {
continue
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
})
if err != nil {
continue
}
}
pBuf := buf.New()
+10 -4
View File
@@ -253,10 +253,16 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
}
sessionItem.Unlock()
}
link, err := sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil {
b.Release()
continue
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil {
b.Release()
continue
}
}
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
+4 -4
View File
@@ -169,8 +169,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
}
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
return errors.New("connection ends").Base(err)
}
@@ -212,8 +212,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return nil
}
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
return errors.New("connection ends").Base(err)
}
+1 -1
View File
@@ -153,7 +153,7 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
return DecodedUDPPacket{}, ErrBadHeaderType
}
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
if diff > 30 {
return DecodedUDPPacket{}, ErrBadTimestamp
}
+2 -2
View File
@@ -49,12 +49,12 @@ func (f *SlidingWindow) Add(counter uint64) {
if counter > f.last {
lastBlockIndex := f.last >> swBlockBitLog
diff := int(blockIndex - lastBlockIndex)
diff := blockIndex - lastBlockIndex
if diff > swRingBlocks {
diff = swRingBlocks
}
for i := 0; i < diff; i++ {
for i := 0; i < int(diff); i++ {
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
f.ring[lastBlockIndex] = 0
}
+8 -21
View File
@@ -18,12 +18,8 @@ import (
)
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
if s.currentConn.Load() == nil {
s.currentConn.Store(conn)
}
if s.timer != nil {
s.timer.Update()
}
s.currentConn.Store(conn)
s.timer.Update()
}
func (s *ServerUDPSession) WriteToClient(b []byte) error {
@@ -57,19 +53,16 @@ func (s *ServerUDPSession) EnsureLink(
policyManager policy.Manager,
responseEncoder func(dest net.Destination, payload []byte) ([]byte, error),
) (*transport.Link, error) {
s.UpdateConn(conn)
if link := s.link.Load(); link != nil {
return link, nil
}
s.Lock()
defer s.Unlock()
if link := s.link.Load(); link != nil {
s.UpdateConn(conn)
return link, nil
}
s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx)
inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil {
@@ -94,7 +87,6 @@ func (s *ServerUDPSession) EnsureLink(
return nil, err
}
s.link.Store(link)
sessionPolicy := policyManager.ForLevel(level)
s.timer = signal.CancelAfterInactivity(sessCtx, func() {
if s.manager != nil {
@@ -103,6 +95,7 @@ func (s *ServerUDPSession) EnsureLink(
s.Close()
cancel()
}, sessionPolicy.Timeouts.ConnectionIdle)
s.link.Store(link)
go handleUDPResponse(s, link, dest, responseEncoder)
return link, nil
@@ -118,19 +111,13 @@ func ResetTCPConn(conn net.Conn) {
}
func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) {
defer func() {
if s.timer != nil {
s.timer.SetTimeout(0)
}
}()
defer s.timer.SetTimeout(0)
for {
resMb, err := link.Reader.ReadMultiBuffer()
if err != nil {
return
}
if s.timer != nil {
s.timer.Update()
}
s.timer.Update()
for i, rb := range resMb {
b := rb.Bytes()
if encode != nil {
+12 -4
View File
@@ -11,6 +11,7 @@ import (
"sync"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/antireplay"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -253,7 +254,7 @@ func ReadClientRequestHeaderWithFixed(reader *StreamReader, fixedChunk []byte) (
}
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
if diff > 30 {
return nil, ErrBadTimestamp
}
@@ -430,7 +431,7 @@ func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt [
}
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
diff := math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))
if diff > 30 {
return nil, ErrBadTimestamp
}
@@ -629,6 +630,7 @@ func InitServerStream(conn net.Conn, method *CipherMethod, psk, saltSlice []byte
func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.Reader, writer buf.Writer, link *transport.Link) error {
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
defer timer.SetTimeout(0)
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
requestDone := func() error {
@@ -644,6 +646,12 @@ func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
}
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil {
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
return errors.New("connection ends").Base(err)
}
return nil
}
+5 -4
View File
@@ -2,10 +2,10 @@ package socks
import (
"context"
"net"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal"
)
@@ -14,6 +14,9 @@ func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, expectedRemote *ne
PacketConn: udpConn,
AssociatedTCPConn: tcpConn,
}
t.Timer = signal.CancelAfterInactivity(context.Background(), func() {
t.Close()
}, net.ConnIdleTimeout)
t.ExpectedRemote.Store(expectedRemote)
return t
}
@@ -60,9 +63,7 @@ func (c *TempUDPConn) RemoteAddr() net.Addr {
}
func (c *TempUDPConn) SetTimeout(d time.Duration) {
c.Timer = signal.CancelAfterInactivity(context.Background(), func() {
c.Close()
}, d)
c.Timer.SetTimeout(d)
}
func (c *TempUDPConn) Close() error {
+4 -2
View File
@@ -219,8 +219,10 @@ After the start network adapter with the name you chose in the config will be cr
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops.
With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep two kinds of traffic of every program but Xray itself from leaving outside the TUN, each chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`:
- `"dns"` (needs `dns`, the config is rejected otherwise): DNS (port 53) only goes through the TUN. Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. On Windows 11 and Server 2022 and later, where those queries may also go over HTTPS or TLS, Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). The `dns` servers therefore have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep traffic of every program but Xray itself from leaving outside the TUN, each kind chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`:
- `"tcp/<port>"`, `"udp/<port>"`: connections to that TCP or UDP port only go through the TUN, e.g. `"tcp/853"`.
- `"windnsclient"`: Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). On Windows 11 and Server 2022 and later, it may send name queries over HTTPS or TLS, to any port. Earlier versions only query port 53, so there it does nothing.
- `"dns"`: stands for `"udp/53"`, `"tcp/53"`, `"windnsclient"`, `"tcp/853"` (DNS over TLS) and `"udp/853"` (DNS over QUIC). Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. Keeping the system's DNS inside the TUN like this, with `"dns"`, `"udp/53"`, `"tcp/53"` or `"windnsclient"`, needs `dns` (the config is rejected otherwise). The `dns` servers then have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those.
With the filters in place, Xray's own connections out also get past Windows Firewall's block rules (other firewalls may still block them), while connections to Xray's inbounds stay subject to them.
+2 -3
View File
@@ -7,12 +7,11 @@
package tun
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
+18 -12
View File
@@ -56,20 +56,21 @@ func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
return nil
}
// tunTCPWriter is the writing side of a TCP connection for the dispatcher. It has no Close: the outbound closes
// its writer when the other side has no more to send, and the system may still be sending
type tunTCPWriter struct {
writer buf.Writer
counter stats.Counter
// TCPWriter is the writing side of a TCP connection of the stack for the dispatcher. It has no Close: the outbound
// closes its writer when the other side has no more to send, and the system may still be sending
type TCPWriter struct {
Writer buf.Writer
// Counter counts what is written, if there is one
Counter stats.Counter
}
func (w *tunTCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
func (w *TCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
size := mb.Len()
if err := w.writer.WriteMultiBuffer(mb); err != nil {
if err := w.Writer.WriteMultiBuffer(mb); err != nil {
return err
}
if w.counter != nil {
w.counter.Add(int64(size))
if w.Counter != nil {
w.Counter.Add(int64(size))
}
return nil
}
@@ -143,7 +144,8 @@ func (t *Handler) Start() error {
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
return errors.New("iface not found")
}
return c.Control(func(fd uintptr) {
var controlErr, sockErr error
controlErr = c.Control(func(fd uintptr) {
addrPort, _ := netip.ParseAddrPort(address)
// skip loopback
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
@@ -151,9 +153,13 @@ func (t *Handler) Start() error {
}
err := setinterface(network, address, fd, iface)
if err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
sockErr = errors.New("[tun] falied to set interface").Base(err)
}
})
if sockErr != nil {
return sockErr
}
return controlErr
})
}
@@ -243,7 +249,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn), Counter: t.uplinkCounter}
writer := buf.NewWriter(conn)
if destination.Network != net.Network_UDP {
writer = &tunTCPWriter{writer: writer, counter: t.downlinkCounter}
writer = &TCPWriter{Writer: writer, Counter: t.downlinkCounter}
} else if t.downlinkCounter != nil {
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
}
-106
View File
@@ -1,106 +0,0 @@
package icmp
import (
"github.com/xtls/xray-core/common/errors"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func ProtocolLabel(netProto tcpip.NetworkProtocolNumber) string {
switch netProto {
case header.IPv4ProtocolNumber:
return "ipv4"
case header.IPv6ProtocolNumber:
return "ipv6"
default:
return "unknown"
}
}
func ParseEchoRequest(netProto tcpip.NetworkProtocolNumber, message []byte) (uint16, uint16, bool) {
switch netProto {
case header.IPv4ProtocolNumber:
if len(message) < header.ICMPv4MinimumSize {
return 0, 0, false
}
icmpHdr := header.ICMPv4(message)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
return 0, 0, false
}
return icmpHdr.Ident(), icmpHdr.Sequence(), true
case header.IPv6ProtocolNumber:
if len(message) < header.ICMPv6MinimumSize {
return 0, 0, false
}
icmpHdr := header.ICMPv6(message)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
return 0, 0, false
}
return icmpHdr.Ident(), icmpHdr.Sequence(), true
default:
return 0, 0, false
}
}
func RewriteChecksum(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
switch netProto {
case header.IPv4ProtocolNumber:
if len(message) < header.ICMPv4MinimumSize {
return errors.New("invalid icmpv4 packet")
}
icmpHdr := header.ICMPv4(message)
icmpHdr.SetChecksum(0)
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0)))
return nil
case header.IPv6ProtocolNumber:
if len(message) < header.ICMPv6MinimumSize {
return errors.New("invalid icmpv6 packet")
}
icmpHdr := header.ICMPv6(message)
icmpHdr.SetChecksum(0)
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr[:header.ICMPv6MinimumSize],
Src: srcIP,
Dst: dstIP,
PayloadCsum: checksum.Checksum(icmpHdr.Payload(), 0),
PayloadLen: len(icmpHdr.Payload()),
}))
return nil
default:
return errors.New("unsupported icmp network protocol")
}
}
func BuildLocalEchoReply(netProto tcpip.NetworkProtocolNumber, request []byte, srcIP, dstIP tcpip.Address) ([]byte, error) {
reply := append([]byte(nil), request...)
switch netProto {
case header.IPv4ProtocolNumber:
if len(reply) < header.ICMPv4MinimumSize {
return nil, errors.New("invalid icmpv4 echo packet")
}
icmpHdr := header.ICMPv4(reply)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
return nil, errors.New("not an icmpv4 echo request")
}
reply[0] = byte(header.ICMPv4EchoReply)
case header.IPv6ProtocolNumber:
if len(reply) < header.ICMPv6MinimumSize {
return nil, errors.New("invalid icmpv6 echo packet")
}
icmpHdr := header.ICMPv6(reply)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
return nil, errors.New("not an icmpv6 echo request")
}
reply[0] = byte(header.ICMPv6EchoReply)
default:
return nil, errors.New("unsupported icmp network protocol")
}
if err := RewriteChecksum(netProto, reply, srcIP, dstIP); err != nil {
return nil, err
}
return reply, nil
}
-174
View File
@@ -1,174 +0,0 @@
package icmp
import (
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func TestParseEchoRequest(t *testing.T) {
t.Run("ipv4 echo", func(t *testing.T) {
var zero tcpip.Address
packet := []byte{
byte(header.ICMPv4Echo), 0,
0, 0,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb,
}
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
t.Fatal(err)
}
ident, sequence, ok := ParseEchoRequest(header.IPv4ProtocolNumber, packet)
if !ok {
t.Fatal("expected ipv4 echo request to parse")
}
if ident != 0x1234 || sequence != 0x5678 {
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
}
})
t.Run("ipv6 echo", func(t *testing.T) {
packet := []byte{
byte(header.ICMPv6EchoRequest), 0,
0, 0,
0xab, 0xcd,
0xef, 0x01,
0xaa, 0xbb,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
t.Fatal(err)
}
ident, sequence, ok := ParseEchoRequest(header.IPv6ProtocolNumber, packet)
if !ok {
t.Fatal("expected ipv6 echo request to parse")
}
if ident != 0xabcd || sequence != 0xef01 {
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
}
})
}
func TestRewriteChecksum(t *testing.T) {
t.Run("ipv4", func(t *testing.T) {
var zero tcpip.Address
packet := []byte{
byte(header.ICMPv4Echo), 0,
0xff, 0xff,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
t.Fatal(err)
}
icmpHdr := header.ICMPv4(packet)
if got, want := icmpHdr.Checksum(), header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksumPayloadV4(icmpHdr.Payload())); got != want {
t.Fatalf("unexpected ipv4 checksum: got %x want %x", got, want)
}
})
t.Run("ipv6", func(t *testing.T) {
packet := []byte{
byte(header.ICMPv6EchoReply), 0,
0xff, 0xff,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
t.Fatal(err)
}
icmpHdr := header.ICMPv6(packet)
want := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr[:header.ICMPv6MinimumSize],
Src: src,
Dst: dst,
PayloadLen: len(icmpHdr.Payload()),
PayloadCsum: checksumPayloadV6(icmpHdr.Payload()),
})
if got := icmpHdr.Checksum(); got != want {
t.Fatalf("unexpected ipv6 checksum: got %x want %x", got, want)
}
})
}
func TestBuildLocalEchoReply(t *testing.T) {
t.Run("ipv4", func(t *testing.T) {
request := []byte{
byte(header.ICMPv4Echo), 0,
0, 0,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
src := tcpip.Address{}
dst := tcpip.Address{}
if err := RewriteChecksum(header.IPv4ProtocolNumber, request, src, dst); err != nil {
t.Fatal(err)
}
reply, err := BuildLocalEchoReply(header.IPv4ProtocolNumber, request, dst, src)
if err != nil {
t.Fatal(err)
}
if request[0] != byte(header.ICMPv4Echo) {
t.Fatal("request mutated")
}
icmpHdr := header.ICMPv4(reply)
if icmpHdr.Type() != header.ICMPv4EchoReply || icmpHdr.Code() != header.ICMPv4UnusedCode {
t.Fatalf("unexpected ipv4 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
}
if icmpHdr.Ident() != 0x1234 || icmpHdr.Sequence() != 0x5678 {
t.Fatalf("unexpected ipv4 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
}
})
t.Run("ipv6", func(t *testing.T) {
request := []byte{
byte(header.ICMPv6EchoRequest), 0,
0, 0,
0xab, 0xcd,
0xef, 0x01,
0xaa, 0xbb, 0xcc,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, request, src, dst); err != nil {
t.Fatal(err)
}
reply, err := BuildLocalEchoReply(header.IPv6ProtocolNumber, request, dst, src)
if err != nil {
t.Fatal(err)
}
if request[0] != byte(header.ICMPv6EchoRequest) {
t.Fatal("request mutated")
}
icmpHdr := header.ICMPv6(reply)
if icmpHdr.Type() != header.ICMPv6EchoReply || icmpHdr.Code() != header.ICMPv6UnusedCode {
t.Fatalf("unexpected ipv6 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
}
if icmpHdr.Ident() != 0xabcd || icmpHdr.Sequence() != 0xef01 {
t.Fatalf("unexpected ipv6 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
}
})
}
func checksumPayloadV4(payload []byte) uint16 {
return checksum.Checksum(payload, 0)
}
func checksumPayloadV6(payload []byte) uint16 {
return checksum.Checksum(payload, 0)
}
+237 -7
View File
@@ -2,8 +2,14 @@ package tun
import (
"encoding/binary"
"math"
"math/bits"
"net/netip"
"slices"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
)
const (
@@ -16,18 +22,38 @@ const (
protoTCP = 6
protoUDP = 17
protoICMPv6 = 58
// the smallest that a link is believed to take whatever is said about it: what every IPv4 host takes,
// and the smallest link that IPv6 runs on
ipv4SmallestMTU = 576
ipv6SmallestMTU = 1280
// the header that an IPv6 packet has in front of each of its fragments, and its number
fragmentHeaderSize = 8
protoFragment = 44
// fragmentsMost is how many packets can be waiting for the rest of their fragments,
// fragmentsTime how long they are waited for at most
fragmentsMost = 64
fragmentsTime = 30 * time.Second
)
// packet is an IP packet with its header read
type packet struct {
proto byte
src, dst netip.Addr
// payload is what follows the IP header
// payload is what follows the IP header, without the last rest bytes of it when they are in other buffers
payload []byte
rest int
// fragment is set if payload is a part of what follows the IP header: the one at offset of the packet
// with the number id, which has more after it or not
fragment bool
more bool
offset int
id uint32
}
// parsePacket reads the IP header of b. Fragments are not accepted: what the system sends to the tun fits its MTU
func parsePacket(b []byte) (p packet, ok bool) {
// parsePacket reads the IP header of a packet that is b and rest bytes after it
func parsePacket(b []byte, rest int) (p packet, ok bool) {
if len(b) < ipv4HeaderSize {
return p, false
}
@@ -35,25 +61,28 @@ func parsePacket(b []byte) (p packet, ok bool) {
case 4:
size := int(b[0]&0xf) * 4
total := int(binary.BigEndian.Uint16(b[2:]))
if size < ipv4HeaderSize || total < size || total > len(b) || binary.BigEndian.Uint16(b[6:])&0x3fff != 0 {
if size < ipv4HeaderSize || total < size || total > len(b)+rest {
return p, false
}
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
p.fragment, p.more, p.offset, p.id = true, f&0x2000 != 0, int(f&0x1fff)*8, uint32(binary.BigEndian.Uint16(b[4:]))
}
p.proto = b[9]
p.src = netip.AddrFrom4([4]byte(b[12:16]))
p.dst = netip.AddrFrom4([4]byte(b[16:20]))
p.payload = b[size:total]
p.payload, p.rest = b[size:min(total, len(b))], max(total-len(b), 0)
case 6:
if len(b) < ipv6HeaderSize {
return p, false
}
total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:]))
if total > len(b) {
if total > len(b)+rest {
return p, false
}
p.proto = b[6]
p.src = netip.AddrFrom16([16]byte(b[8:24]))
p.dst = netip.AddrFrom16([16]byte(b[24:40]))
p.payload = b[ipv6HeaderSize:total]
p.payload, p.rest = b[ipv6HeaderSize:min(total, len(b))], max(total-len(b), 0)
// hop-by-hop options, routing, destination options
for p.proto == 0 || p.proto == 43 || p.proto == 60 {
if len(p.payload) < 8 {
@@ -65,12 +94,55 @@ func parsePacket(b []byte) (p packet, ok bool) {
}
p.proto, p.payload = p.payload[0], p.payload[size:]
}
if p.proto == protoFragment {
f := p.payload
if len(f) < fragmentHeaderSize {
return p, false
}
p.fragment, p.more, p.offset, p.id = true, f[3]&1 != 0, int(binary.BigEndian.Uint16(f[2:])&^7), binary.BigEndian.Uint32(f[4:])
p.proto, p.payload = f[0], f[fragmentHeaderSize:]
}
default:
return p, false
}
return p, true
}
// verified reports whether the checksum of what the packet carries is right
func (p packet) verified() bool {
data := p.payload
switch p.proto {
case protoICMPv4:
return checksum(sum(data, 0)) == 0
case protoUDP:
if len(data) < udpHeaderSize {
return false
}
// the packet may be longer than what it carries
size := int(binary.BigEndian.Uint16(data[4:]))
if size < udpHeaderSize || size > len(data) {
return false
}
if p.src.Is4() && data[6]|data[7] == 0 {
// zero stands for no checksum
return true
}
data = data[:size]
case protoTCP, protoICMPv6:
default:
return true
}
pseudo := uint64(p.proto) + uint64(len(data))
if p.src.Is4() {
src, dst := p.src.As4(), p.dst.As4()
pseudo = sum(dst[:], sum(src[:], pseudo))
} else {
src, dst := p.src.As16(), p.dst.As16()
pseudo = sum(dst[:], sum(src[:], pseudo))
}
return checksum(sum(data, pseudo)) == 0
}
// putIPHeader writes the header of a packet from src to dst into b, without its length, and returns the size of the header
func putIPHeader(b []byte, proto byte, src, dst netip.Addr) int {
if src.Is4() {
@@ -159,3 +231,161 @@ func checksum(s uint64) uint16 {
s = s>>16 + s&0xffff
return ^uint16(s)
}
// writeFragments writes a packet that is larger than mtu to tun in fragments that are not. header is its IP header
// with what follows that up to payload, which is no more than the header of a UDP packet. id is the number
// of the packet if it is an IPv6 one
func writeFragments(tun Tun, mtu int, id uint32, header, payload []byte) error {
var b [ipv6HeaderSize + fragmentHeaderSize + udpHeaderSize]byte
size := ipv4HeaderSize
if header[0]>>4 == 6 {
size = ipv6HeaderSize
}
n := copy(b[:], header[:size])
if size == ipv6HeaderSize {
// the number goes into a header of its own, behind the IP header. That of IPv4 has it
b[6], b[n] = protoFragment, header[6]
binary.BigEndian.PutUint32(b[n+4:], id)
n += fragmentHeaderSize
}
// first goes in front of payload, in the first fragment
first := header[size:]
if n+len(first) > len(b) {
return errors.New("no room for a header of ", len(first), " bytes in a fragment")
}
// every fragment but the last is a number of 8 bytes
room, total := (mtu-n)&^7, len(first)+len(payload)
if room < 8 {
return errors.New("no room for a fragment in an MTU of ", mtu)
}
for offset := 0; offset < total; {
h, part := b[:n], min(room, total-offset)
last := offset+part == total
if size == ipv6HeaderSize {
f := uint16(offset)
if !last {
f |= 1
}
binary.BigEndian.PutUint16(b[size+2:], f)
} else {
f := uint16(offset / 8)
if !last {
f |= 0x2000
}
binary.BigEndian.PutUint16(b[6:], f)
}
setIPLength(b[:size], n+part)
data := payload[max(offset-len(first), 0):min(offset+part-len(first), len(payload))]
if offset == 0 {
h = b[:n+copy(b[n:], first)]
}
if err := tun.WritePacket(h, data); err != nil {
return err
}
offset += part
}
return nil
}
// fragmented is a packet of which not every fragment has come yet
type fragmented struct {
// data is what follows its IP header, as far as its last fragment that has come goes
data []byte
// parts are what of data has come, in order and apart from each other
parts [][2]int
// size is that of data when all of it has come, known from the last fragment
size int
known bool
since time.Time
}
// reassembly puts the packets that come in fragments together
type reassembly struct {
mu sync.Mutex
packets map[fragments]*fragmented
}
// fragments is what the fragments of a packet have in common
type fragments struct {
src, dst netip.Addr
proto byte
id uint32
}
// add takes the fragment p of a packet. With the last one that was missing, it returns the packet.
// A packet whose fragments do not fit each other is given up
func (r *reassembly) add(p packet) (packet, bool) {
fragment, offset, end := p.payload, p.offset, p.offset+len(p.payload)
// what an IP packet can carry
most := math.MaxUint16
if p.src.Is4() {
most -= ipv4HeaderSize
}
if len(fragment) == 0 || end > most || p.more && len(fragment)%8 != 0 {
return p, false
}
key, now := fragments{p.src, p.dst, p.proto, p.id}, time.Now()
r.mu.Lock()
defer r.mu.Unlock()
f := r.packets[key]
if f != nil && now.Sub(f.since) > fragmentsTime {
// what is left of a packet whose other fragments were lost: this is one of another packet with its number
f = nil
}
if f == nil {
if _, stale := r.packets[key]; !stale && len(r.packets) >= fragmentsMost {
// the one that is waited for the longest makes room, not the one that comes now
var longest fragments
var since time.Time
for other, old := range r.packets {
if since.IsZero() || old.since.Before(since) {
longest, since = other, old.since
}
}
delete(r.packets, longest)
}
if r.packets == nil {
r.packets = make(map[fragments]*fragmented)
}
f = &fragmented{since: now}
r.packets[key] = f
}
for _, part := range f.parts {
if offset >= part[0] && end <= part[1] {
// came already
return p, false
}
if offset < part[1] && part[0] < end {
// fragments of one packet do not overlap: these are of two, or made to be taken for something else
delete(r.packets, key)
return p, false
}
}
if f.known && end > f.size || !p.more && len(f.data) > end {
// past its end, or an end before what came
delete(r.packets, key)
return p, false
}
if !p.more {
f.size, f.known = end, true
}
if len(f.data) < end {
f.data = append(f.data, make([]byte, end-len(f.data))...)
}
copy(f.data[offset:], fragment)
i := 0
for i < len(f.parts) && f.parts[i][1] < offset {
i++
}
j := i
for ; j < len(f.parts) && f.parts[j][0] <= end; j++ {
offset, end = min(offset, f.parts[j][0]), max(end, f.parts[j][1])
}
f.parts = slices.Replace(f.parts, i, j, [2]int{offset, end})
if !f.known || f.parts[0] != [2]int{0, f.size} {
return p, false
}
delete(r.packets, key)
p.payload, p.fragment = f.data[:f.size], false
return p, true
}
+399 -55
View File
@@ -3,7 +3,12 @@ package tun
import (
"context"
"encoding/binary"
"io"
"math"
"math/rand/v2"
gonet "net"
"net/netip"
"slices"
"sync"
"sync/atomic"
"time"
@@ -15,8 +20,16 @@ import (
// Stack interface implement ip protocol stack, bridging raw network packets and data streams
type Stack interface {
// Start makes the stack read the packets of its tun
Start() error
Close() error
// Input takes a packet of a tun that hands them over instead of being read. The packet is not kept,
// but it may be written to. The packets of one connection have to come one after another
Input(packet []byte)
// DialTCP opens a connection to address from the one of StackOptions.Addresses that is of its family
DialTCP(ctx context.Context, address netip.AddrPort) (net.Conn, error)
// DialUDP opens a port on StackOptions.Addresses. What anyone sends to the port is read from it
DialUDP() (net.PacketConn, error)
}
// StackOptions for the stack implementation
@@ -24,6 +37,12 @@ type StackOptions struct {
Tun Tun
MTU uint32
IdleTimeout time.Duration
// Addresses are what the stack opens connections from
Addresses []netip.Addr
// Remote is set if the tun is a tunnel to another system and not the tun of this one: what comes through it
// has come a long way, so its checksums are looked at, nothing but that way limits the windows, and the
// segments of TCP carry the times of both sides if the other side has them
Remote bool
}
// flow is what the packets of one connection have in common, src is the side of the system
@@ -33,7 +52,8 @@ type flow struct {
// stack turns the packets that the system sends to the tun into connections and back.
// It has only what the two need to talk through a tun, and a local link when the system passes on
// what others send, see tcpConn
// what others send, see tcpConn. The tun may also be a tunnel to a system that is far away, and the stack
// the one that opens the connections
type stack struct {
ctx context.Context
tun Tun
@@ -42,32 +62,62 @@ type stack struct {
segmenter segmenter
// window is the window of every connection
window int32
// handle is given every new connection
// verify is set if the checksums of what comes are looked at
verify bool
// stamps is set if connections carry the times of RFC 7323 when the other side does
stamps bool
// handle is given every new connection that the other side opens. Without it those are refused
handle func(conn net.Conn, destination net.Destination)
udp *udpConnectionHandler
// addresses are what connections are opened from
addresses []netip.Addr
// bufs is what read reads a packet into if the tun is a segmenter, one after the other, the headers in the first.
// One that keep took is nil. joined is for a packet that is in more than one and is needed in one piece
bufs buf.MultiBuffer
joined []byte
mu sync.Mutex
conns map[flow]*tcpConn
closed bool
// opening is how many of conns wait for the answer to their SYN-ACK
mu sync.Mutex
conns map[flow]*tcpConn
// sockets are the ports that DialUDP has opened
sockets map[netip.AddrPort]*udpSocket
closed bool
// opening is how many of conns wait for the answer to their SYN or SYN-ACK
opening atomic.Int32
// fragments are the packets that are larger than a link on their way and come in parts.
// identification numbers the ones that leave like that: a counter for each of a few groups of destinations,
// each from a start of its own, so that no two of them that go one way meet, and no destination learns how much
// goes to the others
fragments reassembly
identification [64]atomic.Uint32
}
// NewStack builds new ip stack
func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) {
// NewStack builds new ip stack. Without a handler it takes no connections, only opens them
func NewStack(ctx context.Context, options StackOptions, handler ConnectionHandler) (Stack, error) {
s := &stack{
ctx: ctx,
tun: options.Tun,
mtu: int(options.MTU),
window: tcpWindow,
handle: handler.HandleConnection,
conns: make(map[flow]*tcpConn),
ctx: ctx,
tun: options.Tun,
mtu: int(options.MTU),
window: tcpWindow,
addresses: options.Addresses,
verify: options.Remote,
stamps: options.Remote,
conns: make(map[flow]*tcpConn),
sockets: make(map[netip.AddrPort]*udpSocket),
}
if t, ok := options.Tun.(segmenter); ok && t.Segments() {
s.segmenter = t
s.window = tcpLargeWindow
}
s.udp = newUdpConnectionHandler(s.handle, s.writeUDP)
if options.Remote {
s.window = tcpLargeWindow
}
for i := range s.identification {
s.identification[i].Store(rand.Uint32())
}
if handler != nil {
s.handle = handler.HandleConnection
s.udp = newUdpConnectionHandler(s.handle, s.writeUDP)
}
return s, nil
}
@@ -85,6 +135,8 @@ func (s *stack) Close() error {
for _, c := range s.conns {
conns = append(conns, c)
}
sockets := s.sockets
s.sockets = nil
s.mu.Unlock()
for _, c := range conns {
c.mu.Lock()
@@ -93,37 +145,155 @@ func (s *stack) Close() error {
}
c.mu.Unlock()
}
for _, u := range sockets {
u.queue.close()
}
return nil
}
// read takes the packets of the tun until it is closed
func (s *stack) read() {
// a tun that does not split segments gives them as large as a packet can be
b := make([]byte, packetRoom+1<<16)
for {
b, err := s.tun.ReadPacket(b)
if err != nil {
s.mu.Lock()
closed := s.closed
s.mu.Unlock()
if !closed {
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
var err error
if s.segmenter != nil {
err = s.readSegments()
} else {
// as large as a packet can be, whatever the MTU is
b := make([]byte, packetRoom+1<<16)
for err == nil {
var p []byte
if p, err = s.tun.ReadPacket(b); err == nil {
s.Input(p)
}
return
}
p, ok := parsePacket(b)
if !ok {
continue
}
switch p.proto {
case protoTCP:
s.handleTCP(p)
case protoUDP:
s.handleUDP(p)
case protoICMPv4, protoICMPv6:
s.handleICMP(p)
}
}
s.mu.Lock()
closed := s.closed
s.mu.Unlock()
if !closed {
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
}
}
// readSegments is read for a tun that does not split segments and gives them as large as a packet can be.
// They are read into buffers for a connection to keep what is large as it is, see keep
func (s *stack) readSegments() error {
s.bufs = make(buf.MultiBuffer, (1<<16)/buf.Size)
defer buf.ReleaseMulti(s.bufs)
parts := make([][]byte, len(s.bufs))
var r room
for {
for i, b := range s.bufs {
if b == nil {
s.bufs[i] = buf.New()
_, _ = s.bufs[i].ReadFrom(&r)
parts[i] = r.b
}
}
n, err := s.segmenter.ReadParts(parts)
if err != nil {
return err
}
s.input(parts[0][:min(n, buf.Size)], max(n-buf.Size, 0))
}
}
// room takes what it is given to read into as read: that is the room of a Buffer as it is, not cleared
type room struct{ b []byte }
func (r *room) Read(b []byte) (int, error) {
r.b = b
return len(b), nil
}
// Input implements Stack. Several tunnels may hand over their packets at once
func (s *stack) Input(b []byte) {
s.input(b, 0)
}
// input takes a packet that is b and rest bytes after it, in the bufs that follow the one that b is in
func (s *stack) input(b []byte, rest int) {
p, ok := parsePacket(b, rest)
if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() {
// nothing comes from many at once, and an address that stands for this machine itself is not one
// that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which
// would be dialed as that: ::ffff:0.0.0.0 is this machine as well
return
}
if s.verify && b[0]>>4 == 4 && checksum(sum(b[:int(b[0]&0xf)*4], 0)) != 0 {
return
}
// what was put together may be of two packets with one number, its checksum tells
verify := s.verify || p.fragment
if p.rest != 0 && (verify || p.proto != protoTCP) {
// only a segment of TCP that is not looked at can stay in pieces
p.payload, p.rest = s.whole(p.payload, p.rest), 0
}
if p.fragment {
p, ok = s.fragments.add(p)
}
if !ok || verify && !p.verified() {
return
}
switch p.proto {
case protoTCP:
s.handleTCP(p)
case protoUDP:
s.handleUDP(p)
case protoICMPv4, protoICMPv6:
s.handleICMP(p)
}
}
// whole returns in one piece what is b and rest bytes after it, in the bufs that follow the one that b is in
func (s *stack) whole(b []byte, rest int) []byte {
if rest == 0 {
return b
}
s.joined = append(s.joined[:0], b...)
for i := 1; rest > 0; i++ {
n := min(rest, buf.Size)
s.joined = append(s.joined, s.bufs[i].BytesTo(int32(n))...)
rest -= n
}
return s.joined
}
// keep adds to mb what is b and rest bytes after it, in the bufs that follow the one that b is in: nothing is copied,
// the buffers are taken for it and no longer read into
func (s *stack) keep(mb buf.MultiBuffer, b []byte, rest int) buf.MultiBuffer {
// b ends where what was read into the first one does, or before it
first := s.bufs[0]
from := first.Cap() - int32(cap(b))
first.Resize(from, from+int32(len(b)))
mb, s.bufs[0] = append(mb, first), nil
for i := 1; rest > 0; i++ {
n := min(rest, buf.Size)
s.bufs[i].Resize(0, int32(n))
mb, s.bufs[i] = append(mb, s.bufs[i]), nil
rest -= n
}
return mb
}
// local returns the address that packets to remote are sent from
func (s *stack) local(remote netip.Addr) (netip.Addr, error) {
for _, a := range s.addresses {
if a.Is4() == remote.Is4() {
return a, nil
}
}
return netip.Addr{}, errors.New("no address to reach ", remote, " from")
}
// ephemeral returns a port for what is opened from here: the first that free takes, from a random one on
func ephemeral(free func(port uint16) bool) (uint16, bool) {
start := rand.Uint32()
for i := range uint32(1 << 15) {
if port := uint16(1<<15 | (start+i)&(1<<15-1)); free(port) {
return port, true
}
}
return 0, false
}
func (s *stack) handleTCP(p packet) {
@@ -146,6 +316,7 @@ func (s *stack) handleTCP(p packet) {
window: binary.BigEndian.Uint16(t[14:]),
options: t[tcpHeaderSize:size],
payload: t[size:],
rest: p.rest,
}
s.mu.Lock()
@@ -154,7 +325,7 @@ func (s *stack) handleTCP(p packet) {
if c != nil && c.input(seg) {
return
}
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn {
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn || s.handle == nil {
if seg.flags&tcpRst == 0 {
s.refuse(f, seg)
}
@@ -164,7 +335,8 @@ func (s *stack) handleTCP(p packet) {
if s.opening.Load() >= tcpOpening {
return
}
c = newTCPConn(s, f, seg)
c = newTCPConn(s, f)
c.synchronize(seg)
s.mu.Lock()
if s.closed {
s.mu.Unlock()
@@ -174,7 +346,7 @@ func (s *stack) handleTCP(p packet) {
s.mu.Unlock()
s.opening.Add(1)
c.mu.Lock()
c.sendSynAck()
c.sendSyn()
c.arm(tcpTick)
c.mu.Unlock()
}
@@ -191,7 +363,7 @@ func (s *stack) refuse(f flow, seg segment) {
binary.BigEndian.PutUint32(t[4:], seg.ack)
t[13] = tcpRst
} else {
ack := seg.seq + uint32(len(seg.payload))
ack := seg.seq + uint32(len(seg.payload)+seg.rest)
if seg.flags&(tcpSyn|tcpFin) != 0 {
ack++
}
@@ -224,51 +396,189 @@ func (s *stack) handleUDP(p packet) {
if size < udpHeaderSize || size > len(u) {
return
}
var socket *udpSocket
if s.handle == nil {
// only for a port that was opened from here
s.mu.Lock()
socket = s.sockets[netip.AddrPortFrom(p.dst, binary.BigEndian.Uint16(u[2:]))]
s.mu.Unlock()
if socket == nil {
return
}
}
// the packet waits for its reader, longer than read keeps it
b := buf.NewWithSize(int32(size - udpHeaderSize))
b.Write(u[udpHeaderSize:size])
src := net.UDPDestination(net.IPAddress(p.src.AsSlice()), net.Port(binary.BigEndian.Uint16(u)))
dst := net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:])))
s.udp.HandlePacket(src, dst, b)
if socket == nil {
s.udp.HandlePacket(src, net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:]))), b)
} else if b.UDP = &src; !socket.queue.push(b) {
b.Release()
}
}
// writeUDP writes a packet with the payload from src to dst to the tun
func (s *stack) writeUDP(payload []byte, src net.Destination, dst net.Destination) error {
srcIP, _ := netip.AddrFromSlice(src.Address.IP())
dstIP, _ := netip.AddrFromSlice(dst.Address.IP())
srcIP, dstIP = srcIP.Unmap(), dstIP.Unmap()
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() {
return errors.New("failed to write udp packet from ", src, " to ", dst)
return s.sendUDP(payload, netip.AddrPortFrom(srcIP, uint16(src.Port)), netip.AddrPortFrom(dstIP, uint16(dst.Port)))
}
func (s *stack) sendUDP(payload []byte, src, dst netip.AddrPort) error {
srcIP, dstIP := src.Addr().Unmap(), dst.Addr().Unmap()
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() || len(payload) > math.MaxUint16-udpHeaderSize {
return errors.New("failed to write udp packet of ", len(payload), " bytes from ", src, " to ", dst)
}
var b [ipv6HeaderSize + udpHeaderSize]byte
n := putIPHeader(b[:], protoUDP, srcIP, dstIP)
u := b[n : n+udpHeaderSize]
binary.BigEndian.PutUint16(u, uint16(src.Port))
binary.BigEndian.PutUint16(u[2:], uint16(dst.Port))
binary.BigEndian.PutUint16(u, src.Port())
binary.BigEndian.PutUint16(u[2:], dst.Port())
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(payload)))
setIPLength(b[:n], n+udpHeaderSize+len(payload))
check := transportChecksum(b[:n], protoUDP, u, payload)
if check == 0 {
// zero stands for no checksum
check = 0xffff
}
binary.BigEndian.PutUint16(u[6:], check)
return s.tun.WritePacket(b[:n+udpHeaderSize], payload)
return s.write(b[:n+udpHeaderSize], payload)
}
// handleICMP answers an echo request as its destination would
// write completes the IP header of a packet that is in two parts and writes it to the tun, in fragments if it is
// larger than the MTU. A segment of a connection never is, and is not written by this
func (s *stack) write(header, payload []byte) error {
size, last := ipv6HeaderSize, 39
if header[0]>>4 == 4 {
size, last = ipv4HeaderSize, 19
}
// the group is chosen by the last bytes of the destination
id := s.identification[(header[last]^header[last-1]<<3)%byte(len(s.identification))].Add(1)
if header[0]>>4 == 4 {
// a link on the way may split it as well, and what is put together there is told apart by its number
binary.BigEndian.PutUint16(header[4:], uint16(id))
}
if len(header)+len(payload) > s.mtu {
return writeFragments(s.tun, s.mtu, id, header, payload)
}
setIPLength(header[:size], len(header)+len(payload))
return s.tun.WritePacket(header, payload)
}
// DialUDP implements Stack
func (s *stack) DialUDP() (net.PacketConn, error) {
u := &udpSocket{stack: s, queue: newPacketQueue(udpQueueLimit)}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil, io.ErrClosedPipe
}
port, ok := ephemeral(func(port uint16) bool {
for _, a := range s.addresses {
if s.sockets[netip.AddrPortFrom(a, port)] != nil {
return false
}
}
return true
})
if !ok || len(s.addresses) == 0 {
return nil, errors.New("no port left on ", s.addresses)
}
u.port = port
for _, a := range s.addresses {
s.sockets[netip.AddrPortFrom(a, port)] = u
}
return u, nil
}
// udpSocket is a UDP port that was opened from here, on every address of the stack. It is not bound to one
// other side: what anyone sends to it is read from it, with who sent it
type udpSocket struct {
stack *stack
port uint16
queue *packetQueue
}
func (u *udpSocket) ReadFrom(p []byte) (int, net.Addr, error) {
b, ok := u.queue.pop()
if !ok {
return 0, nil, gonet.ErrClosed
}
defer b.Release()
return copy(p, b.Bytes()), b.UDP.RawNetAddr(), nil
}
func (u *udpSocket) WriteTo(p []byte, addr net.Addr) (int, error) {
to, ok := addr.(*net.UDPAddr)
if !ok {
return 0, errors.New("not a udp address: ", addr)
}
local, err := u.stack.local(to.AddrPort().Addr().Unmap())
if err != nil {
return 0, err
}
if err := u.stack.sendUDP(p, netip.AddrPortFrom(local, u.port), to.AddrPort()); err != nil {
return 0, err
}
return len(p), nil
}
func (u *udpSocket) Close() error {
u.stack.mu.Lock()
for _, a := range u.stack.addresses {
if local := netip.AddrPortFrom(a, u.port); u.stack.sockets[local] == u {
delete(u.stack.sockets, local)
}
}
u.stack.mu.Unlock()
u.queue.close()
return nil
}
// LocalAddr implements net.PacketConn with the first address of the stack
func (u *udpSocket) LocalAddr() net.Addr {
return gonet.UDPAddrFromAddrPort(netip.AddrPortFrom(u.stack.addresses[0], u.port))
}
func (u *udpSocket) SetDeadline(time.Time) error {
return nil
}
func (u *udpSocket) SetReadDeadline(time.Time) error {
return nil
}
func (u *udpSocket) SetWriteDeadline(time.Time) error {
return nil
}
// handleICMP answers an echo request as its destination would, and takes the word that a packet of a connection
// was too large for a link on its way or found no way at all
func (s *stack) handleICMP(p packet) {
m := p.payload
request, reply := byte(8), byte(0)
if p.proto == protoICMPv6 {
request, reply = 128, 129
}
if len(m) < 8 || m[0] != request || m[1] != 0 {
if len(m) < 8 {
return
}
switch {
case p.proto == protoICMPv4 && m[0] == 3 && m[1] == 4:
s.undelivered(m[8:], max(int(binary.BigEndian.Uint16(m[6:])), ipv4SmallestMTU))
return
case p.proto == protoICMPv6 && m[0] == 2:
s.undelivered(m[8:], max(int(binary.BigEndian.Uint32(m[4:])), ipv6SmallestMTU))
return
case p.proto == protoICMPv4 && m[0] == 3, p.proto == protoICMPv6 && m[0] == 1:
s.undelivered(m[8:], 0)
return
}
if m[0] != request || m[1] != 0 || s.handle == nil && !slices.Contains(s.addresses, p.dst) {
// a stack that takes no connections is one host, not all of them
return
}
var b [ipv6HeaderSize]byte
n := putIPHeader(b[:], p.proto, p.dst, p.src)
setIPLength(b[:n], n+len(m))
// the reply is the request with another type
m[0], m[2], m[3] = reply, 0, 0
if p.proto == protoICMPv6 {
@@ -276,5 +586,39 @@ func (s *stack) handleICMP(p packet) {
} else {
binary.BigEndian.PutUint16(m[2:], checksum(sum(m, 0)))
}
_ = s.tun.WritePacket(b[:n], m)
_ = s.write(b[:n], m)
}
// undelivered takes the start of a packet that was sent from here and did not get to where it goes. If it did not fit
// a link on its way, which takes packets of mtu bytes, the connection that sent it goes on with smaller segments.
// Without an mtu there is no way at all, and a connection that is being opened is not waited for
func (s *stack) undelivered(b []byte, mtu int) {
var proto byte
var local, remote netip.Addr
switch {
case len(b) >= ipv4HeaderSize && b[0]>>4 == 4:
proto, local, remote = b[9], netip.AddrFrom4([4]byte(b[12:16])), netip.AddrFrom4([4]byte(b[16:20]))
b = b[min(int(b[0]&0xf)*4, len(b)):]
case len(b) >= ipv6HeaderSize && b[0]>>4 == 6:
proto, local, remote = b[6], netip.AddrFrom16([16]byte(b[8:24])), netip.AddrFrom16([16]byte(b[24:40]))
b = b[ipv6HeaderSize:]
}
// of the segment there are the ports and the sequence number at least
if proto != protoTCP || len(b) < 8 {
return
}
s.mu.Lock()
c := s.conns[flow{
src: netip.AddrPortFrom(remote, binary.BigEndian.Uint16(b[2:])),
dst: netip.AddrPortFrom(local, binary.BigEndian.Uint16(b)),
}]
s.mu.Unlock()
if c == nil {
return
}
if seq := binary.BigEndian.Uint32(b[4:]); mtu != 0 {
c.shrink(seq, mtu)
} else {
c.unreachable(seq)
}
}
+1571 -159
View File
File diff suppressed because it is too large Load Diff
+308 -46
View File
@@ -1,10 +1,12 @@
package tun
import (
"context"
"encoding/binary"
"io"
"math"
"math/rand/v2"
"net/netip"
"slices"
"sync"
"time"
@@ -37,7 +39,9 @@ const (
tcpHeld = 1024
// tcpSmallest is the smallest mss that the system is believed, the one of Linux
tcpSmallest = 48
// tcpOpening is how many connections can be waiting for the answer to their SYN-ACK
// tcpStamps is what the times of both sides take of the options of every segment that carries them (RFC 7323)
tcpStamps = 12
// tcpOpening is how many connections that the other side opens can be waiting for the answer to their SYN-ACK
tcpOpening = 65535
// tcpStart is how many segments are sent before any of them is acknowledged
tcpStart = 10
@@ -45,15 +49,24 @@ const (
// tcpParts of how many parts of what was written it is made at most
tcpSegment = math.MaxUint16 - ipv6HeaderSize - tcpHeaderSize
tcpParts = 7
// tcpKept is how large a payload is at least for the buffers that it was read into to be kept. A smaller one
// is copied to where the one before it ends, for a reader to get buffers that are full
tcpKept = buf.Size / 2
// tcpTick is how often a connection that waits for something looks at what it waits for
tcpTick = 100 * time.Millisecond
// ticks without anything acknowledged before it is sent again, twice as many every next time, a minute at most
// ticks without anything acknowledged before it is sent again, twice as many every next time, a minute at most,
// while an open connection that waits sends again every two seconds at most or as often as an answer takes,
// twice as seldom every next minute: when its way was away for a while it goes on soon after the way is back,
// and the next segment still waits the doubled time, so that its answer can be timed
tcpRetransmitTicks = 3
tcpRetransmitTicksOpen = 20
tcpRetransmitTicksMost = 600
// how many times it is sent again before the connection is given up: that is after four minutes, as what is
// behind a wireless link may be away for a while, and after twenty seconds for one that never answered
tcpRetransmits = 10
tcpMinuteTicks = int(time.Minute / tcpTick)
// an open connection is given up after four minutes of ticks without an acknowledgment since the timer first sent
// it again, as what is behind a wireless link may be away for a while (ticks, so that the time a computer sleeps
// is not counted), and one that never answered after it was sent again that many times, after twenty seconds
tcpGiveUpTicks = 4 * tcpMinuteTicks
tcpSynRetransmits = 5
// ticks between the probes of a window that stays closed
tcpProbeTicks = 10
@@ -62,17 +75,24 @@ const (
)
var (
errTCPReset = errors.New("connection reset")
errTCPTimeout = errors.New("connection timed out")
errTCPReset = errors.New("connection reset")
errTCPTimeout = errors.New("connection timed out")
errTCPRefused = errors.New("connection refused")
errTCPUnreachable = errors.New("destination unreachable")
)
// tcpStarted is where the times that segments carry count from
var tcpStarted = time.Now()
// segment is a TCP segment that came from the tun
type segment struct {
seq, ack uint32
flags byte
window uint16
options []byte
payload []byte
// payload is without its last rest bytes when those are in other buffers of the stack, see stack.whole
payload []byte
rest int
}
// nextOption returns the first option of o and what follows it, kind is zero when there is none
@@ -92,7 +112,7 @@ type held struct {
data *buf.Buffer
}
// tcpConn is a TCP connection that the system opened through the tun.
// tcpConn is a TCP connection that the system opened through the tun, or one that was opened to it by DialTCP.
// Mostly the two are on the same machine, with nothing but the queue of the tun in between, which drops what
// does not fit. But the system may also pass on what others send, as a router does, and then the link to them
// is in between. So a connection has windows that are small enough, keeps what came after a lost segment,
@@ -114,12 +134,19 @@ type tcpConn struct {
sndShift, rcvShift uint8
// sack is set if the system takes selective acknowledgments
sack bool
// stamps is set if every segment carries the time of this side and the last one that came of the other side,
// which sees by it how long its segments take, also the ones that it sent again (RFC 7323).
// stampBase is what the time of this side starts from, stampSent the one that the last segment carried
stamps bool
stampBase, stampSent uint32
mu sync.Mutex
readable sync.Cond
writable sync.Cond
established bool
// opened is there while DialTCP waits for the answer to its SYN, and closed when that wait is over
opened chan struct{}
// closed is set when Close is called
closed bool
// err is why the connection is gone
@@ -140,6 +167,8 @@ type tcpConn struct {
rcvHeld []held
// rcvLast is the seq of the last one that was put into rcvHeld
rcvLast uint32
// stamp is the time of the other side that is sent back to it
stamp uint32
sndUna, sndNxt uint32
sndWnd uint32
@@ -165,6 +194,9 @@ type tcpConn struct {
recover uint32
// lost is the end of what is lost past the last of sacked, resent the end of what was sent again since then
lost, resent uint32
// headAt is when the segment at sndUna was sent again, or took that place while something was lost.
// It is zero when that segment is not sent once more for the system still asking for it: it was already
headAt time.Time
// timed is the end of a segment that was sent at timedAt and not again, to see how long its acknowledgment takes.
// The first one is the SYN-ACK
timed uint32
@@ -176,17 +208,17 @@ type tcpConn struct {
armed bool
// stalled counts ticks without an acknowledgment of what waits for one
stalled, retransmitTicks, retransmits int
lingered bool
// lostTicks counts the ticks since the timer sent again the first time since something was acknowledged
lostTicks int
lingered bool
}
func newTCPConn(s *stack, f flow, syn segment) *tcpConn {
func newTCPConn(s *stack, f flow) *tcpConn {
c := &tcpConn{
stack: s,
flow: f,
src: net.TCPDestination(net.IPAddress(f.src.Addr().AsSlice()), net.Port(f.src.Port())),
dst: net.TCPDestination(net.IPAddress(f.dst.Addr().AsSlice()), net.Port(f.dst.Port())),
rcvNxt: syn.seq + 1,
sndWnd: uint32(syn.window),
window: s.window,
ssthresh: uint32(s.window),
timedAt: time.Now(),
@@ -196,6 +228,17 @@ func newTCPConn(s *stack, f flow, syn segment) *tcpConn {
c.writable.L = &c.mu
c.ipSize = putIPHeader(c.ip[:], protoTCP, f.dst.Addr(), f.src.Addr())
c.mss = s.mtu - c.ipSize - tcpHeaderSize
c.sndUna = rand.Uint32()
c.sndNxt = c.sndUna + 1
c.stampBase = rand.Uint32()
return c
}
// synchronize takes what the SYN of the other side says. c.mu is held, or nobody else has c yet
func (c *tcpConn) synchronize(syn segment) {
c.rcvNxt = syn.seq + 1
c.sndWnd = uint32(syn.window)
c.sack, c.stamps, c.sndShift, c.rcvShift = false, false, 0, 0
for kind, value, o := nextOption(syn.options); kind != 0; kind, value, o = nextOption(o) {
switch {
case kind == 2 && len(value) == 2:
@@ -205,20 +248,77 @@ func newTCPConn(s *stack, f flow, syn segment) *tcpConn {
c.rcvShift = tcpWindowShift
case kind == 4:
c.sack = true
case kind == 8 && len(value) == 8 && c.stack.stamps:
c.stamps, c.stamp = true, binary.BigEndian.Uint32(value)
}
}
if c.stamps {
c.mss -= tcpStamps
}
c.mss = max(c.mss, tcpSmallest)
c.cwnd = tcpStart * uint32(c.mss)
c.sndUna = rand.Uint32()
c.sndNxt = c.sndUna + 1
return c
}
// DialTCP implements Stack
func (s *stack) DialTCP(ctx context.Context, remote netip.AddrPort) (net.Conn, error) {
remote = netip.AddrPortFrom(remote.Addr().Unmap(), remote.Port())
local, err := s.local(remote.Addr())
if err != nil {
return nil, err
}
opened := make(chan struct{})
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil, io.ErrClosedPipe
}
f := flow{src: remote}
port, ok := ephemeral(func(port uint16) bool {
f.dst = netip.AddrPortFrom(local, port)
return s.conns[f] == nil
})
if !ok {
s.mu.Unlock()
return nil, errors.New("no port left to reach ", remote, " from")
}
f.dst = netip.AddrPortFrom(local, port)
c := newTCPConn(s, f)
// what the SYN offers, until the answer says what of it is taken
c.sack, c.stamps, c.rcvShift, c.opened = true, s.stamps, tcpWindowShift, opened
s.conns[f] = c
s.mu.Unlock()
s.opening.Add(1)
c.mu.Lock()
if c.err == nil {
c.sendSyn()
c.arm(tcpTick)
}
c.mu.Unlock()
select {
case <-opened:
case <-ctx.Done():
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.established {
if c.err == nil {
c.abort(ctx.Err())
}
return nil, c.err
}
return c, nil
}
// header writes the headers of a segment with size bytes of payload into b and returns their size.
// The checksum of the segment is left to write. c.mu is held
func (c *tcpConn) header(b []byte, seq uint32, flags byte, options []byte, size int) int {
n := copy(b, c.ip[:c.ipSize])
t := b[n : n+tcpHeaderSize+len(options)]
stamps := 0
if c.stamps {
stamps = tcpStamps
}
t := b[n : n+tcpHeaderSize+stamps+len(options)]
binary.BigEndian.PutUint16(t, c.flow.dst.Port())
binary.BigEndian.PutUint16(t[2:], c.flow.src.Port())
binary.BigEndian.PutUint32(t[4:], seq)
@@ -233,7 +333,18 @@ func (c *tcpConn) header(b []byte, seq uint32, flags byte, options []byte, size
binary.BigEndian.PutUint16(t[14:], uint16(min(window>>c.rcvShift, math.MaxUint16)))
}
t[16], t[17], t[18], t[19] = 0, 0, 0, 0
copy(t[tcpHeaderSize:], options)
if c.stamps {
o := t[tcpHeaderSize:]
o[0], o[1], o[2], o[3] = 1, 1, 8, 10
if !c.sending {
// what is sent while WriteMultiBuffer writes a segment has the time of that one: it may get to the tun
// first, and the other side drops a segment with a time before the last one that it took
c.stampSent = c.stampBase + uint32(time.Since(tcpStarted)/time.Millisecond)
}
binary.BigEndian.PutUint32(o[4:], c.stampSent)
binary.BigEndian.PutUint32(o[8:], c.stamp)
}
copy(t[tcpHeaderSize+stamps:], options)
setIPLength(b[:n], n+len(t)+size)
c.rcvAcked = c.rcvNxt
c.rcvAdvertised = window
@@ -254,7 +365,8 @@ func (c *tcpConn) send(seq uint32, flags byte, options, payload []byte) {
c.write(b[:c.header(b[:], seq, flags, options, len(payload))], payload)
}
func (c *tcpConn) sendSynAck() {
// sendSyn sends the answer to the SYN of the other side, or the SYN of a connection that is opened from here
func (c *tcpConn) sendSyn() {
options := [12]byte{2, 4}
binary.BigEndian.PutUint16(options[2:], uint16(c.stack.mtu-c.ipSize-tcpHeaderSize))
n := 4
@@ -264,7 +376,11 @@ func (c *tcpConn) sendSynAck() {
if c.rcvShift != 0 {
n += copy(options[n:], []byte{1, 3, 3, c.rcvShift})
}
c.send(c.sndUna, tcpSyn|tcpAck, options[:n], nil)
flags := byte(tcpSyn | tcpAck)
if c.opened != nil {
flags = tcpSyn
}
c.send(c.sndUna, flags, options[:n], nil)
}
// sendAck acknowledges what was received, and tells what of rcvHeld waits for a lost segment
@@ -278,8 +394,12 @@ func (c *tcpConn) sendAck() {
c.send(c.sndNxt, tcpAck, nil, nil)
return
}
// the block with the segment that came last goes first
var blocks [4][2]uint32
// the block with the segment that came last goes first. Next to the times there is room for one less
var room [4][2]uint32
blocks := room[:]
if c.stamps {
blocks = room[:3]
}
n, last := 1, false
for i := 0; i < len(c.rcvHeld); {
start, end, recent := c.rcvHeld[i].seq, c.rcvHeld[i].seq, false
@@ -306,6 +426,22 @@ func (c *tcpConn) sendAck() {
c.send(c.sndNxt, tcpAck, options[:4+8*len(list)], nil)
}
// stamped keeps the time that a segment of the other side carries, to send it back. It is the one of the segment
// that the stream goes on with, so that the other side sees how long that one took, and not one of what came
// before it. c.mu is held
func (c *tcpConn) stamped(s segment) {
if int32(s.seq-c.rcvAcked) > 0 {
return
}
for kind, value, o := nextOption(s.options); kind != 0; kind, value, o = nextOption(o) {
if kind == 8 && len(value) == 8 {
if stamp := binary.BigEndian.Uint32(value); int32(stamp-c.stamp) > 0 {
c.stamp = stamp
}
}
}
}
// input takes a segment of the connection. It returns false if the connection is gone
// and the segment is the start of another one
func (c *tcpConn) input(s segment) bool {
@@ -314,6 +450,32 @@ func (c *tcpConn) input(s segment) bool {
if c.err != nil {
return s.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn
}
if c.opened != nil {
// only the answer to the SYN is waited for, or the word that nobody takes it
switch {
case s.flags&tcpAck == 0:
case s.ack != c.sndNxt:
// of a connection that the other side still has from before: it is told that there is none
if s.flags&tcpRst == 0 {
c.stack.refuse(c.flow, s)
}
case s.flags&tcpRst != 0:
c.abort(errTCPRefused)
case s.flags&tcpSyn != 0:
c.synchronize(s)
c.established = true
c.sndUna, c.sndBase = s.ack, s.ack
c.stalled, c.retransmits = 0, 0
if !c.timedAt.IsZero() {
c.clocked()
}
c.stack.opening.Add(-1)
close(c.opened)
c.opened = nil
c.sendAck()
}
return true
}
if s.flags&tcpRst != 0 {
// from where the system is, or was before its FIN, not from someone who knows no more than the addresses
if s.seq-c.rcvNxt <= uint32(c.window) || c.rcvFin && s.seq+1 == c.rcvNxt {
@@ -324,9 +486,12 @@ func (c *tcpConn) input(s segment) bool {
if s.flags&tcpSyn != 0 {
switch {
case !c.established && s.seq+1 == c.rcvNxt:
c.sendSynAck()
c.sendSyn()
c.timedAt = time.Time{}
case c.closed && s.flags&tcpAck == 0:
case c.closed && s.flags&tcpAck == 0 && !c.stack.verify:
// the system of this machine opens the same ports again: it has let the old connection go.
// What comes through a tunnel is answered with what is expected instead, as anyone who knows
// the addresses could send it, and the other side answers that with a reset if it is gone
c.abort(errTCPReset)
return false
default:
@@ -337,6 +502,9 @@ func (c *tcpConn) input(s segment) bool {
if s.flags&tcpAck == 0 {
return true
}
if c.stamps {
c.stamped(s)
}
if !c.established {
if s.ack != c.sndNxt {
return true
@@ -385,6 +553,7 @@ func (c *tcpConn) acknowledge(s segment) {
if !c.timedAt.IsZero() && int32(c.sndUna-c.timed) >= 0 {
c.clocked()
}
recovering := c.recovering
switch {
case c.recovering && int32(c.sndUna-c.recover) >= 0:
c.recovering = false
@@ -393,11 +562,18 @@ func (c *tcpConn) acknowledge(s segment) {
if end := c.sndUna + min(uint32(c.mss), c.recover-c.sndUna); int32(end-c.lost) > 0 {
c.lost = end
}
c.headAt = time.Now()
}
// the way takes more if what it was given has arrived, as long as it was given about as much as it takes
if sent >= c.cwnd/2 {
if c.cwnd < c.ssthresh {
c.cwnd = min(c.cwnd+acked, c.ssthresh)
grow := acked
if recovering {
// after a timeout: one segment that is sent again can acknowledge all that came after it,
// which says nothing about how much the way takes at once
grow = min(grow, 2*uint32(c.mss))
}
c.cwnd = min(c.cwnd+grow, c.ssthresh)
} else if !c.recovering {
// a segment more for every cwnd that arrives
c.cwnd = min(c.cwnd+max(uint32(uint64(c.mss)*uint64(acked)/uint64(c.cwnd)), 1), uint32(c.window))
@@ -417,12 +593,22 @@ func (c *tcpConn) acknowledge(s segment) {
}
}
}
if !c.recovering && c.sndUna != c.sndNxt && (len(c.sacked) > 0 || c.dupAcks >= 3) {
// the system has something past sndUna, or says the same for the third time: the segment at sndUna is lost
if !c.recovering && c.sndUna != c.sndNxt && (len(c.sacked) > 0 || !c.sack && c.dupAcks >= 3) {
// the system has something past sndUna, or says the same for the third time when it cannot say what it has:
// the segment at sndUna is lost. One that can say it repeats itself also for what it got twice
c.congested()
c.lost = c.sndUna + min(uint32(c.mss), c.sndNxt-c.sndUna)
}
if c.recovering {
if acked == 0 && c.srtt != 0 && !c.headAt.IsZero() && time.Since(c.headAt) > max(c.srtt+max(c.srtt/4, 4*c.rttvar), tcpTick/4) {
// the segment that the system waits for was sent again longer ago than an answer takes, and the system
// still answers what comes after it: that one was lost as well. It is sent once more, and only once:
// the timer would find it too, but late, and then nothing is known anymore of how much the way takes
if start, end := c.hole(c.sndUna); start == c.sndUna && start != end {
c.retransmit(start, start+min(end-start, uint32(c.mss)))
c.headAt, c.stalled = time.Time{}, 0
}
}
c.resend()
}
c.sndWnd = window
@@ -451,7 +637,12 @@ func (c *tcpConn) clocked() {
c.rttvar += ((c.srtt - d).Abs() - c.rttvar) / 4
c.srtt += (d - c.srtt) / 8
}
c.retransmitTicks = max(tcpRetransmitTicks, int((c.srtt+4*c.rttvar)/tcpTick)+2)
c.retransmitTicks = c.answerTicks()
}
// answerTicks is how many ticks an acknowledgment is waited for as long as nothing was sent again. c.mu is held
func (c *tcpConn) answerTicks() int {
return max(tcpRetransmitTicks, int((c.srtt+4*c.rttvar)/tcpTick)+2)
}
// sackAdd puts what the system says it has into sacked. c.mu is held
@@ -481,7 +672,7 @@ func (c *tcpConn) congested() {
c.ssthresh = max(min(c.cwnd, c.sndNxt-c.sndUna)/2, 2*uint32(c.mss))
c.cwnd = c.ssthresh
c.recovering, c.recover, c.resent = true, c.sndNxt, c.sndUna
c.timedAt = time.Time{}
c.timedAt, c.headAt = time.Time{}, time.Time{}
}
// hole returns the first part at or after seq of what is lost: what the system does not have
@@ -543,6 +734,10 @@ func (c *tcpConn) resend() {
room = c.cwnd - flight
}
end = start + min(end-start, room)
if start == c.sndUna {
// its answer is waited for from now on
c.headAt, c.stalled = time.Now(), 0
}
c.retransmit(start, end)
c.resent = end
}
@@ -559,6 +754,12 @@ func (c *tcpConn) space() uint32 {
// receive takes the payload of a segment. c.mu is held
func (c *tcpConn) receive(s segment) {
// what is large, the next and all taken stays in the buffers that it was read into
size := len(s.payload) + s.rest
keep := c.stack.bufs != nil && size >= tcpKept && s.seq == c.rcvNxt && size <= int(c.window-c.rcvLen)
if !keep {
s.payload = c.stack.whole(s.payload, s.rest)
}
seq, payload, fin := s.seq, s.payload, s.flags&tcpFin != 0
if len(payload) == 0 && !fin {
if int32(c.rcvNxt-seq) > 0 {
@@ -599,9 +800,14 @@ func (c *tcpConn) receive(s segment) {
return
}
c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload)
c.rcvLen += int32(len(payload))
c.rcvNxt += uint32(len(payload))
if keep {
c.rcvQueue = c.stack.keep(c.rcvQueue, payload, s.rest)
} else {
size = len(payload)
c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload)
}
c.rcvLen += int32(size)
c.rcvNxt += uint32(size)
for !fin && len(c.rcvHeld) > 0 && int32(c.rcvNxt-c.rcvHeld[0].seq) >= 0 {
h := c.rcvHeld[0]
if before := int32(c.rcvNxt - h.seq); before < h.data.Len() {
@@ -619,7 +825,7 @@ func (c *tcpConn) receive(s segment) {
c.readable.Signal()
// the reader acknowledges what it takes, with one segment for all that came by then.
// It is not waited for when it does not read for long
if fin || now || c.rcvNxt-c.rcvAcked >= tcpWindow/4 {
if fin || now || c.rcvNxt-c.rcvAcked >= uint32(c.window/4) {
c.sendAck()
} else {
c.arm(tcpTick)
@@ -642,9 +848,9 @@ func (c *tcpConn) release() {
// hold keeps a segment that is not the next one. c.mu is held
func (c *tcpConn) hold(seq uint32, payload []byte) {
i := 0
for i < len(c.rcvHeld) && int32(c.rcvHeld[i].seq-seq) < 0 {
i++
i := len(c.rcvHeld)
for i > 0 && int32(c.rcvHeld[i-1].seq-seq) >= 0 {
i--
}
// without what the one before and the one after have
if i > 0 {
@@ -666,8 +872,9 @@ func (c *tcpConn) hold(seq uint32, payload []byte) {
if len(payload) == 0 || len(c.rcvHeld) == tcpHeld {
return
}
b := buf.NewWithSize(int32(len(payload)))
b.Write(payload)
// as much memory as it carries: the room that is left counts bytes, and a thousand tiny segments in pooled
// buffers would take two megabytes
b := buf.FromBytes(slices.Clone(payload))
c.rcvHeld = slices.Insert(c.rcvHeld, i, held{seq, b})
c.rcvLast = seq
}
@@ -716,6 +923,38 @@ func (c *tcpConn) queued(parts [][]byte, seq uint32, size int) ([][]byte, int) {
return parts, n
}
// shrink makes the segments fit a link on the way that takes packets of mtu bytes, when the one at seq did not.
// None of what is on its way fits: it is sent again, the first segment at once and the others as they are
// acknowledged. Only what is said about a segment that was sent and is not acknowledged is believed
func (c *tcpConn) shrink(seq uint32, mtu int) {
c.mu.Lock()
defer c.mu.Unlock()
mss := mtu - c.ipSize - tcpHeaderSize
if c.stamps {
mss -= tcpStamps
}
mss = max(mss, tcpSmallest)
if c.err != nil || !c.established || seq-c.sndUna >= c.sndNxt-c.sndUna || mss >= c.mss {
return
}
c.mss = mss
// as what is lost, without the way taking less for it: it is the size that it did not take
c.recovering, c.recover, c.lost = true, c.sndNxt, c.sndNxt
c.timedAt = time.Time{}
c.resent = c.sndUna + min(uint32(mss), c.sndNxt-c.sndUna)
c.headAt = time.Now()
c.retransmit(c.sndUna, c.resent)
}
// unreachable ends the wait for the answer to the SYN at seq, which a router on the way could not deliver
func (c *tcpConn) unreachable(seq uint32) {
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil && c.opened != nil && seq == c.sndUna {
c.abort(errTCPUnreachable)
}
}
// arm makes tick run after d, unless it is going to run already. c.mu is held
func (c *tcpConn) arm(d time.Duration) {
if c.armed {
@@ -742,19 +981,34 @@ func (c *tcpConn) tick() {
}
switch {
case c.sndUna != c.sndNxt:
if c.stalled++; c.stalled >= c.retransmitTicks {
if c.retransmits++; c.retransmits > tcpRetransmits || !c.established && c.retransmits > tcpSynRetransmits {
c.stalled++
due := c.stalled >= c.retransmitTicks
if c.established && c.retransmits > 0 {
due = due || c.stalled >= max(tcpRetransmitTicksOpen, c.answerTicks())<<(c.lostTicks/tcpMinuteTicks)
}
if due {
if c.retransmits++; c.retransmits == 1 {
c.lostTicks = 0
} else {
c.lostTicks += c.stalled
}
if c.established && c.lostTicks > tcpGiveUpTicks || !c.established && c.retransmits > tcpSynRetransmits {
c.abort(errTCPTimeout)
return
}
c.stalled = 0
c.retransmitTicks = min(c.retransmitTicks*2, tcpRetransmitTicksMost)
if !c.established {
c.sendSynAck()
c.sendSyn()
c.timedAt = time.Time{}
} else {
// what the system does not say it has is lost, and the way to it is tried with a segment again
// what the system does not say it has is lost, and the way to it is tried with a segment again.
// It takes half of what it took before the loss, not half of that again if the loss was seen already
ssthresh, recovering := c.ssthresh, c.recovering
c.congested()
if recovering {
c.ssthresh = ssthresh
}
c.cwnd, c.lost = uint32(c.mss), c.sndNxt
c.resend()
}
@@ -792,6 +1046,10 @@ func (c *tcpConn) abort(err error) {
if c.timer != nil {
c.timer.Stop()
}
if c.opened != nil {
close(c.opened)
c.opened = nil
}
for _, h := range c.rcvHeld {
h.data.Release()
}
@@ -879,9 +1137,9 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
c.writing = len(mb)
// a segment is one part of a Buffer, or as much as there is to send if the tun splits it by itself
var room [tcpParts][]byte
limit, parts := c.mss, room[:0:1]
parts := room[:0:1]
if c.stack.segmenter != nil {
limit, parts = tcpSegment, room[:0]
parts = room[:0]
}
for end := c.sndNxt + size; c.sndNxt != end; {
// not a part of a segment when all of it can go later, with what is acknowledged by then
@@ -897,7 +1155,11 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
if c.err != nil {
break
}
most := min(limit, int(c.space()))
// the mss may have become smaller while the last segment was written
most := min(c.mss, int(c.space()))
if c.stack.segmenter != nil {
most = min(tcpSegment, int(c.space()))
}
if most > c.mss {
// the same for what the tun splits
most -= most % c.mss
@@ -907,7 +1169,7 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
if c.sndNxt+uint32(n) == end {
flags |= tcpPsh
}
var b [ipv6HeaderSize + tcpHeaderSize]byte
var b [ipv6HeaderSize + tcpHeaderSize + tcpStamps]byte
h := b[:c.header(b[:], c.sndNxt, flags, nil, n)]
if c.sndUna == c.sndNxt {
// the ticks without an acknowledgment are counted from the first segment that waits for one
+5 -2
View File
@@ -16,10 +16,13 @@ type Tun interface {
}
// segmenter is a tun whose other side takes TCP segments that are larger than the MTU,
// splits them only if it has to send them on, and completes their checksum then
// splits them only if it has to send them on, and completes their checksum then.
// What it sends comes as it is too, not split to the MTU
type segmenter interface {
// Segments reports whether WriteSegment can be used
// Segments reports whether ReadParts and WriteSegment can be used
Segments() bool
// ReadParts waits for a packet, reads it into parts, one after the other, and returns its size
ReadParts(parts [][]byte) (int, error)
// WriteSegment writes a packet with a TCP segment whose payload is in parts, to be sent on with mss bytes
// of it at most in one. Its checksum is that of the pseudo-header only, not inverted
WriteSegment(header []byte, payload [][]byte, mss int) error
+4 -7
View File
@@ -558,19 +558,16 @@ func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
}
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
var err1, err2 error
switch network {
case "tcp6", "udp6", "ip6":
err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
fallthrough
// IPV6_BOUND_IF binds the IPv4 traffic of a dual-stack socket as
// well, and an IPv6 socket rejects IP_BOUND_IF with EINVAL.
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
case "tcp4", "udp4", "ip4":
err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
default:
panic(network + " " + address)
}
return errors.Join(err1, err2)
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
+40
View File
@@ -3,7 +3,10 @@
package tun
import (
"context"
"net"
"sync"
"syscall"
"testing"
"time"
@@ -231,3 +234,40 @@ func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
}
}
}
// TestSetinterfaceSocketFamilies binds each kind of socket Xray's dialers
// open to lo0, including the dual-stack one Go opens for UDP.
func TestSetinterfaceSocketFamilies(t *testing.T) {
lo, err := net.InterfaceByName("lo0")
if err != nil {
t.Skip(err)
}
lc := net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
var err error
if controlErr := c.Control(func(fd uintptr) {
err = setinterface(network, address, fd, lo)
}); controlErr != nil {
return controlErr
}
return err
}}
for _, s := range []struct{ network, address string }{
{"udp", "0.0.0.0:0"}, // dual-stack
{"udp4", "127.0.0.1:0"},
{"udp6", "[::1]:0"},
{"tcp4", "127.0.0.1:0"},
{"tcp6", "[::1]:0"},
} {
var closer interface{ Close() error }
if s.network[:3] == "udp" {
closer, err = lc.ListenPacket(context.Background(), s.network, s.address)
} else {
closer, err = lc.Listen(context.Background(), s.network, s.address)
}
if err != nil {
t.Errorf("%s %s: %v", s.network, s.address, err)
continue
}
closer.Close()
}
}
+43 -1
View File
@@ -6,6 +6,8 @@ import (
"encoding/binary"
"os"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
@@ -22,6 +24,14 @@ type fdTun struct {
// vnet is the size of the virtio_net_hdr in front of every packet, zero if the tun has none.
// With it the system does not split what it sends to the tun and takes what is not split from it
vnet int
// raw is file for ReadParts, readv what it calls when there may be a packet, iovecs where that reads to:
// header, which takes the virtio_net_hdr, and the parts. read and readErr are what came of it
raw syscall.RawConn
readv func(fd uintptr) bool
header [packetRoom]byte
iovecs []unix.Iovec
read int
readErr error
writeLock sync.RWMutex
closed bool
@@ -29,7 +39,25 @@ type fdTun struct {
// newFdTun takes a file descriptor that does not block
func newFdTun(fd, vnet int) *fdTun {
return &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet}
t := &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet}
t.raw, _ = t.file.SyscallConn()
t.readv = func(fd uintptr) bool {
for {
n, _, errno := unix.Syscall(unix.SYS_READV, fd, uintptr(unsafe.Pointer(&t.iovecs[0])), uintptr(len(t.iovecs)))
switch errno {
case 0:
t.read, t.readErr = int(n), nil
case unix.EINTR:
continue
case unix.EAGAIN:
return false
default:
t.read, t.readErr = 0, errno
}
return true
}
}
return t
}
func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
@@ -38,6 +66,20 @@ func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
return b[min(t.vnet, n):n], err
}
// ReadParts implements segmenter
func (t *fdTun) ReadParts(parts [][]byte) (int, error) {
t.iovecs = append(t.iovecs[:0], unix.Iovec{Base: &t.header[0]})
t.iovecs[0].SetLen(t.vnet)
for i, p := range parts {
t.iovecs = append(t.iovecs, unix.Iovec{Base: &p[0]})
t.iovecs[i+1].SetLen(len(p))
}
if err := t.raw.Read(t.readv); err != nil {
return 0, err
}
return max(t.read-t.vnet, 0), t.readErr
}
func (t *fdTun) WritePacket(header, payload []byte) error {
// a virtio_net_hdr of zeros leaves nothing to the system
var vnet [packetRoom]byte
+66
View File
@@ -0,0 +1,66 @@
//go:build linux
package tun
import (
"bytes"
"crypto/rand"
"testing"
"time"
"golang.org/x/sys/unix"
)
// A tun with a virtio_net_hdr reads one packet at a time into parts, without the header, waits for the next one,
// and stops waiting when it is closed.
func TestReadParts(t *testing.T) {
// what a tun is like: one packet for every read, and a file descriptor that does not block
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
if err != nil {
t.Fatal(err)
}
defer unix.Close(fds[1])
tun := newFdTun(fds[0], vnetSize)
defer tun.closeFile()
large := make([]byte, 20000)
rand.Read(large)
parts := [][]byte{make([]byte, 8192), make([]byte, 8192), make([]byte, 8192)}
send := func(packet []byte) {
if _, err := unix.Write(fds[1], append(bytes.Repeat([]byte{0xee}, vnetSize), packet...)); err != nil {
t.Error(err)
}
}
send(large)
send([]byte("small"))
for _, packet := range [][]byte{large, []byte("small")} {
n, err := tun.ReadParts(parts)
if err != nil || n != len(packet) || !bytes.Equal(bytes.Join(parts, nil)[:n], packet) {
t.Fatal("read ", n, " bytes of ", len(packet), ", error: ", err)
}
}
go func() {
time.Sleep(50 * time.Millisecond)
send([]byte("later"))
}()
if n, err := tun.ReadParts(parts); err != nil || string(parts[0][:n]) != "later" {
t.Fatal("read ", parts[0][:n], " after waiting, error: ", err)
}
read := make(chan error)
go func() {
_, err := tun.ReadParts(parts)
read <- err
}()
time.Sleep(50 * time.Millisecond)
tun.closeFile()
select {
case err := <-read:
if err == nil {
t.Fatal("read after the tun was closed")
}
case <-time.After(5 * time.Second):
t.Fatal("still waits after the tun was closed")
}
}

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