Compare commits

..
77 Commits
Author SHA1 Message Date
Fangliding 7c9282cc9c Format 2026-09-08 18:59:03 +08:00
Fangliding 5ed59b3911 Use go mod package 2026-09-08 18:58:48 +08:00
RPRXandGitHub 37ceb8b4b6 Xray-core v26.9.8
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-09-08 09:47:24 +00:00
RPRXandGitHub fd2ca74822 go.mod: Specify Go 1.27.x
https://github.com/XTLS/Xray-core/pull/6327#issuecomment-5581777988
2026-09-08 08:44:33 +00:00
RPRX 47cfe9994a Update github.com/xtls/reality to 20260908062103
https://github.com/XTLS/REALITY/commit/8cdf7bf9c7f09cb9814bf08c3eb877f68b85fba8
https://github.com/XTLS/REALITY/commit/e1986a4d31ca33c087a72ab4644aef1295693b69
https://github.com/XTLS/REALITY/commit/393f8de3ee2d685271d79ee608334e441b2db324
2026-09-08 07:22:54 +00:00
MeowandGitHub 3e2f040cd8 Direct/Freedom outbound: Better Compatibility (#6058)
Fixes https://github.com/XTLS/Xray-core/issues/6393
2026-09-08 03:31:25 +00:00
LjhAUMEMandGitHub c7245c0336 TUN inbound: Refine Windows support (#6478)
https://github.com/XTLS/Xray-core/pull/6478#issuecomment-5489052152
2026-09-08 02:51:35 +00:00
eef6e63bc1 XHTTP client: Fix a data race in WaitReadCloser (#6694)
https://github.com/XTLS/Xray-core/pull/6694#issuecomment-5575759500

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-09-07 22:38:16 +00:00
Hossin AsaadiandGitHub 6ce8dc53e7 common/buf/writer.go: Fix buffered writes failing when payload exceeds remaining capacity (#6723)
Fixes https://github.com/XTLS/Xray-core/issues/5287
2026-09-07 21:14:41 +00:00
01a034be53 Blackhole outbound: Add customizable response data support (#6713)
https://github.com/XTLS/Xray-core/pull/6711#issuecomment-5508683394

---------

Co-authored-by: Raaad1on <djilyawhite@gmail.com>
2026-09-07 21:09:58 +00:00
XXcipherXandGitHub de2caf3cef Hysteria server: Fix Unix masquerade socket path (#6705)
https://github.com/XTLS/Xray-core/pull/6705#issuecomment-5489788730
2026-09-07 21:01:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cecc88f43c Bump google.golang.org/grpc from 1.83.1 to 1.83.2 (#6704)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.2
  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-09-07 20:42:22 +00:00
pexcnandGitHub cd4ce973e9 WebSocket client: Avoid panic before real dialing in delayDialConn (#6544)
Fixes https://github.com/teddysun/xray-plugin-android/issues/11
2026-09-01 04:57:10 +00:00
patternihaandGitHub fc7b980636 Sockopt: Fix indentation of the CustomSockopt block for Linux (#6568)
https://github.com/XTLS/Xray-core/pull/6568#issuecomment-5488145252

Fixes https://github.com/XTLS/Xray-core/issues/6564
Fixes https://github.com/XTLS/Xray-core/issues/6602
2026-09-01 04:47:55 +00:00
YounesandRPRX 8ee131cbbb HTTP inbound: Fix a potential panic in readResponseAndHandle100Continue() (#6545) 2026-09-01 02:59:56 +03:00
Jorik_SmithandGitHub 2776ea6d74 SS2022 outbound: Close connection to server on early returns (#6698)
https://github.com/XTLS/Xray-core/pull/6698#issuecomment-5468662565
2026-08-31 23:59:04 +00:00
meanwhile131andGitHub 5e245b082e Routing: Export HealthCheckSettings (#6680) 2026-08-30 12:19:54 +00:00
d9c54026c5 Sniffing: Support QUICv2 (#6695)
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-30 12:15:25 +00:00
brookwkoandGitHub c1958dba04 TUN inbound: Support autoSystemRoutingTable and autoOutboundsInterface on FreeBSD as well (#6691)
https://github.com/XTLS/Xray-core/pull/6691#issuecomment-5445818753

Closes https://github.com/XTLS/Xray-core/issues/5594#issuecomment-4789101025
2026-08-27 22:32:45 +00:00
540b9070f5 Transport: Bind the UDP outbound socket in the destination family (#6688)
https://github.com/XTLS/Xray-core/pull/6688#issuecomment-5445559197

Completes https://github.com/XTLS/Xray-core/pull/6624#issuecomment-5430488664

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-27 21:46:26 +00:00
LjhAUMEMandGitHub ada99a4eb0 Hysteria: Upgrade to official v2.12.2 (#6565)
https://github.com/XTLS/Xray-core/pull/6565#issuecomment-5202554275

Fixes https://github.com/XTLS/Xray-core/issues/6408
Fixes https://github.com/XTLS/Xray-core/issues/6598
...
2026-08-27 17:25:56 +00:00
风扇滑翔翼 65458e919f Config: Fix some issues (#6640)
https://github.com/XTLS/Xray-core/pull/6640#issuecomment-5420106315

Fixes https://github.com/XTLS/Xray-core/issues/6636
Fixes https://github.com/XTLS/Xray-core/issues/6600
...
2026-08-27 20:03:59 +08:00
JidosandGitHub aa3d6589da TUN inbound: Wait() blocks via kqueue instead of busy-spinning on Darwin (#6580)
Fixes https://github.com/XTLS/Xray-core/issues/6579
2026-08-26 21:29:58 +00:00
Maksim VarentsovandGitHub 25c11e2d2b Tunnel inbound: SO_REUSEPORT fix and IPv6 support for the OpenBSD transparent proxy (#6624)
Completes https://github.com/XTLS/Xray-core/pull/6546
2026-08-26 21:05:05 +00:00
dffc7ada5e XHTTP client: Define Request.GetBody() for packet-up so h2 can replay after GOAWAY (#6632)
https://github.com/XTLS/Xray-core/pull/6632#issuecomment-5430735467

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-26 20:41:43 +00:00
77f98eba09 XHTTP client: Fix a race condition and a data race (#6665)
https://github.com/XTLS/Xray-core/pull/6665#issuecomment-5429028477

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-26 19:19:41 +00:00
风扇滑翔翼andGitHub f124daf5a3 Observatory: Fix consuming 100% CPU when no outbound matches subjectSelector (#6669)
Fixes https://github.com/XTLS/Xray-core/issues/6666
2026-08-25 18:30:33 +00:00
598bde7412 Router: Refactor to fix API data race (#6678)
Fixes https://github.com/XTLS/Xray-core/pull/6673

---------

Co-authored-by: Kosta <makostadev@xyecoc.com>
2026-08-25 18:00:14 +00:00
9b373e39ca Sniffer: Fix SniffUTP() (#6667)
Fixes https://github.com/XTLS/Xray-core/pull/6664

---------

Co-authored-by: n0ctal <4c866w5fn9@privaterelay.appleid.com>
2026-08-25 16:49:14 +00:00
c7e569b037 WireGuard outbound: Add remoteDNS & honor TTL (#6620)
Closes https://github.com/XTLS/Xray-core/pull/6569#issuecomment-5263789755

Fixes https://github.com/XTLS/Xray-core/issues/6567#issuecomment-5150957597

---------

Co-authored-by: LagPixelLOL <2282688304@qq.com>
2026-08-25 14:28:36 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f02a357861 Bump github.com/stretchr/testify from 1.12.0 to 1.12.1 (#6657)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.12.0 to 1.12.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.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-08-24 02:38:00 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5fe6d6217a Bump google.golang.org/grpc from 1.83.0 to 1.83.1 (#6676)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.0 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.0...v1.83.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.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-08-24 02:29:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0604ffa957 Bump github.com/miekg/dns from 1.1.72 to 1.1.73 (#6675)
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.72 to 1.1.73.
- [Commits](https://github.com/miekg/dns/compare/v1.1.72...v1.1.73)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-version: 1.1.73
  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-08-24 02:29:12 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d3f1a24285 Bump github.com/pion/stun/v3 from 3.1.6 to 3.1.7 (#6674)
Bumps [github.com/pion/stun/v3](https://github.com/pion/stun) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/pion/stun/releases)
- [Commits](https://github.com/pion/stun/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: github.com/pion/stun/v3
  dependency-version: 3.1.7
  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-08-24 02:28:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2323273e37 Bump github.com/stretchr/testify from 1.11.1 to 1.12.0 (#6641)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.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-08-19 05:31:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
09107b71dc Bump golang.org/x/net from 0.57.0 to 0.58.0 (#6633)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.57.0 to 0.58.0.
- [Commits](https://github.com/golang/net/compare/v0.57.0...v0.58.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.58.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-08-19 05:31:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7021606ad3 Bump google.golang.org/protobuf from 1.36.11 to 1.36.12 (#6616)
Bumps google.golang.org/protobuf from 1.36.11 to 1.36.12.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.12
  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-08-19 05:31:30 +00:00
风扇滑翔翼andGitHub 7d214f8b09 WireGuard outbound: Fix sendThrough support (#6570)
Fixes https://github.com/XTLS/Xray-core/issues/6559
2026-08-12 08:38:01 +00:00
yiguodevandGitHub 8b419d833d Routing: Fix process for macOS IPv4-mapped sockets (#6557)
Fixes https://github.com/XTLS/Xray-core/issues/6533
2026-08-12 05:40:07 +00:00
fanymagnetandGitHub a12801c13b Routing: Add localOS that directly matches runtime.GOOS (#6553)
https://github.com/XTLS/Xray-core/pull/6553#issuecomment-5262686006
2026-08-12 05:29:32 +00:00
Maksim VarentsovandGitHub a000371b2a Tunnel inbound: Support TPROXY on OpenBSD as well (#6546)
https://github.com/XTLS/Xray-core/pull/6546#issuecomment-5100711574
2026-08-12 04:51:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bc6e966af8 Bump github.com/cloudflare/circl from 1.6.4 to 1.6.5 (#6609)
Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.4 to 1.6.5.
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.6.4...v1.6.5)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/circl
  dependency-version: 1.6.5
  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-08-10 05:44:15 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fc5620de98 Bump docker/login-action from 4.5.2 to 4.6.0 (#6610)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.2 to 4.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4.5.2...v4.6.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.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-08-10 05:44:12 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b02bdcf4cc Bump google.golang.org/grpc from 1.82.1 to 1.83.0 (#6585)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.1 to 1.83.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.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-08-09 08:27:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2b329b3675 Bump docker/login-action from 4 to 4.5.2 (#6556)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4 to 4.5.2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4...v4.5.2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.2
  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-08-09 08:27:08 +00:00
RPRXandGitHub 5ca6f4b7d4 Xray-core v26.7.28
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-07-28 07:59:48 +00:00
RPRX 18e283909c XHTTP client: Reduce default maxConnections from 6 to 3 for anti-TSPU
https://github.com/XTLS/Xray-core/issues/6376#issuecomment-5101210849

Replaces https://github.com/XTLS/Xray-core/commit/18b85adb4e288f49a7894351c6e0f2428c0beef6
2026-07-28 07:57:55 +00:00
6ab123bf8f XMC finalmask: Add default directional padding/keep-alive packets, matching Minecraft 26.1.2 login/join/etc traffic shapes (#6487)
https://github.com/XTLS/Xray-core/pull/6487#issuecomment-5092235757

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-07-28 06:45:36 +00:00
4aba687dd3 XHTTP & gRPC servers: Get accurate localAddr (#6526)
Fixes https://github.com/XTLS/Xray-core/pull/6476

---------

Co-authored-by: echoowall <echoowall@gmail.com>
2026-07-27 13:11:25 +00:00
yiguodevandRPRX 5b1b41058e Routing: Exclude iOS from Darwin for process (#6524)
Fixes https://github.com/XTLS/Xray-core/pull/6434#issuecomment-5016175948
2026-07-27 12:40:18 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6e3322d219 Bump actions/setup-go from 6 to 7 (#6511)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-17 22:28:51 +00:00
Jasper344612andGitHub 1d8eb81d70 TUN inbound: Add desc config for Windows ("Wintun" by default) (#6486)
https://github.com/XTLS/Xray-docs-next/pull/872#issuecomment-4936055488
2026-07-17 22:21:33 +00:00
e78d8ef184 REALITY server: Refine warnings in parsing configuration (#6508)
https://github.com/XTLS/Xray-core/pull/6508#issuecomment-4995787310
https://github.com/XTLS/Xray-core/pull/6507#issuecomment-4995466980

---------

Co-authored-by: Enle <42325154+EnleStars@users.noreply.github.com>
Co-authored-by: Aleksey <fraybyl@gmail.com>
2026-07-17 21:51:29 +00:00
Jasper344612andGitHub 6ce924ad56 TUN inbound: Change default name to "utunN" (10~1024) (#6485)
https://github.com/XTLS/Xray-docs-next/pull/872#issuecomment-4936853064
2026-07-16 19:53:04 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
035d438979 Bump google.golang.org/grpc from 1.82.0 to 1.82.1 (#6502)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.0 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.0...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.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-07-16 18:04:03 +00:00
RPRXandGitHub 50231eaff9 Xray-core v26.7.11
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-07-11 15:51:51 +00:00
1f74c480d6 Docker: Add pre-release tag (#6470)
https://github.com/XTLS/Xray-core/pull/6470#issuecomment-4947190027

---------

Co-authored-by: KobeArthurScofield <26771058+KobeArthurScofield@users.noreply.github.com>
Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-07-11 15:45:59 +00:00
RPRXandGitHub af7eb68028 REALITY server: Set default "minClientVer": "26.3.27" (change it at your own risk)
https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533
https://t.me/projectXtls/3373
https://t.me/projectXtls/3378
2026-07-11 14:30:54 +00:00
Exclude0122andGitHub 35387572e0 Finalmask: Add XMC (TCP, Minecraft) (#6210)
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4573680059
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4582554889
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4937008835
2026-07-11 13:59:24 +00:00
风扇滑翔翼andGitHub 8f15190c23 QUIC sniffer: Fix potential panic on malformed QUIC packets again (#6471)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-xqmr-94vq-cxvx
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-9h6x-9r9m-5qw2
2026-07-10 21:40:50 +00:00
风扇滑翔翼andGitHub 64fada32b5 TLS client: Pinning CA must have serverName (or higher-priority verifyPeerCertByName or outbound's address) for pinnedPeerCertSha256 (#6472)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-5wf9-h793-w73c
2026-07-10 21:03:35 +00:00
0bafca9486 Stats: Fix GetOrRegister*() races (#6468)
Fixes https://github.com/XTLS/Xray-core/pull/6467#issue-4856851059

---------

Co-authored-by: Joshua Su <i@joshua.su>
2026-07-10 20:34:27 +00:00
yiguodevandGitHub d5bc58dc6b Root config: Add env config (#6400)
https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4880198100
https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4916892425
https://github.com/XTLS/Xray-core/pull/6400#issuecomment-4929136670
2026-07-10 02:56:49 +00:00
patternihaandGitHub c18b39ed80 TLS client: Support more cipherSuites for "unsafe" (golang) fingerprint for anti-NIN (#6450)
https://github.com/XTLS/Xray-core/pull/6450#issuecomment-4929495504
2026-07-10 02:02:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e2ad0acf60 Bump github.com/pires/go-proxyproto from 0.14.0 to 0.15.0 (#6464)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.15.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-07-10 01:56:16 +00:00
c320e89108 HTTPUpgrade server: Keep accepting after transient errors as RAW does; Add handshake read deadline as WS does (#6463)
https://github.com/XTLS/Xray-core/pull/6463#issuecomment-4929183257

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-07-09 20:55:40 +00:00
LjhAUMEMandGitHub 412898fed7 WireGuard outbound: Fix a race condition (#6461)
Fixes https://github.com/XTLS/Xray-core/pull/6321#issuecomment-4881463309
2026-07-09 20:40:02 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5c62d50d43 Bump golang.org/x/net from 0.56.0 to 0.57.0 (#6455)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.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-07-09 14:11:38 +00:00
ivolframandGitHub 1aabe7ea78 XHTTP transport: Do not force trailing path / when both sessionID and seq are placed elsewhere (#6307)
https://github.com/XTLS/Xray-core/discussions/6385#discussioncomment-17454120

https://github.com/XTLS/Xray-core/pull/6307#issuecomment-4917498294

Closes https://github.com/XTLS/Xray-core/issues/6410
2026-07-08 17:59:19 +00:00
e4e7614c62 TLS ECH: Update ECH configuration parsing logic to be more robust (#6441)
https://github.com/XTLS/Xray-core/pull/6441#issuecomment-4903624643

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-07-08 11:12:37 +00:00
yiguodevandGitHub 987290ba48 Routing: process supports macOS as well (#6447)
https://github.com/XTLS/Xray-core/pull/6434#issuecomment-4903384018
2026-07-08 11:03:24 +00:00
d7fa2076c3 Xray-core: Forbid unencrypted outbounds on public Internet for VLESS and Trojan; Remove "none/zero/plain" for VMess and Shadowsocks (#6303)
https://github.com/XTLS/Xray-core/pull/5640#issuecomment-4611416512
https://github.com/XTLS/Xray-core/pull/6303#issuecomment-4668055274

---------

Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-07-07 02:01:53 +00:00
fb548f54d2 infra/conf/transport_internet.go: Split into multiple files; Rename network to method (#6426)
https://github.com/XTLS/Xray-core/issues/6376#issuecomment-4817591444
https://github.com/XTLS/Xray-core/pull/6426#issuecomment-4899048255

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-07-07 01:26:15 +00:00
yiguodevandGitHub 0495b17650 TUN inbound: Refine gateway and autoSystemRoutingTable on macOS (#6434)
https://github.com/XTLS/Xray-core/pull/6434#issuecomment-4885521586
2026-07-07 00:28:50 +00:00
yiguodevandGitHub 65f6f0a43b TUN inbound: Refine gateway and autoSystemRoutingTable on Linux (#6398)
https://github.com/XTLS/Xray-core/pull/6398#issuecomment-4880261591
2026-07-04 02:25:15 +00:00
Jasper344612andGitHub 3263ae9255 TUN inbound: Fix autoOutboundsInterface on Linux (#6413)
Fixes https://github.com/XTLS/Xray-core/issues/6412
2026-07-04 01:38:25 +00:00
LjhAUMEMandGitHub 3dc8bf3d8b Hysteria inbound: Fix dynamic UUID not accepted (#6395)
Fixes https://github.com/XTLS/Xray-core/pull/6375#issuecomment-4825923934
2026-07-04 01:28:21 +00:00
178 changed files with 12060 additions and 4341 deletions
+1
View File
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json 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 RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files # Create log files
+1
View File
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json 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 RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files # Create log files
+20 -1
View File
@@ -64,6 +64,14 @@ jobs:
echo "Latest: '$LATEST'." echo "Latest: '$LATEST'."
echo "LATEST=$LATEST" >>${GITHUB_ENV} echo "LATEST=$LATEST" >>${GITHUB_ENV}
NEWEST=false
if [[ "${{ github.event_name }}" == "release" ]]; then
NEWEST=true
fi
echo "Newest: '$NEWEST'."
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v7 uses: actions/checkout@v7
@@ -74,7 +82,7 @@ jobs:
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v4 uses: docker/login-action@v4.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -124,6 +132,13 @@ jobs:
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }} ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
docker buildx imagetools create \
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
- name: Inspect image - name: Inspect image
run: | run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }} docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
@@ -131,3 +146,7 @@ jobs:
if [[ "${{ env.LATEST }}" == "true" ]]; then if [[ "${{ env.LATEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
fi fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
fi
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+1 -1
View File
@@ -193,7 +193,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+3 -5
View File
@@ -61,15 +61,13 @@ jobs:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false cache: false
- name: Check Format - name: Check Format
run: | run: go run ./infra/vformat/main.go -mode check -pwd ./
go install -v mvdan.cc/gofumpt@latest
go run ./infra/vformat/main.go -mode check -pwd ./
test: test:
needs: check-assets needs: check-assets
@@ -85,7 +83,7 @@ jobs:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+5 -5
View File
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
p := d.policy.ForLevel(user.Level) p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink { if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink" name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
inboundLink.Writer = &SizeStatWriter{ inboundLink.Writer = &SizeStatWriter{
Counter: c, Counter: c,
Writer: inboundLink.Writer, Writer: inboundLink.Writer,
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
} }
if p.Stats.UserDownlink { if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink" name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
outboundLink.Writer = &SizeStatWriter{ outboundLink.Writer = &SizeStatWriter{
Counter: c, Counter: c,
Writer: outboundLink.Writer, Writer: outboundLink.Writer,
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
p := policyManager.ForLevel(user.Level) p := policyManager.ForLevel(user.Level)
if p.Stats.UserUplink { if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink" name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil { if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
link.Reader.(*buf.TimeoutWrapperReader).Counter = c link.Reader.(*buf.TimeoutWrapperReader).Counter = c
} }
} }
if p.Stats.UserDownlink { if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink" name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil { if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
link.Writer = &SizeStatWriter{ link.Writer = &SizeStatWriter{
Counter: c, Counter: c,
Writer: link.Writer, Writer: link.Writer,
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) { func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
name := "user>>>" + email + ">>>online" name := "user>>>" + email + ">>>online"
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil { if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
om.AddIP(ip) om.AddIP(ip)
context.AfterFunc(ctx, func() { om.RemoveIP(ip) }) context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
} }
+6
View File
@@ -78,6 +78,12 @@ func (o *Observer) background() {
sleepTime = time.Duration(o.config.ProbeInterval) sleepTime = time.Duration(o.config.ProbeInterval)
} }
if len(outbounds) == 0 {
errors.LogWarning(o.ctx, "no outbound matches subjectSelector ", o.config.SubjectSelector)
time.Sleep(sleepTime)
continue
}
if !o.config.EnableConcurrency { if !o.config.EnableConcurrency {
sort.Strings(outbounds) sort.Strings(outbounds)
for _, v := range outbounds { for _, v := range outbounds {
+11 -22
View File
@@ -330,7 +330,6 @@ type SenderConfig struct {
// Send traffic through the given IP. Only IP is allowed. // Send traffic through the given IP. Only IP is allowed.
Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"` Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"` MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"` ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"` TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
@@ -382,13 +381,6 @@ func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig {
return nil return nil
} }
func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig {
if x != nil {
return x.ProxySettings
}
return nil
}
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig { func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
if x != nil { if x != nil {
return x.MultiplexSettings return x.MultiplexSettings
@@ -506,14 +498,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" +
"\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" + "\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" +
"\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" + "\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" +
"\x0eOutboundConfig\"\x9d\x03\n" + "\x0eOutboundConfig\"\xd6\x02\n" +
"\fSenderConfig\x12-\n" + "\fSenderConfig\x12-\n" +
"\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" + "\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" +
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12K\n" + "\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12T\n" +
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" + "\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" + "\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" + "\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategyJ\x04\b\x03\x10\x04\"\xa4\x01\n" +
"\x12MultiplexingConfig\x12\x18\n" + "\x12MultiplexingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" + "\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
@@ -548,8 +539,7 @@ var file_app_proxyman_config_proto_goTypes = []any{
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain (*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig (*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage (*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig (internet.DomainStrategy)(0), // 13: xray.transport.internet.DomainStrategy
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
} }
var file_app_proxyman_config_proto_depIdxs = []int32{ var file_app_proxyman_config_proto_depIdxs = []int32{
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule 7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
@@ -562,14 +552,13 @@ var file_app_proxyman_config_proto_depIdxs = []int32{
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage 12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain 10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig 11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig 6, // 10: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig 13, // 11: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy 12, // [12:12] is the sub-list for method output_type
13, // [13:13] is the sub-list for method output_type 12, // [12:12] is the sub-list for method input_type
13, // [13:13] is the sub-list for method input_type 12, // [12:12] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension type_name 12, // [12:12] is the sub-list for extension extendee
13, // [13:13] is the sub-list for extension extendee 0, // [0:12] is the sub-list for field type_name
0, // [0:13] is the sub-list for field type_name
} }
func init() { file_app_proxyman_config_proto_init() } func init() { file_app_proxyman_config_proto_init() }
+1 -1
View File
@@ -57,7 +57,7 @@ message SenderConfig {
// Send traffic through the given IP. Only IP is allowed. // Send traffic through the given IP. Only IP is allowed.
xray.common.net.IPOrDomain via = 1; xray.common.net.IPOrDomain via = 1;
xray.transport.internet.StreamConfig stream_settings = 2; xray.transport.internet.StreamConfig stream_settings = 2;
xray.transport.internet.ProxyConfig proxy_settings = 3; reserved 3;
MultiplexingConfig multiplex_settings = 4; MultiplexingConfig multiplex_settings = 4;
string via_cidr = 5; string via_cidr = 5;
xray.transport.internet.DomainStrategy target_strategy = 6; xray.transport.internet.DomainStrategy target_strategy = 6;
+2 -2
View File
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink { if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink" name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name) c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil { if c != nil {
uplinkCounter = c uplinkCounter = c
} }
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink { if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink" name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name) c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil { if c != nil {
downlinkCounter = c downlinkCounter = c
} }
+13 -60
View File
@@ -15,7 +15,6 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/mux"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
@@ -26,8 +25,6 @@ import (
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
"github.com/xtls/xray-core/transport/pipe"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -39,7 +36,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink { if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink" name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name) c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil { if c != nil {
uplinkCounter = c uplinkCounter = c
} }
@@ -47,7 +44,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink { if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink" name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name) c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil { if c != nil {
downlinkCounter = c downlinkCounter = c
} }
@@ -63,7 +60,6 @@ type Handler struct {
streamSettings *internet.MemoryStreamConfig streamSettings *internet.MemoryStreamConfig
proxyConfig proto.Message proxyConfig proto.Message
proxy proxy.Outbound proxy proxy.Outbound
outboundManager outbound.Manager
mux *mux.ClientManager mux *mux.ClientManager
xudp *mux.ClientManager xudp *mux.ClientManager
udp443 string udp443 string
@@ -77,7 +73,6 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag) uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
h := &Handler{ h := &Handler{
tag: config.Tag, tag: config.Tag,
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
} }
@@ -108,9 +103,11 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
ctx = session.ContextWithFullHandler(ctx, h) ctx = session.ContextWithFullHandler(ctx, h)
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings) if h.streamSettings != nil {
ctx = session.ContextWithStreamSettings(ctx, h.streamSettings)
}
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig) rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -197,7 +194,6 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
common.Interrupt(link.Reader) common.Interrupt(link.Reader)
return return
} }
} else { } else {
unchangedDomain := ob.Target.Address.Domain() unchangedDomain := ob.Target.Address.Domain()
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))]) ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
@@ -270,66 +266,26 @@ func (h *Handler) DestIpAddress() net.IP {
// Dial implements internet.Dialer. // Dial implements internet.Dialer.
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) { func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if h.senderSettings != nil { if h.senderSettings != nil && h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx)
if h.senderSettings.ProxySettings.HasTag() { ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
tag := h.senderSettings.ProxySettings.Tag
handler := h.outboundManager.GetHandler(tag)
if handler != nil {
errors.LogDebug(ctx, "proxying to ", tag, " for dest ", dest)
outbounds := session.OutboundsFromContext(ctx)
ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{
Target: dest,
Tag: tag,
})) // add another outbound in session ctx
opts := pipe.OptionsFromContext(ctx)
uplinkReader, uplinkWriter := pipe.New(opts...)
downlinkReader, downlinkWriter := pipe.New(opts...)
go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
conn := cnc.NewConnection(cnc.ConnectionInputMulti(uplinkWriter), cnc.ConnectionOutputMulti(downlinkReader))
if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil {
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
conn = tls.Client(conn, tlsConfig)
}
return h.getStatCouterConnection(conn), nil
}
errors.LogError(ctx, "failed to get outbound handler with tag: ", tag)
return nil, errors.New("failed to get outbound handler with tag: " + tag)
}
if h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
}
} }
conn, err := internet.Dial(ctx, dest, h.streamSettings) conn, err := internet.Dial(ctx, dest, h.streamSettings)
conn = h.getStatCouterConnection(conn) conn = h.getStatCouterConnection(conn)
outbounds := session.OutboundsFromContext(ctx)
if outbounds != nil {
ob := outbounds[len(outbounds)-1]
ob.Conn = conn
} else {
// for Vision's pre-connect
}
return conn, err return conn, err
} }
func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) { func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil && !h.senderSettings.ProxySettings.HasTag() && (h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) { if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil &&
(h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
var domain string var domain string
addr := h.senderSettings.Via.AsAddress() addr := h.senderSettings.Via.AsAddress()
domain = h.senderSettings.Via.GetDomain() domain = h.senderSettings.Via.GetDomain()
switch { switch {
case h.senderSettings.ViaCidr != "": case h.senderSettings.ViaCidr != "":
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr) ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
case domain == "origin": case domain == "origin":
if inbound := session.InboundFromContext(ctx); inbound != nil { if inbound := session.InboundFromContext(ctx); inbound != nil {
if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() { if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
@@ -344,12 +300,9 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String()) errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
} }
} }
// case addr.Family().IsDomain(): default: // case addr.Family().IsDomain():
default:
ob.Gateway = addr ob.Gateway = addr
} }
} }
} }
+3 -3
View File
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
// GetPrincipleTarget implements routing.BalancerPrincipleTarget // GetPrincipleTarget implements routing.BalancerPrincipleTarget
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) { func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
if b, ok := r.balancers[tag]; ok { if b, ok := (*r.balancers.Load())[tag]; ok {
if s, ok := b.strategy.(BalancingPrincipleTarget); ok { if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
candidates, err := b.SelectOutbounds() candidates, err := b.SelectOutbounds()
if err != nil { if err != nil {
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
// SetOverrideTarget implements routing.BalancerOverrider // SetOverrideTarget implements routing.BalancerOverrider
func (r *Router) SetOverrideTarget(tag, target string) error { func (r *Router) SetOverrideTarget(tag, target string) error {
if b, ok := r.balancers[tag]; ok { if b, ok := (*r.balancers.Load())[tag]; ok {
b.override.Put(target) b.override.Put(target)
return nil return nil
} }
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
// GetOverrideTarget implements routing.BalancerOverrider // GetOverrideTarget implements routing.BalancerOverrider
func (r *Router) GetOverrideTarget(tag string) (string, error) { func (r *Router) GetOverrideTarget(tag string) (string, error) {
if b, ok := r.balancers[tag]; ok { if b, ok := (*r.balancers.Load())[tag]; ok {
return b.override.Get(), nil return b.override.Get(), nil
} }
return "", errors.New("cannot find tag") return "", errors.New("cannot find tag")
-17
View File
@@ -2,25 +2,8 @@ package router
import ( import (
sync "sync" sync "sync"
"github.com/xtls/xray-core/common/errors"
) )
func (r *Router) OverrideBalancer(balancer string, target string) error {
var b *Balancer
for tag, bl := range r.balancers {
if tag == balancer {
b = bl
break
}
}
if b == nil {
return errors.New("balancer '", balancer, "' not found")
}
b.override.Put(target)
return nil
}
type overrideSettings struct { type overrideSettings struct {
target string target string
} }
+20
View File
@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"slices" "slices"
"strings" "strings"
@@ -393,3 +394,22 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
} }
return false return false
} }
// LocalOSMatcher matches the operating system Xray itself is running on. That never
// changes while Xray is running, so the result is resolved when the rule is built.
type LocalOSMatcher struct {
matched bool
}
func NewLocalOSMatcher(names []string) *LocalOSMatcher {
return &LocalOSMatcher{
matched: slices.ContainsFunc(names, func(name string) bool {
return strings.EqualFold(name, runtime.GOOS)
}),
}
}
// Apply implements Condition.
func (m *LocalOSMatcher) Apply(_ routing.Context) bool {
return m.matched
}
+27
View File
@@ -2,7 +2,9 @@ package router_test
import ( import (
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"strings"
"testing" "testing"
. "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router"
@@ -343,6 +345,31 @@ func TestChinaSites(t *testing.T) {
} }
} }
func TestLocalOSRule(t *testing.T) {
otherOS := "plan9"
if runtime.GOOS == otherOS {
otherOS = "linux"
}
cases := []struct {
localOS []string
output bool
}{
{localOS: []string{runtime.GOOS}, output: true},
{localOS: []string{otherOS}, output: false},
{localOS: []string{otherOS, runtime.GOOS}, output: true},
{localOS: []string{strings.ToUpper(runtime.GOOS)}, output: true},
}
for _, test := range cases {
cond, err := (&RoutingRule{LocalOs: test.localOS}).BuildCondition()
common.Must(err)
if got := cond.Apply(withBackground()); got != test.output {
t.Errorf("for localOS %v on %s: expected %v, got %v", test.localOS, runtime.GOOS, test.output, got)
}
}
}
func BenchmarkMphDomainMatcher(b *testing.B) { func BenchmarkMphDomainMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr) rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
+4
View File
@@ -33,6 +33,10 @@ func (r *Rule) Apply(ctx routing.Context) bool {
func (rr *RoutingRule) BuildCondition() (Condition, error) { func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds := NewConditionChan() conds := NewConditionChan()
if len(rr.LocalOs) > 0 {
conds.Add(NewLocalOSMatcher(rr.LocalOs))
}
if len(rr.InboundTag) > 0 { if len(rr.InboundTag) > 0 {
conds.Add(NewInboundTagMatcher(rr.InboundTag)) conds.Add(NewInboundTagMatcher(rr.InboundTag))
} }
+14 -4
View File
@@ -107,8 +107,10 @@ type RoutingRule struct {
VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"` VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"`
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
unknownFields protoimpl.UnknownFields // List of operating systems for matching the one Xray itself is running on.
sizeCache protoimpl.SizeCache LocalOs []string `protobuf:"bytes,23,rep,name=local_os,json=localOs,proto3" json:"local_os,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *RoutingRule) Reset() { func (x *RoutingRule) Reset() {
@@ -278,6 +280,13 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig {
return nil return nil
} }
func (x *RoutingRule) GetLocalOs() []string {
if x != nil {
return x.LocalOs
}
return nil
}
type isRoutingRule_TargetTag interface { type isRoutingRule_TargetTag interface {
isRoutingRule_TargetTag() isRoutingRule_TargetTag()
} }
@@ -637,7 +646,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor
const file_app_router_config_proto_rawDesc = "" + const file_app_router_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" + "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xdc\a\n" +
"\vRoutingRule\x12\x12\n" + "\vRoutingRule\x12\x12\n" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" + "\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" + "\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" +
@@ -661,7 +670,8 @@ const file_app_router_config_proto_rawDesc = "" +
"\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" + "\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" +
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" + "\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" +
"\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" + "\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" +
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" + "\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12\x19\n" +
"\blocal_os\x18\x17 \x03(\tR\alocalOs\x1a=\n" +
"\x0fAttributesEntry\x12\x10\n" + "\x0fAttributesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
+3
View File
@@ -56,6 +56,9 @@ message RoutingRule {
repeated string process = 21; repeated string process = 21;
WebhookConfig webhook = 22; WebhookConfig webhook = 22;
// List of operating systems for matching the one Xray itself is running on.
repeated string local_os = 23;
} }
message WebhookConfig { message WebhookConfig {
+59 -114
View File
@@ -2,7 +2,9 @@ package router
import ( import (
"context" "context"
"maps"
"sync" "sync"
"sync/atomic"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -17,8 +19,8 @@ import (
// Router is an implementation of routing.Router. // Router is an implementation of routing.Router.
type Router struct { type Router struct {
domainStrategy Config_DomainStrategy domainStrategy Config_DomainStrategy
rules []*Rule rules atomic.Pointer[[]*Rule]
balancers map[string]*Balancer balancers atomic.Pointer[map[string]*Balancer]
dns dns.Client dns dns.Client
ctx context.Context ctx context.Context
@@ -43,52 +45,9 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
r.ohm = ohm r.ohm = ohm
r.dispatcher = dispatcher r.dispatcher = dispatcher
r.balancers = make(map[string]*Balancer, len(config.BalancingRule)) r.rules.Store(new([]*Rule))
for _, rule := range config.BalancingRule { r.balancers.Store(&map[string]*Balancer{})
balancer, err := rule.Build(ohm, dispatcher) return r.ReloadRules(config, false)
if err != nil {
return err
}
balancer.InjectContext(ctx)
r.balancers[rule.Tag] = balancer
}
r.rules = make([]*Rule, 0, len(config.Rule))
for _, rule := range config.Rule {
cond, err := rule.BuildCondition()
if err != nil {
r.closeWebhooks()
return err
}
rr := &Rule{
Condition: cond,
Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(),
}
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
r.closeWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
r.closeWebhooks()
return errors.New("balancer ", btag, " not found")
}
rr.Balancer = brule
}
r.rules = append(r.rules, rr)
}
return nil
} }
// PickRoute implements routing.Router. // PickRoute implements routing.Router.
@@ -124,18 +83,22 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
if !shouldAppend { oldRules := *r.rules.Load()
for _, rule := range r.rules { oldBalancers := *r.balancers.Load()
if rule.Webhook != nil {
rule.Webhook.Close() var newRules []*Rule
} newBalancers := make(map[string]*Balancer)
existTags := make(map[string]bool, len(oldRules)+len(config.Rule))
if shouldAppend {
newRules = append(newRules, oldRules...)
maps.Copy(newBalancers, oldBalancers)
for _, rule := range oldRules {
existTags[rule.RuleTag] = true
} }
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
r.rules = make([]*Rule, 0, len(config.Rule))
} }
for _, rule := range config.BalancingRule { for _, rule := range config.BalancingRule {
_, found := r.balancers[rule.Tag] if _, found := newBalancers[rule.Tag]; found {
if found {
return errors.New("duplicate balancer tag") return errors.New("duplicate balancer tag")
} }
balancer, err := rule.Build(r.ohm, r.dispatcher) balancer, err := rule.Build(r.ohm, r.dispatcher)
@@ -143,27 +106,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
return err return err
} }
balancer.InjectContext(r.ctx) balancer.InjectContext(r.ctx)
r.balancers[rule.Tag] = balancer newBalancers[rule.Tag] = balancer
}
startIdx := len(r.rules)
closeNewWebhooks := func() {
for i := startIdx; i < len(r.rules); i++ {
if r.rules[i].Webhook != nil {
r.rules[i].Webhook.Close()
}
}
r.rules = r.rules[:startIdx]
} }
for _, rule := range config.Rule { for _, rule := range config.Rule {
if r.RuleExists(rule.GetRuleTag()) {
closeNewWebhooks()
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
}
cond, err := rule.BuildCondition() cond, err := rule.BuildCondition()
if err != nil { if err != nil {
closeNewWebhooks()
return err return err
} }
rr := &Rule{ rr := &Rule{
@@ -171,69 +119,64 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
Tag: rule.GetTag(), Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(), RuleTag: rule.GetRuleTag(),
} }
if rr.RuleTag != "" && existTags[rr.RuleTag] {
return errors.New("duplicate ruleTag ", rr.RuleTag)
}
existTags[rr.RuleTag] = true
if wh := rule.GetWebhook(); wh != nil { if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh) notifier, err := NewWebhookNotifier(wh)
if err != nil { if err != nil {
closeNewWebhooks()
return err return err
} }
rr.Webhook = notifier rr.Webhook = notifier
} }
btag := rule.GetBalancingTag() if btag := rule.GetBalancingTag(); len(btag) > 0 {
if len(btag) > 0 { brule, found := newBalancers[btag]
brule, found := r.balancers[btag]
if !found { if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
closeNewWebhooks()
return errors.New("balancer ", btag, " not found") return errors.New("balancer ", btag, " not found")
} }
rr.Balancer = brule rr.Balancer = brule
} }
r.rules = append(r.rules, rr) newRules = append(newRules, rr)
} }
r.balancers.Store(&newBalancers)
r.rules.Store(&newRules)
if !shouldAppend {
closeWebhooks(oldRules)
}
return nil return nil
} }
func (r *Router) RuleExists(tag string) bool {
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag == tag {
return true
}
}
}
return false
}
// RemoveRule implements routing.Router. // RemoveRule implements routing.Router.
func (r *Router) RemoveRule(tag string) error { func (r *Router) RemoveRule(tag string) error {
if tag == "" {
return errors.New("empty tag name!")
}
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
newRules := []*Rule{} oldRules := *r.rules.Load()
if tag != "" { newRules := make([]*Rule, 0, len(oldRules))
for _, rule := range r.rules { var removed []*Rule
if rule.RuleTag != tag { for _, rule := range oldRules {
newRules = append(newRules, rule) if rule.RuleTag != tag {
} else if rule.Webhook != nil { newRules = append(newRules, rule)
rule.Webhook.Close() } else {
} removed = append(removed, rule)
} }
r.rules = newRules
return nil
} }
return errors.New("empty tag name!") r.rules.Store(&newRules)
closeWebhooks(removed)
return nil
} }
// ListRule implements routing.Router // ListRule implements routing.Router
func (r *Router) ListRule() []routing.Route { func (r *Router) ListRule() []routing.Route {
r.mu.Lock() rules := *r.rules.Load()
defer r.mu.Unlock() ruleList := make([]routing.Route, 0, len(rules))
ruleList := make([]routing.Route, 0) for _, rule := range rules {
for _, rule := range r.rules {
ruleList = append(ruleList, &Route{ ruleList = append(ruleList, &Route{
outboundTag: rule.Tag, outboundTag: rule.Tag,
ruleTag: rule.RuleTag, ruleTag: rule.RuleTag,
@@ -252,7 +195,9 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
} }
for _, rule := range r.rules { rules := *r.rules.Load()
for _, rule := range rules {
if rule.Apply(ctx) { if rule.Apply(ctx) {
return rule, ctx, nil return rule, ctx, nil
} }
@@ -265,7 +210,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
// Try applying rules again if we have IPs. // Try applying rules again if we have IPs.
for _, rule := range r.rules { for _, rule := range rules {
if rule.Apply(ctx) { if rule.Apply(ctx) {
return rule, ctx, nil return rule, ctx, nil
} }
@@ -279,9 +224,9 @@ func (r *Router) Start() error {
return nil return nil
} }
// closeWebhooks closes all webhook notifiers in the current rule set. // closeWebhooks closes all webhook notifiers in the given rule set.
func (r *Router) closeWebhooks() { func closeWebhooks(rules []*Rule) {
for _, rule := range r.rules { for _, rule := range rules {
if rule.Webhook != nil { if rule.Webhook != nil {
rule.Webhook.Close() rule.Webhook.Close()
} }
@@ -292,7 +237,7 @@ func (r *Router) closeWebhooks() {
func (r *Router) Close() error { func (r *Router) Close() error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
r.closeWebhooks() closeWebhooks(*r.rules.Load())
return nil return nil
} }
+17 -23
View File
@@ -8,6 +8,7 @@ import (
"net" "net"
"net/http" "net/http"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -40,6 +41,7 @@ type WebhookNotifier struct {
deduplication uint32 deduplication uint32
client *http.Client client *http.Client
seen sync.Map seen sync.Map
lastSweep atomic.Int64
done chan struct{} done chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
closeOnce sync.Once closeOnce sync.Once
@@ -77,11 +79,6 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
} }
} }
if h.deduplication > 0 {
h.wg.Add(1)
go h.cleanupLoop()
}
return h, nil return h, nil
} }
@@ -201,6 +198,7 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
} }
ttl := time.Duration(h.deduplication) * time.Second ttl := time.Duration(h.deduplication) * time.Second
now := time.Now() now := time.Now()
h.maybeSweep(now, ttl)
if v, loaded := h.seen.LoadOrStore(email, now); loaded { if v, loaded := h.seen.LoadOrStore(email, now); loaded {
if now.Sub(v.(time.Time)) < ttl { if now.Sub(v.(time.Time)) < ttl {
return true return true
@@ -210,27 +208,23 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
return false return false
} }
func (h *WebhookNotifier) cleanupLoop() { func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
defer h.wg.Done() last := h.lastSweep.Load()
ttl := time.Duration(h.deduplication) * time.Second if now.UnixNano()-last < int64(ttl) {
ticker := time.NewTicker(ttl) return
defer ticker.Stop()
for {
select {
case <-h.done:
return
case <-ticker.C:
now := time.Now()
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
}
} }
if !h.lastSweep.CompareAndSwap(last, now.UnixNano()) {
return // another goroutine did the sweep
}
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
} }
// Only need to call if the Notifier is really used, otherwise GC can clean it
func (h *WebhookNotifier) Close() error { func (h *WebhookNotifier) Close() error {
h.closeOnce.Do(func() { h.closeOnce.Do(func() {
close(h.done) close(h.done)
+48
View File
@@ -48,6 +48,20 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
return c, nil return c, nil
} }
// GetOrRegisterCounter implements stats.Manager.
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.counters[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new counter ", name)
c := new(Counter)
m.counters[name] = c
return c, nil
}
// UnregisterCounter implements stats.Manager. // UnregisterCounter implements stats.Manager.
func (m *Manager) UnregisterCounter(name string) error { func (m *Manager) UnregisterCounter(name string) error {
m.access.Lock() m.access.Lock()
@@ -97,6 +111,20 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
return om, nil return om, nil
} }
// GetOrRegisterOnlineMap implements stats.Manager.
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
m.access.Lock()
defer m.access.Unlock()
if om, found := m.onlineMaps[name]; found {
return om, nil
}
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
om := NewOnlineMap()
m.onlineMaps[name] = om
return om, nil
}
// UnregisterOnlineMap implements stats.Manager. // UnregisterOnlineMap implements stats.Manager.
func (m *Manager) UnregisterOnlineMap(name string) error { func (m *Manager) UnregisterOnlineMap(name string) error {
m.access.Lock() m.access.Lock()
@@ -149,6 +177,26 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
return c, nil return c, nil
} }
// GetOrRegisterChannel implements stats.Manager.
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.channels[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new channel ", name)
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
if m.running {
// Start before publishing so no goroutine can observe an unstarted channel.
if err := c.Start(); err != nil {
return nil, err
}
}
m.channels[name] = c
return c, nil
}
// UnregisterChannel implements stats.Manager. // UnregisterChannel implements stats.Manager.
func (m *Manager) UnregisterChannel(name string) error { func (m *Manager) UnregisterChannel(name string) error {
m.access.Lock() m.access.Lock()
+1 -1
View File
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
} }
_, isFile := reader.(*os.File) _, isFile := reader.(*os.File)
if !isFile && useReadv { if !isFile && useReadV() {
if sc, ok := reader.(syscall.Conn); ok { if sc, ok := reader.(syscall.Conn); ok {
rawConn, err := sc.SyscallConn() rawConn, err := sc.SyscallConn()
if err != nil { if err != nil {
+15 -3
View File
@@ -5,6 +5,7 @@ package buf
import ( import (
"io" "io"
"sync/atomic"
"syscall" "syscall"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
@@ -143,13 +144,24 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
return mb, nil return mb, nil
} }
var useReadv bool var useReadv atomic.Bool
func init() { func useReadV() bool {
return useReadv.Load()
}
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL" const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue }) value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
enabled := false
switch value { switch value {
case defaultFlagValue, "auto", "enable": case defaultFlagValue, "auto", "enable":
useReadv = true enabled = true
} }
useReadv.Store(enabled)
return nil
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
} }
+3 -1
View File
@@ -10,7 +10,9 @@ import (
"github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/features/stats"
) )
const useReadv = false func useReadV() bool {
return false
}
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader { func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
panic("not implemented") panic("not implemented")
+3 -1
View File
@@ -118,7 +118,9 @@ func (w *BufferedWriter) Write(b []byte) (int, error) {
nBytes, err := w.buffer.Write(b) nBytes, err := w.buffer.Write(b)
totalBytes += nBytes totalBytes += nBytes
if err != nil {
// ErrBufferFull means a partial write, so flush below and continue
if err != nil && err != ErrBufferFull {
return totalBytes, err return totalBytes, err
} }
if !w.buffered || w.buffer.IsFull() { if !w.buffered || w.buffer.IsFull() {
+49
View File
@@ -0,0 +1,49 @@
package geodata
import (
"sync"
"github.com/xtls/xray-core/common"
)
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",
"ff00::/8",
}))))
})
func GetPrivateIPMatcher() IPMatcher { return privateIPMatcher() }
var privateDomainMatcher = sync.OnceValue(func() DomainMatcher {
return common.Must2(DomainReg.BuildDomainMatcher(common.Must2(ParseDomainRules([]string{
"lan",
"localdomain",
"example",
"invalid",
"localhost",
"test",
"local",
"home.arpa",
"internal",
"regexp:^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", // Dotless domains
}, Domain_Domain))))
})
func GetPrivateDomainMatcher() DomainMatcher { return privateDomainMatcher() }
+350
View File
@@ -0,0 +1,350 @@
//go:build darwin && !ios
package net
import (
"bytes"
"net"
"net/netip"
"path/filepath"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/errors"
)
const (
darwinProcPIDListFDs = 1
darwinProcPIDFDSocketInfo = 3
darwinProcFDTypeSocket = 2
darwinProcFDInfoSize = 8
darwinSocketFDInfoSize = 792
darwinSocketFDInfoPSIOff = 24
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
darwinInSockInfoSize = 80
darwinInSockInfoIPv4 = 0x1
darwinInSockInfoIPv6 = 0x2
darwinSockInfoIN = 1
darwinSockInfoTCP = 2
)
type darwinSocketMatchLevel int
const (
darwinSocketNoMatch darwinSocketMatchLevel = iota
darwinSocketPortMatch
darwinSocketRemoteMatch
darwinSocketLocalMatch
darwinSocketExactMatch
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
isLocal, err := IsLocal(net.ParseIP(srcIP))
if err != nil {
return 0, "", "", errors.New("failed to determine if address is local: ", err)
}
if !isLocal {
return 0, "", "", ErrNotLocal
}
if network != "tcp" && network != "udp" {
panic("Unsupported network type for process lookup.")
}
srcAddr, err := netip.ParseAddr(srcIP)
if err != nil {
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
}
srcAddr = srcAddr.Unmap()
var dstAddr netip.Addr
hasDstAddr := false
if destIP != "" && destPort != 0 {
dstAddr, err = netip.ParseAddr(destIP)
if err != nil {
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
}
dstAddr = dstAddr.Unmap()
hasDstAddr = true
}
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
if err != nil {
return 0, "", "", errors.New("failed to list processes").Base(err)
}
var bestPID int32
bestLevel := darwinSocketNoMatch
ambiguousBest := false
for _, process := range processes {
pid := process.Proc.P_pid
if pid <= 0 {
continue
}
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
if err != nil || matchLevel == darwinSocketNoMatch {
continue
}
if matchLevel == darwinSocketExactMatch {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
break
}
if matchLevel > bestLevel {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
continue
}
if matchLevel == bestLevel {
ambiguousBest = true
}
}
if bestLevel == darwinSocketNoMatch {
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
if ambiguousBest {
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
absPath, err := darwinProcessPath(bestPID)
if err != nil {
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
}
absPath = filepath.ToSlash(absPath)
return int(bestPID), filepath.Base(absPath), absPath, nil
}
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
fds, err := darwinProcessFDs(pid)
if err != nil {
return darwinSocketNoMatch, err
}
bestLevel := darwinSocketNoMatch
info := make([]byte, darwinSocketFDInfoSize)
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
if fdType != darwinProcFDTypeSocket {
continue
}
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
continue
}
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
if level == darwinSocketExactMatch {
return level, nil
}
if level > bestLevel {
bestLevel = level
}
}
return bestLevel, nil
}
func darwinProcessFDs(pid int32) ([]byte, error) {
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
if err != nil {
return nil, err
}
if n <= 0 {
return nil, nil
}
buf := make([]byte, n)
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
switch network {
case "tcp":
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
return darwinSocketNoMatch
}
case "udp":
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
return darwinSocketNoMatch
}
default:
return darwinSocketNoMatch
}
vflag := info[darwinInSockInfoVFlagOff]
if srcAddr.Is4() {
// Dual-stack sockets expose IPv4-mapped connections as AF_INET6
// while marking the endpoint as IPv4 in ini_vflag.
if (family != unix.AF_INET && family != unix.AF_INET6) || vflag&darwinInSockInfoIPv4 == 0 {
return darwinSocketNoMatch
}
} else {
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
return darwinSocketNoMatch
}
}
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
if !darwinPortMatches(localPort, srcPort) {
return darwinSocketNoMatch
}
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
if !hasDstAddr {
if localAddrMatches {
return darwinSocketExactMatch
}
return darwinSocketNoMatch
}
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
return darwinSocketExactMatch
}
switch {
case localAddrMatches && remoteMatches:
return darwinSocketExactMatch
case localAddrMatches:
return darwinSocketLocalMatch
case remoteMatches:
return darwinSocketRemoteMatch
default:
return darwinSocketPortMatch
}
}
func darwinPortMatches(value int32, port uint16) bool {
raw := uint16(value)
return raw == port || darwinNtohs(raw) == port
}
func darwinNtohs(value uint16) uint16 {
return value<<8 | value>>8
}
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
if addr.Is4() {
ip := addr.As4()
return bytes.Equal(raw[12:16], ip[:])
}
ip := addr.As16()
return bytes.Equal(raw, ip[:])
}
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
if darwinAddrMatches(raw, addr) {
return true
}
if addr.Is4() {
return darwinBytesAreZero(raw[12:16])
}
return darwinBytesAreZero(raw)
}
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
}
func darwinBytesAreZero(raw []byte) bool {
for _, value := range raw {
if value != 0 {
return false
}
}
return true
}
func darwinReadNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func darwinProcessPath(pid int32) (string, error) {
buf := make([]byte, unix.PathMax)
n, err := darwinProcPIDPath(pid, buf)
if err != nil {
return "", err
}
if n <= 0 {
return "", errors.New("empty process path")
}
return strings.TrimRight(string(buf[:n]), "\x00"), nil
}
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
var libc_proc_pidinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
var libc_proc_pidfdinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
var libc_proc_pidpath_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
// Implemented in the runtime package (runtime/sys_darwin.go).
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
//go:linkname syscall_syscall6 syscall.syscall6
+18
View File
@@ -0,0 +1,18 @@
//go:build darwin && !ios
#include "textflag.h"
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidinfo(SB)
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidfdinfo(SB)
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidpath(SB)
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)
+356
View File
@@ -0,0 +1,356 @@
//go:build darwin && !ios
package net
import (
stdnet "net"
"net/netip"
"os"
"testing"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
func TestFindProcessDarwinTCP(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
remote := conn.RemoteAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPIPv4Mapped(t *testing.T) {
listener, err := stdnet.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
listenerAddr := listener.Addr().(*stdnet.TCPAddr)
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
if err != nil {
t.Fatal(err)
}
defer unix.Close(fd)
mappedAddr := [16]byte{10: 0xff, 11: 0xff, 12: 127, 15: 1}
if err := unix.Connect(fd, &unix.SockaddrInet6{
Port: listenerAddr.Port,
Addr: mappedAddr,
}); err != nil {
t.Fatal(err)
}
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local, err := unix.Getsockname(fd)
if err != nil {
t.Fatal(err)
}
localPort := local.(*unix.SockaddrInet6).Port
pid, name, path, err := FindProcess("tcp", "127.0.0.1", uint16(localPort), "127.0.0.1", uint16(listenerAddr.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinUDP(t *testing.T) {
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
local := conn.LocalAddr().(*stdnet.UDPAddr)
pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinNonLocal(t *testing.T) {
_, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0)
if err != ErrNotLocal {
t.Fatalf("expected ErrNotLocal, got %v", err)
}
}
func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic")
}
}()
_, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0)
}
func assertCurrentProcess(t *testing.T, pid int, name string, path string) {
t.Helper()
if pid != os.Getpid() {
t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path)
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if path == "" || name == "" {
t.Fatalf("expected process path and name, got name=%q path=%q", name, path)
}
if sameFile(executable, path) {
return
}
t.Fatalf("expected executable %q, got %q", executable, path)
}
func sameFile(left string, right string) bool {
leftInfo, leftErr := os.Stat(left)
rightInfo, rightErr := os.Stat(right)
if leftErr != nil || rightErr != nil {
return false
}
return os.SameFile(leftInfo, rightInfo)
}
func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
src := netip.MustParseAddr("198.18.0.2")
dst := netip.MustParseAddr("203.0.113.10")
otherLocal := netip.MustParseAddr("192.168.1.10")
otherRemote := netip.MustParseAddr("198.51.100.10")
unspecifiedLocal := netip.MustParseAddr("0.0.0.0")
tests := []struct {
name string
local netip.Addr
remote netip.Addr
hasDst bool
wantLevel darwinSocketMatchLevel
}{
{
name: "exact",
local: src,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local with matching remote",
local: unspecifiedLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local without destination",
local: unspecifiedLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketExactMatch,
},
{
name: "local match with different remote",
local: src,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketLocalMatch,
},
{
name: "remote match with different local",
local: otherLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketRemoteMatch,
},
{
name: "port only with destination",
local: otherLocal,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketPortMatch,
},
{
name: "different local without destination",
local: otherLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketNoMatch,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443)
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst)
if level != test.wantLevel {
t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel)
}
})
}
}
func TestDarwinSocketInfoMatchLevelIPv4Mapped(t *testing.T) {
src := netip.MustParseAddr("127.0.0.1")
dst := netip.MustParseAddr("203.0.113.10")
info := newDarwinSocketInfo("tcp", src, 12345, dst, 443)
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET6))
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, true)
if level != darwinSocketExactMatch {
t.Fatalf("unexpected match level: got %d, want %d", level, darwinSocketExactMatch)
}
}
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
info := make([]byte, darwinSocketFDInfoSize)
switch network {
case "tcp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP))
case "udp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN))
}
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET))
info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4
writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort))
writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort))
copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local)
copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote)
return info
}
func writeDarwinNativeUint32(b []byte, offset int, value uint32) {
*(*uint32)(unsafe.Pointer(&b[offset])) = value
}
func copyDarwinIPv4(dst []byte, addr netip.Addr) {
ip := addr.As4()
copy(dst[12:16], ip[:])
}
+11
View File
@@ -0,0 +1,11 @@
//go:build ios
package net
import (
"github.com/xtls/xray-core/common/errors"
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
return 0, "", "", errors.New("process lookup is not supported on this platform")
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows && !linux && !android //go:build !windows && !linux && !android && !darwin
package net package net
+40
View File
@@ -0,0 +1,40 @@
package platform
import (
"errors"
"sync"
)
var envReloadRegistry = struct {
sync.RWMutex
handlers []func() error
}{}
// RegisterEnvReload registers an environment reload handler and runs it once
// immediately so package defaults keep the same behavior as init-time reads.
func RegisterEnvReload(handler func() error) {
if handler == nil {
return
}
envReloadRegistry.Lock()
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
envReloadRegistry.Unlock()
if err := handler(); err != nil {
panic(err)
}
}
// ReloadEnvSettings refreshes all registered environment-backed package state.
func ReloadEnvSettings() error {
envReloadRegistry.RLock()
handlers := append([]func() error{}, envReloadRegistry.handlers...)
envReloadRegistry.RUnlock()
var errs []error
for _, handler := range handlers {
if err := handler(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
+25 -34
View File
@@ -3,11 +3,8 @@ package bittorrent
import ( import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"math"
"time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
) )
type SniffHeader struct{} type SniffHeader struct{}
@@ -39,50 +36,44 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
return nil, common.ErrNoClue return nil, common.ErrNoClue
} }
buffer := buf.FromBytes(b) // type 4 (ST_SYN), version 1
if b[0] != 0x41 {
var typeAndVersion uint8
if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil {
return nil, common.ErrNoClue
} else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 {
return nil, errNotBittorrent return nil, errNotBittorrent
} }
var extension uint8 // timestamp_difference is always 0 in new connections
if binary.BigEndian.Uint32(b[8:12]) != 0 {
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
} else if extension != 0 && extension != 1 {
return nil, errNotBittorrent return nil, errNotBittorrent
} }
// Walk the extension chain. Selective ack (1) and extension bits (2)
extension, offset := b[1], 20
for extension != 0 { for extension != 0 {
if extension != 1 { if len(b) < offset+2 {
return nil, errNotBittorrent return nil, errNotBittorrent
} }
if binary.Read(buffer, binary.BigEndian, &extension) != nil { length := int(b[offset+1])
return nil, common.ErrNoClue switch extension {
case 1: // selective ack
if length < 4 || length%4 != 0 {
return nil, errNotBittorrent
}
case 2: // extension bits: fixed 8 bytes, sent in ST_SYN by µTorrent
if length != 8 {
return nil, errNotBittorrent
}
default:
return nil, errNotBittorrent
} }
if len(b) < offset+2+length {
var length uint8 return nil, errNotBittorrent
if err := binary.Read(buffer, binary.BigEndian, &length); err != nil {
return nil, common.ErrNoClue
}
if common.Error2(buffer.ReadBytes(int32(length))) != nil {
return nil, common.ErrNoClue
} }
extension = b[offset]
offset += 2 + length
} }
if common.Error2(buffer.ReadBytes(2)) != nil { // extensions should consume all ST_SYN payload
return nil, common.ErrNoClue if len(b) != offset {
}
var timestamp uint32
if err := binary.Read(buffer, binary.BigEndian, &timestamp); err != nil {
return nil, common.ErrNoClue
}
if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) {
return nil, errNotBittorrent return nil, errNotBittorrent
} }
@@ -0,0 +1,67 @@
package bittorrent
import (
"encoding/binary"
"testing"
"github.com/xtls/xray-core/common"
)
// utpPacket builds the fixed 20-byte header defined by BEP 29.
func utpPacket(packetType, extension byte, tsDiff uint32, payload ...byte) []byte {
b := make([]byte, 20)
b[0] = packetType<<4 | 1
b[1] = extension
binary.BigEndian.PutUint16(b[2:4], 0x4a3f) // connection_id, random
binary.BigEndian.PutUint32(b[4:8], 0x8c3a91d2) // timestamp_microseconds, sender's clock
binary.BigEndian.PutUint32(b[8:12], tsDiff)
binary.BigEndian.PutUint32(b[12:16], 0x00100000) // wnd_size
binary.BigEndian.PutUint16(b[16:18], 0x71ee) // seq_nr, random in libutp/libtorrent
binary.BigEndian.PutUint16(b[18:20], 0x0000) // ack_nr
return append(b, payload...)
}
func TestSniffUTP(t *testing.T) {
selectiveAck := []byte{0, 4, 0xff, 0x00, 0xff, 0x00}
wrongVersion := utpPacket(4, 0, 0)
wrongVersion[0] = 4<<4 | 2
cases := []struct {
name string
payload []byte
err error
}{
{"syn", utpPacket(4, 0, 0), nil},
{"syn with selective ack", append(utpPacket(4, 1, 0), selectiveAck...), nil},
{"syn with extension bits", append(utpPacket(4, 2, 0), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8), nil},
{"extension bits with wrong length", append(utpPacket(4, 2, 0), 0, 4, 1, 2, 3, 4), errNotBittorrent},
{"syn with nonzero timestamp_difference", utpPacket(4, 0, 0x1234), errNotBittorrent},
{"syn with trailing payload", utpPacket(4, 0, 0, 'x'), errNotBittorrent},
// txid 0x4100, no EDNS0: the worst case colliding with the uTP header
{"dns query", []byte{
0x41, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 'a', 0x02, 'c', 'o', 0x00, 0x00, 0x01, 0x00, 0x01,
}, errNotBittorrent},
{"established connection packets", utpPacket(0, 0, 0x5678, 'x', 'y', 'z'), errNotBittorrent},
{"state", utpPacket(2, 0, 0x5678), errNotBittorrent},
{"fin", utpPacket(1, 0, 0x5678), errNotBittorrent},
{"wrong version", wrongVersion, errNotBittorrent},
{"unknown packet type", utpPacket(5, 0, 0), errNotBittorrent},
{"unknown extension", utpPacket(4, 2, 0), errNotBittorrent},
{"extension chain past the datagram", utpPacket(4, 1, 0, 0, 8, 0xff), errNotBittorrent},
{"selective ack not in multiples of 4", append(utpPacket(4, 1, 0), 0, 3, 0xff, 0x00, 0xff), errNotBittorrent},
{"shorter than the header", utpPacket(4, 0, 0)[:19], common.ErrNoClue},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
h, err := SniffUTP(c.payload)
if err != c.err {
t.Fatalf("expected error %v, got %v", c.err, err)
}
if err == nil && h == nil {
t.Fatal("expected a sniff header, got nil")
}
})
}
}
+2 -10
View File
@@ -28,8 +28,6 @@ const (
SecurityType_AUTO SecurityType = 2 SecurityType_AUTO SecurityType = 2
SecurityType_AES128_GCM SecurityType = 3 SecurityType_AES128_GCM SecurityType = 3
SecurityType_CHACHA20_POLY1305 SecurityType = 4 SecurityType_CHACHA20_POLY1305 SecurityType = 4
SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06]
SecurityType_ZERO SecurityType = 6
) )
// Enum value maps for SecurityType. // Enum value maps for SecurityType.
@@ -39,16 +37,12 @@ var (
2: "AUTO", 2: "AUTO",
3: "AES128_GCM", 3: "AES128_GCM",
4: "CHACHA20_POLY1305", 4: "CHACHA20_POLY1305",
5: "NONE",
6: "ZERO",
} }
SecurityType_value = map[string]int32{ SecurityType_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
"AUTO": 2, "AUTO": 2,
"AES128_GCM": 3, "AES128_GCM": 3,
"CHACHA20_POLY1305": 4, "CHACHA20_POLY1305": 4,
"NONE": 5,
"ZERO": 6,
} }
) )
@@ -129,15 +123,13 @@ const file_common_protocol_headers_proto_rawDesc = "" +
"\n" + "\n" +
"\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" + "\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" +
"\x0eSecurityConfig\x126\n" + "\x0eSecurityConfig\x126\n" +
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" + "\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*L\n" +
"\fSecurityType\x12\v\n" + "\fSecurityType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\b\n" + "\aUNKNOWN\x10\x00\x12\b\n" +
"\x04AUTO\x10\x02\x12\x0e\n" + "\x04AUTO\x10\x02\x12\x0e\n" +
"\n" + "\n" +
"AES128_GCM\x10\x03\x12\x15\n" + "AES128_GCM\x10\x03\x12\x15\n" +
"\x11CHACHA20_POLY1305\x10\x04\x12\b\n" + "\x11CHACHA20_POLY1305\x10\x04B^\n" +
"\x04NONE\x10\x05\x12\b\n" +
"\x04ZERO\x10\x06B^\n" +
"\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3" "\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3"
var ( var (
-2
View File
@@ -11,8 +11,6 @@ enum SecurityType {
AUTO = 2; AUTO = 2;
AES128_GCM = 3; AES128_GCM = 3;
CHACHA20_POLY1305 = 4; CHACHA20_POLY1305 = 4;
NONE = 5; // [DEPRECATED 2023-06]
ZERO = 6;
} }
message SecurityConfig { message SecurityConfig {
-8
View File
@@ -1,18 +1,10 @@
package quic package quic
import ( import (
"crypto"
"crypto/cipher" "crypto/cipher"
_ "crypto/tls" _ "crypto/tls"
_ "unsafe" _ "unsafe"
) )
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13 //go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+91 -74
View File
@@ -3,7 +3,6 @@ package quic
import ( import (
"crypto" "crypto"
"crypto/aes" "crypto/aes"
"crypto/tls"
"encoding/binary" "encoding/binary"
"io" "io"
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
return s.domain return s.domain
} }
const ( var (
versionDraft29 uint32 = 0xff00001d errNotQUIC = errors.New("not quic")
version1 uint32 = 0x1 errNotQUICInitial = errors.New("not initial packet")
) )
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var ( var (
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} quicDraft29 = quicVersionSpec{
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} ver: 0xff00001d,
initialSuite = &CipherSuiteTLS13{ typeInitial: 0b00,
ID: tls.TLS_AES_128_GCM_SHA256, initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
KeyLen: 16, labelPrefix: "quic",
AEAD: AEADAESGCMTLS13, }
Hash: crypto.SHA256, quicV1 = quicVersionSpec{
ver: 0x1,
typeInitial: 0b00,
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
labelPrefix: "quic",
}
quicV2 = quicVersionSpec{
ver: 0x6b3343cf,
typeInitial: 0b01,
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
labelPrefix: "quicv2",
}
quicVersionSpecMap = map[uint32]*quicVersionSpec{
quicDraft29.ver: &quicDraft29,
quicV1.ver: &quicV1,
quicV2.ver: &quicV2,
} }
errNotQuic = errors.New("not quic")
errNotQuicInitial = errors.New("not initial packet")
) )
func SniffQUIC(b []byte) (*SniffHeader, error) { func SniffQUIC(b []byte) (*SniffHeader, error) {
@@ -63,60 +83,61 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b) buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte() typeByte, err := buffer.ReadByte()
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
isLongHeader := typeByte&0x80 > 0 isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 { if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQuicInitial return nil, errNotQUICInitial
} }
vb, err := buffer.ReadBytes(4) vb, err := buffer.ReadBytes(4)
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
versionNumber := binary.BigEndian.Uint32(vb) versionNumber := binary.BigEndian.Uint32(vb)
if versionNumber != 0 && typeByte&0x40 == 0 { var s *quicVersionSpec
return nil, errNotQuic if v, ok := quicVersionSpecMap[versionNumber]; ok {
} else if versionNumber != versionDraft29 && versionNumber != version1 { s = v
return nil, errNotQuic } else {
return nil, errNotQUIC
} }
packetType := (typeByte & 0x30) >> 4
isQuicInitial := packetType == 0x0
var destConnID []byte var destConnID []byte
if l, err := buffer.ReadByte(); err != nil { if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil { } else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
if l, err := buffer.ReadByte(); err != nil { if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil { } else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2 packetType := (typeByte & 0x30) >> 4
tokenLen, err := readShortQuicVarint(buffer) isQUICInitial := packetType == s.typeInitial
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQUICVarint(buffer)
if err != nil || tokenLen > int32(len(b)) { if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQuic return nil, errNotQUIC
} }
if _, err = buffer.ReadBytes(tokenLen); err != nil { if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
} }
packetLen, err := readShortQuicVarint(buffer) packetLen, err := readShortQUICVarint(buffer)
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
// packetLen is impossible to be shorter than this // packetLen is impossible to be shorter than this
if packetLen < 4 { if packetLen < 4 {
return nil, errNotQuic return nil, errNotQUIC
} }
hdrLen := len(b) - int(buffer.Len()) hdrLen := len(b) - int(buffer.Len())
@@ -125,25 +146,23 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
} }
restPayload := b[hdrLen+int(packetLen):] restPayload := b[hdrLen+int(packetLen):]
if !isQuicInitial { // Skip this packet if it's not initial packet if !isQUICInitial { // Skip this packet if it's not initial packet
b = restPayload b = restPayload
continue continue
} }
var salt []byte salt := s.initialSalt
if versionNumber == version1 { label := s.labelPrefix
salt = quicSalt
} else {
salt = quicSaltOld
}
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen) hpKey := hkdfExpandLabel(secret, label+" hp", 16)
block, err := aes.NewCipher(hpKey) block, err := aes.NewCipher(hpKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQUIC
}
cache.Clear() cache.Clear()
mask := cache.Extend(int32(block.BlockSize())) mask := cache.Extend(int32(block.BlockSize()))
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)]) block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
@@ -153,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1] b[hdrLen+i] ^= mask[i+1]
} }
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) iv := hkdfExpandLabel(secret, label+" iv", 12)
cipher := AEADAESGCMTLS13(key, iv) cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize())) nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -179,44 +198,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame case 0x00: // PADDING frame
case 0x01: // PING frame case 0x01: // PING frame
case 0x02, 0x03: // ACK frame case 0x02, 0x03: // ACK frame
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
} }
if frameType == 0x03 { if frameType == 0x03 {
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
} }
case 0x06: // CRYPTO frame, we will use this frame case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQuicVarint(buffer) // Field: Offset offset, err := readShortQUICVarint(buffer) // Field: Offset
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
length, err := readShortQuicVarint(buffer) // Field: Length length, err := readShortQUICVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() { if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
@@ -232,13 +251,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
@@ -248,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default: default:
// Only above frame types are permitted in initial packet. // Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8 // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQuicInitial return nil, errNotQUICInitial
} }
} }
@@ -266,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData return nil, protocol.ErrProtoNeedMoreData
} }
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context)) b := make([]byte, 0, 2+1+6+len(label)+1)
binary.BigEndian.PutUint16(b, uint16(length)) b = binary.BigEndian.AppendUint16(b, uint16(length))
b[2] = uint8(6 + len(label)) b = append(b, byte(6+len(label)))
b = append(b, []byte("tls13 ")...) b = append(b, "tls13 "...)
b = append(b, []byte(label)...) b = append(b, label...)
b = b[:3+6+len(label)+1] b = append(b, 0) // context
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
out := make([]byte, length) out := make([]byte, length)
n, err := hkdf.Expand(hash.New, secret, b).Read(out) n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
if err != nil || n != length { if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly") panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
} }
return out return out
} }
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields. // readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535 // we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion // returns int32 to reduce type conversion
func readShortQuicVarint(reader io.ByteReader) (int32, error) { func readShortQUICVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader) v, err := quicvarint.Read(reader)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if v > 65535 { if v > 65535 {
// not used( // not used(
return 0, errNotQuicInitial return 0, errNotQUICInitial
} }
return int32(v), nil return int32(v), nil
} }
File diff suppressed because one or more lines are too long
+1
View File
@@ -207,6 +207,7 @@ func getConfig() string {
"tag": "XHTTP_IN", "tag": "XHTTP_IN",
"streamSettings": { "streamSettings": {
"network": "xhttp", "network": "xhttp",
"security": "tls",
"xhttpSettings": { "xhttpSettings": {
"host": "bing.com", "host": "bing.com",
"path": "/xhttp_client_upload", "path": "/xhttp_client_upload",
-2
View File
@@ -70,8 +70,6 @@ type Outbound struct {
Tag string Tag string
// Name of the outbound proxy that handles the connection. // Name of the outbound proxy that handles the connection.
Name string Name string
// Unused. Conn is actually internet.Connection. May be nil. It is currently nil for outbound with proxySettings
Conn net.Conn
// CanSpliceCopy is a property for this connection // CanSpliceCopy is a property for this connection
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot // 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
CanSpliceCopy int CanSpliceCopy int
+33 -19
View File
@@ -8,7 +8,7 @@ import (
"io" "io"
"strconv" "strconv"
"strings" "strings"
"time" "sync/atomic"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -27,25 +27,39 @@ var AddrParser = protocol.NewAddressParser(
) )
var ( var (
Show bool Show atomic.Bool
BaseKey []byte baseKey atomic.Value
) )
func init() { func reloadEnvSettings() error {
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" { Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true")
Show = true raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" })
if raw == "" {
ensureBaseKey()
return nil
} }
BaseKey = make([]byte, 32) key, _ := base64.RawURLEncoding.DecodeString(raw)
rand.Read(BaseKey) if len(key) != 32 {
go func() { return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key)))
time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV }
if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" { baseKey.Store(append([]byte(nil), key...))
if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 { return nil
return }
}
panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey))) func ensureBaseKey() []byte {
} if key := baseKey.Load(); key != nil {
}() return key.([]byte)
}
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
panic(err)
}
baseKey.Store(key)
return key
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
} }
func GetGlobalID(ctx context.Context) (globalID [8]byte) { func GetGlobalID(ctx context.Context) (globalID [8]byte) {
@@ -54,10 +68,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
} }
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP && if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") { (inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
h := blake3.New(8, BaseKey) h := blake3.New(8, ensureBaseKey())
h.Write([]byte(inbound.Source.String())) h.Write([]byte(inbound.Source.String()))
copy(globalID[:], h.Sum(nil)) copy(globalID[:], h.Sum(nil))
if Show { if Show.Load() {
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID)) errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
} }
} }
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 6 Version_y byte = 9
Version_z byte = 27 Version_z byte = 8
) )
var ( var (
+3
View File
@@ -187,6 +187,9 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) {
} }
func initInstanceWithConfig(config *Config, server *Instance) (bool, error) { func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
if err := platform.ReloadEnvSettings(); err != nil {
return true, errors.New("failed to reload environment settings").Base(err)
}
server.ctx = context.WithValue(server.ctx, "cone", server.ctx = context.WithValue(server.ctx, "cone",
platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true") platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true")
+18 -8
View File
@@ -3,6 +3,7 @@ package policy
import ( import (
"context" "context"
"runtime" "runtime"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
@@ -82,32 +83,41 @@ func ManagerType() interface{} {
return (*Manager)(nil) return (*Manager)(nil)
} }
var defaultBufferSize int32 var defaultBufferSize atomic.Int32
func init() { func reloadEnvSettings() error {
defaultBufferSize.Store(readDefaultBufferSize())
return nil
}
func readDefaultBufferSize() int32 {
const defaultValue = -17 const defaultValue = -17
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue) size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
switch size { switch size {
case 0: case 0:
defaultBufferSize = -1 // For pipe to use unlimited size return -1 // For pipe to use unlimited size
case defaultValue: // Env flag not defined. Use default values per CPU-arch. case defaultValue: // Env flag not defined. Use default values per CPU-arch.
switch runtime.GOARCH { switch runtime.GOARCH {
case "arm", "mips", "mipsle": case "arm", "mips", "mipsle":
defaultBufferSize = 0 return 0
case "arm64", "mips64", "mips64le": case "arm64", "mips64", "mips64le":
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices return 4 * 1024 // 4k cache for low-end devices
default: default:
defaultBufferSize = 512 * 1024 return 512 * 1024
} }
default: default:
defaultBufferSize = int32(size) * 1024 * 1024 return int32(size) * 1024 * 1024
} }
} }
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
}
func defaultBufferPolicy() Buffer { func defaultBufferPolicy() Buffer {
return Buffer{ return Buffer{
PerConnection: defaultBufferSize, PerConnection: defaultBufferSize.Load(),
} }
} }
+21 -30
View File
@@ -81,6 +81,8 @@ type Manager interface {
// RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters. // RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters.
RegisterCounter(string) (Counter, error) RegisterCounter(string) (Counter, error)
// GetOrRegisterCounter returns the counter by its identifier, atomically creating and registering it if absent.
GetOrRegisterCounter(string) (Counter, error)
// UnregisterCounter unregisters a counter from the manager by its identifier. // UnregisterCounter unregisters a counter from the manager by its identifier.
UnregisterCounter(string) error UnregisterCounter(string) error
// GetCounter returns a counter by its identifier. // GetCounter returns a counter by its identifier.
@@ -91,6 +93,8 @@ type Manager interface {
// RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps. // RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps.
RegisterOnlineMap(string) (OnlineMap, error) RegisterOnlineMap(string) (OnlineMap, error)
// GetOrRegisterOnlineMap returns the OnlineMap by its identifier, atomically creating and registering it if absent.
GetOrRegisterOnlineMap(string) (OnlineMap, error)
// UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier. // UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier.
UnregisterOnlineMap(string) error UnregisterOnlineMap(string) error
// GetOnlineMap returns an OnlineMap by its identifier. // GetOnlineMap returns an OnlineMap by its identifier.
@@ -101,6 +105,8 @@ type Manager interface {
// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels. // RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
RegisterChannel(string) (Channel, error) RegisterChannel(string) (Channel, error)
// GetOrRegisterChannel returns the channel by its identifier, atomically creating and registering it if absent.
GetOrRegisterChannel(string) (Channel, error)
// UnregisterChannel unregisters a channel from the manager by its identifier. // UnregisterChannel unregisters a channel from the manager by its identifier.
UnregisterChannel(string) error UnregisterChannel(string) error
// GetChannel returns a channel by its identifier. // GetChannel returns a channel by its identifier.
@@ -110,36 +116,6 @@ type Manager interface {
GetAllOnlineUsers() []string GetAllOnlineUsers() []string
} }
// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter.
func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
counter := m.GetCounter(name)
if counter != nil {
return counter, nil
}
return m.RegisterCounter(name)
}
// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new OnlineMap.
func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
onlineMap := m.GetOnlineMap(name)
if onlineMap != nil {
return onlineMap, nil
}
return m.RegisterOnlineMap(name)
}
// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
channel := m.GetChannel(name)
if channel != nil {
return channel, nil
}
return m.RegisterChannel(name)
}
// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. // ManagerType returns the type of Manager interface. Can be used to implement common.HasType.
// //
// xray:api:stable // xray:api:stable
@@ -160,6 +136,11 @@ func (NoopManager) RegisterCounter(string) (Counter, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
// GetOrRegisterCounter implements Manager.
func (NoopManager) GetOrRegisterCounter(string) (Counter, error) {
return nil, errors.New("not implemented")
}
// UnregisterCounter implements Manager. // UnregisterCounter implements Manager.
func (NoopManager) UnregisterCounter(string) error { func (NoopManager) UnregisterCounter(string) error {
return nil return nil
@@ -178,6 +159,11 @@ func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
// GetOrRegisterOnlineMap implements Manager.
func (NoopManager) GetOrRegisterOnlineMap(string) (OnlineMap, error) {
return nil, errors.New("not implemented")
}
// UnregisterOnlineMap implements Manager. // UnregisterOnlineMap implements Manager.
func (NoopManager) UnregisterOnlineMap(string) error { func (NoopManager) UnregisterOnlineMap(string) error {
return nil return nil
@@ -196,6 +182,11 @@ func (NoopManager) RegisterChannel(string) (Channel, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
// GetOrRegisterChannel implements Manager.
func (NoopManager) GetOrRegisterChannel(string) (Channel, error) {
return nil, errors.New("not implemented")
}
// UnregisterChannel implements Manager. // UnregisterChannel implements Manager.
func (NoopManager) UnregisterChannel(string) error { func (NoopManager) UnregisterChannel(string) error {
return nil return nil
+28 -24
View File
@@ -1,61 +1,65 @@
module github.com/xtls/xray-core module github.com/xtls/xray-core
go 1.26 go 1.27
require ( require (
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.4 github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0 github.com/klauspost/cpuid/v2 v2.4.0
github.com/miekg/dns v1.1.72 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/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.6 github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.14.0 github.com/pires/go-proxyproto v0.15.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1 github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.56.0 golang.org/x/net v0.58.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.22.0
golang.org/x/sys v0.46.0 golang.org/x/sys v0.47.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1 golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.82.0 google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3 h12.io/socks v1.0.3
lukechampine.com/blake3 v1.4.1 lukechampine.com/blake3 v1.4.1
mvdan.cc/gofumpt v0.12.0
) )
require ( require (
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // 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/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/text v0.2.0 // indirect github.com/koron/go-ssdp v0.0.4 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect github.com/pion/transport/v4 v4.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.36.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.38.0 // indirect golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+71 -57
View File
@@ -1,20 +1,19 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= 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.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -23,97 +22,111 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
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 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= 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 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 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 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.4.0/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= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8=
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.14.0 h1:2vIGIfVG8eVRsKF0xukEoeT5RWhDXxBU0uv6smLOKdI= github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.14.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 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 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 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-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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 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/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-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.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,21 +134,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.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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.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.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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 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/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-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-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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -147,23 +161,23 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
+20 -30
View File
@@ -1,52 +1,42 @@
package conf package conf
import ( import (
"encoding/json" "encoding/base64"
"strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/blackhole" "github.com/xtls/xray-core/proxy/blackhole"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
type NoneResponse struct{} type ResponseConfig struct {
Type string `json:"type"`
func (*NoneResponse) Build() (proto.Message, error) { CustomResponseData string `json:"customResponseData"`
return new(blackhole.NoneResponse), nil
}
type HTTPResponse struct{}
func (*HTTPResponse) Build() (proto.Message, error) {
return new(blackhole.HTTPResponse), nil
} }
type BlackholeConfig struct { type BlackholeConfig struct {
Response json.RawMessage `json:"response"` Response *ResponseConfig `json:"response"`
} }
func (v *BlackholeConfig) Build() (proto.Message, error) { func (v *BlackholeConfig) Build() (proto.Message, error) {
config := new(blackhole.Config) config := new(blackhole.Config)
if v.Response != nil { if v.Response != nil {
response, _, err := configLoader.Load(v.Response) responseName := strings.ToLower(v.Response.Type)
if err != nil { switch responseName {
return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err) case "none", "":
config.Response = &blackhole.Response{Type: "none"}
case "http":
config.Response = &blackhole.Response{Type: "http"}
case "custom":
data, err := base64.StdEncoding.DecodeString(v.Response.CustomResponseData)
if err != nil {
return nil, errors.New("failed to decode custom response data: " + err.Error())
}
config.Response = &blackhole.Response{Type: "custom", CustomResponseData: data}
default:
return nil, errors.New("unknown blackhole response: " + responseName)
} }
responseSettings, err := response.(Buildable).Build()
if err != nil {
return nil, err
}
config.Response = serial.ToTypedMessage(responseSettings)
} }
return config, nil return config, nil
} }
var configLoader = NewJSONConfigLoader(
ConfigCreatorCache{
"none": func() interface{} { return new(NoneResponse) },
"http": func() interface{} { return new(HTTPResponse) },
},
"type",
"",
)
+25 -2
View File
@@ -3,7 +3,6 @@ package conf_test
import ( import (
"testing" "testing"
"github.com/xtls/xray-core/common/serial"
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/blackhole" "github.com/xtls/xray-core/proxy/blackhole"
) )
@@ -22,7 +21,7 @@ func TestHTTPResponseJSON(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &blackhole.Config{ Output: &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), Response: &blackhole.Response{Type: "http"},
}, },
}, },
{ {
@@ -32,3 +31,27 @@ func TestHTTPResponseJSON(t *testing.T) {
}, },
}) })
} }
func TestCustomResponseJSON(t *testing.T) {
creator := func() Buildable {
return new(BlackholeConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"response": {
"type": "custom",
"customResponseData": "Y3VzdG9tIHJlc3BvbnNl"
}
}`,
Parser: loadJSON(creator),
Output: &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: []byte("custom response"),
},
},
},
})
}
+3
View File
@@ -65,6 +65,9 @@ func (v *Address) UnmarshalJSON(data []byte) error {
} }
func (v *Address) Build() *net.IPOrDomain { func (v *Address) Build() *net.IPOrDomain {
if v == nil {
return nil
}
return net.NewIPOrDomain(v.Address) return net.NewIPOrDomain(v.Address)
} }
+59
View File
@@ -0,0 +1,59 @@
package conf
import (
"encoding/json"
"os"
"testing"
)
func TestRootEnvAppliesArbitraryValues(t *testing.T) {
const (
valueKey = "XRAY_TEST_CONFIG_ENV"
emptyKey = "XRAY_TEST_CONFIG_EMPTY"
)
t.Setenv(valueKey, "before")
t.Setenv(emptyKey, "before")
config := new(Config)
if err := json.Unmarshal([]byte(`{
"env": {
"XRAY_TEST_CONFIG_ENV": "configured",
"XRAY_TEST_CONFIG_EMPTY": ""
}
}`), config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err != nil {
t.Fatal(err)
}
if got := os.Getenv(valueKey); got != "configured" {
t.Fatalf("env %q = %q, want %q", valueKey, got, "configured")
}
if got := os.Getenv(emptyKey); got != "" {
t.Fatalf("env %q = %q, want empty", emptyKey, got)
}
}
func TestEnvConfigOverride(t *testing.T) {
base := EnvConfig{
"ONE": "one",
"TWO": "old",
}
override := EnvConfig{
"TWO": "new",
"THREE": "three",
}
base.Override(override)
want := map[string]string{
"ONE": "one",
"TWO": "new",
"THREE": "three",
}
for key, value := range want {
if got := base[key]; got != value {
t.Fatalf("env %q = %q, want %q", key, got, value)
}
}
}
-41
View File
@@ -1,41 +0,0 @@
package conf
import (
"github.com/xtls/xray-core/transport/internet/grpc"
"google.golang.org/protobuf/proto"
)
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
func (g *GRPCConfig) Build() (proto.Message, error) {
if g.IdleTimeout <= 0 {
g.IdleTimeout = 0
}
if g.HealthCheckTimeout <= 0 {
g.HealthCheckTimeout = 0
}
if g.InitialWindowsSize < 0 {
// default window size of gRPC-go
g.InitialWindowsSize = 0
}
return &grpc.Config{
Authority: g.Authority,
ServiceName: g.ServiceName,
MultiMode: g.MultiMode,
IdleTimeout: g.IdleTimeout,
HealthCheckTimeout: g.HealthCheckTimeout,
PermitWithoutStream: g.PermitWithoutStream,
InitialWindowsSize: g.InitialWindowsSize,
UserAgent: g.UserAgent,
}, nil
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
type BurstObservatoryConfig struct { type BurstObservatoryConfig struct {
SubjectSelector []string `json:"subjectSelector"` SubjectSelector []string `json:"subjectSelector"`
// health check settings // health check settings
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"` HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
} }
func (b BurstObservatoryConfig) Build() (proto.Message, error) { func (b BurstObservatoryConfig) Build() (proto.Message, error) {
+5
View File
@@ -148,6 +148,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
LocalIP *StringList `json:"localIP"` LocalIP *StringList `json:"localIP"`
LocalPort *PortList `json:"localPort"` LocalPort *PortList `json:"localPort"`
Process *StringList `json:"process"` Process *StringList `json:"process"`
LocalOS *StringList `json:"localOS"`
Webhook *WebhookRuleConfig `json:"webhook"` Webhook *WebhookRuleConfig `json:"webhook"`
} }
rawFieldRule := new(RawFieldRule) rawFieldRule := new(RawFieldRule)
@@ -261,6 +262,10 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
rule.Process = *rawFieldRule.Process rule.Process = *rawFieldRule.Process
} }
if rawFieldRule.LocalOS != nil && len(*rawFieldRule.LocalOS) > 0 {
rule.LocalOs = *rawFieldRule.LocalOS
}
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" { if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
rule.Webhook = &router.WebhookConfig{ rule.Webhook = &router.WebhookConfig{
Url: rawFieldRule.Webhook.URL, Url: rawFieldRule.Webhook.URL,
+3 -3
View File
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
Tolerance float64 `json:"tolerance,omitempty"` Tolerance float64 `json:"tolerance,omitempty"`
} }
// healthCheckSettings holds settings for health Checker // HealthCheckSettings holds settings for health Checker
type healthCheckSettings struct { type HealthCheckSettings struct {
Destination string `json:"destination"` Destination string `json:"destination"`
Connectivity string `json:"connectivity"` Connectivity string `json:"connectivity"`
Interval duration.Duration `json:"interval"` Interval duration.Duration `json:"interval"`
@@ -53,7 +53,7 @@ type healthCheckSettings struct {
HttpMethod string `json:"httpMethod"` HttpMethod string `json:"httpMethod"`
} }
func (h healthCheckSettings) Build() (proto.Message, error) { func (h HealthCheckSettings) Build() (proto.Message, error) {
var httpMethod string var httpMethod string
if h.HttpMethod == "" { if h.HttpMethod == "" {
httpMethod = "HEAD" httpMethod = "HEAD"
-2
View File
@@ -24,8 +24,6 @@ func cipherFromString(c string) shadowsocks.CipherType {
return shadowsocks.CipherType_CHACHA20_POLY1305 return shadowsocks.CipherType_CHACHA20_POLY1305
case "xchacha20-poly1305", "aead_xchacha20_poly1305", "xchacha20-ietf-poly1305": case "xchacha20-poly1305", "aead_xchacha20_poly1305", "xchacha20-ietf-poly1305":
return shadowsocks.CipherType_XCHACHA20_POLY1305 return shadowsocks.CipherType_XCHACHA20_POLY1305
case "none", "plain":
return shadowsocks.CipherType_NONE
default: default:
return shadowsocks.CipherType_UNKNOWN return shadowsocks.CipherType_UNKNOWN
} }
-208
View File
@@ -1,208 +0,0 @@
package conf
import (
"sort"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet/headers/http"
"github.com/xtls/xray-core/transport/internet/headers/noop"
"google.golang.org/protobuf/proto"
)
type NoOpConnectionAuthenticator struct{}
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
return new(noop.ConnectionConfig), nil
}
type AuthenticatorRequest struct {
Version string `json:"version"`
Method string `json:"method"`
Path StringList `json:"path"`
Headers map[string]*StringList `json:"headers"`
}
func sortMapKeys(m map[string]*StringList) []string {
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
config := &http.RequestConfig{
Uri: []string{"/"},
Header: []*http.Header{
{
Name: "Host",
Value: []string{"www.baidu.com", "www.bing.com"},
},
{
Name: "User-Agent",
Value: []string{utils.ChromeUA},
},
{
Name: "Sec-CH-UA",
Value: []string{utils.ChromeUACH},
},
{
Name: "Sec-CH-UA-Mobile",
Value: []string{"?0"},
},
{
Name: "Sec-CH-UA-Platform",
Value: []string{"Windows"},
},
{
Name: "Sec-Fetch-Mode",
Value: []string{"no-cors", "cors", "same-origin"},
},
{
Name: "Sec-Fetch-Dest",
Value: []string{"empty"},
},
{
Name: "Sec-Fetch-Site",
Value: []string{"none"},
},
{
Name: "Sec-Fetch-User",
Value: []string{"?1"},
},
{
Name: "Accept-Encoding",
Value: []string{"gzip, deflate"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Method) > 0 {
config.Method = &http.Method{Value: v.Method}
}
if len(v.Path) > 0 {
config.Uri = append([]string(nil), v.Path...)
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type AuthenticatorResponse struct {
Version string `json:"version"`
Status string `json:"status"`
Reason string `json:"reason"`
Headers map[string]*StringList `json:"headers"`
}
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
config := &http.ResponseConfig{
Header: []*http.Header{
{
Name: "Content-Type",
Value: []string{"application/octet-stream", "video/mpeg"},
},
{
Name: "Transfer-Encoding",
Value: []string{"chunked"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
{
Name: "Cache-Control",
Value: []string{"private", "no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Status) > 0 || len(v.Reason) > 0 {
config.Status = &http.Status{
Code: "200",
Reason: "OK",
}
if len(v.Status) > 0 {
config.Status.Code = v.Status
}
if len(v.Reason) > 0 {
config.Status.Reason = v.Reason
}
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type Authenticator struct {
Request AuthenticatorRequest `json:"request"`
Response AuthenticatorResponse `json:"response"`
}
func (v *Authenticator) Build() (proto.Message, error) {
config := new(http.Config)
requestConfig, err := v.Request.Build()
if err != nil {
return nil, err
}
config.Request = requestConfig
responseConfig, err := v.Response.Build()
if err != nil {
return nil, err
}
config.Response = responseConfig
return config, nil
}
+959
View File
@@ -0,0 +1,959 @@
package conf
import (
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/netip"
"net/url"
"regexp"
"strings"
googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"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"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
"github.com/xtls/xray-core/transport/internet/finalmask/realm"
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
"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/tls"
"google.golang.org/protobuf/proto"
)
func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
switch strings.ToLower(typ) {
case "", "array":
if len(data) == 0 {
return data, nil
}
var packet []byte
if err := json.Unmarshal(data, &packet); err != nil {
return nil, err
}
return packet, nil
case "str":
var str string
if err := json.Unmarshal(data, &str); err != nil {
return nil, err
}
return []byte(str), nil
case "hex":
var str string
if err := json.Unmarshal(data, &str); err != nil {
return nil, err
}
return hex.DecodeString(str)
case "base64":
var str string
if err := json.Unmarshal(data, &str); err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(str)
default:
return nil, errors.New("unknown type")
}
}
var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) },
"xmc": func() interface{} { return new(XMC) },
}, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"header-custom": func() interface{} { return new(HeaderCustomUDP) },
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
"noise": func() interface{} { return new(NoiseMask) },
"salamander": func() interface{} { return new(Salamander) },
"sudoku": func() interface{} { return new(Sudoku) },
"xdns": func() interface{} { return new(Xdns) },
"xicmp": func() interface{} { return new(Xicmp) },
"realm": func() interface{} { return new(Realm) },
}, "type", "settings")
)
type TCPItem struct {
Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Capture string `json:"capture"`
Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
}
type HeaderCustomTCP struct {
Clients [][]TCPItem `json:"clients"`
Servers [][]TCPItem `json:"servers"`
Errors [][]TCPItem `json:"errors"`
}
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
for _, value := range c.Clients {
for _, item := range value {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
for _, value := range c.Servers {
for _, item := range value {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
for _, value := range c.Errors {
for _, item := range value {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
errInvalidRange := errors.New("invalid randRange")
clients := make([]*custom.TCPSequence, len(c.Clients))
for i, value := range c.Clients {
clients[i] = &custom.TCPSequence{}
for _, item := range value {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errInvalidRange
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
servers := make([]*custom.TCPSequence, len(c.Servers))
for i, value := range c.Servers {
servers[i] = &custom.TCPSequence{}
for _, item := range value {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errInvalidRange
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
errors := make([]*custom.TCPSequence, len(c.Errors))
for i, value := range c.Errors {
errors[i] = &custom.TCPSequence{}
for _, item := range value {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errInvalidRange
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
return &custom.TCPConfig{
Clients: clients,
Servers: servers,
Errors: errors,
}, nil
}
type FragmentMask struct {
Packets string `json:"packets"`
Length Int32Range `json:"length"`
Delay Int32Range `json:"delay"`
Lengths []Int32Range `json:"lengths"`
Delays []Int32Range `json:"delays"`
MaxSplit Int32Range `json:"maxSplit"`
}
func (c *FragmentMask) Build() (proto.Message, error) {
config := &fragment.Config{}
switch strings.ToLower(c.Packets) {
case "tlshello":
config.PacketsFrom = 0
config.PacketsTo = 1
case "":
config.PacketsFrom = 0
config.PacketsTo = 0
default:
from, to, err := ParseRangeString(c.Packets)
if err != nil {
return nil, errors.New("Invalid PacketsFrom").Base(err)
}
config.PacketsFrom = int64(from)
config.PacketsTo = int64(to)
if config.PacketsFrom == 0 {
return nil, errors.New("PacketsFrom can't be 0")
}
}
if len(c.Lengths) > 0 {
for _, r := range c.Lengths {
config.LengthsMin = append(config.LengthsMin, int64(r.From))
config.LengthsMax = append(config.LengthsMax, int64(r.To))
}
} else {
config.LengthsMin = append(config.LengthsMin, int64(c.Length.From))
config.LengthsMax = append(config.LengthsMax, int64(c.Length.To))
}
if config.LengthsMin[len(config.LengthsMin)-1] == 0 {
return nil, errors.New("last lengths entry min can't be 0")
}
if len(c.Delays) > 0 {
for _, r := range c.Delays {
config.DelaysMin = append(config.DelaysMin, int64(r.From))
config.DelaysMax = append(config.DelaysMax, int64(r.To))
}
} else {
config.DelaysMin = append(config.DelaysMin, int64(c.Delay.From))
config.DelaysMax = append(config.DelaysMax, int64(c.Delay.To))
}
config.MaxSplitMin = int64(c.MaxSplit.From)
config.MaxSplitMax = int64(c.MaxSplit.To)
return config, nil
}
type NoiseItem struct {
Rand Int32Range `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Type string `json:"type"`
Packet json.RawMessage `json:"packet"`
Delay Int32Range `json:"delay"`
}
type NoiseMask struct {
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
}
func (c *NoiseMask) Build() (proto.Message, error) {
for _, item := range c.Noise {
if len(item.Packet) > 0 && item.Rand.To > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0")
}
}
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
for _, item := range c.Noise {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errors.New("invalid randRange")
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
noiseSlice = append(noiseSlice, &noise.Item{
RandMin: int64(item.Rand.From),
RandMax: int64(item.Rand.To),
RandRangeMin: item.RandRange.From,
RandRangeMax: item.RandRange.To,
Packet: item.Packet,
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
})
}
return &noise.Config{
ResetMin: int64(c.Reset.From),
ResetMax: int64(c.Reset.To),
Items: noiseSlice,
}, nil
}
type UDPItem struct {
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Capture string `json:"capture"`
Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
}
type CustomTransform struct {
Op string `json:"op"`
Args []CustomTransformArg `json:"args"`
}
type CustomTransformArg struct {
Type string `json:"type"`
Bytes json.RawMessage `json:"bytes"`
U64 *uint64 `json:"u64"`
Reuse string `json:"reuse"`
Metadata string `json:"metadata"`
Transform *CustomTransform `json:"transform"`
}
func validateCustomVarName(name string) error {
if name == "" {
return nil
}
if !customVarNamePattern.MatchString(name) {
return errors.New("invalid variable name")
}
return nil
}
func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error {
if err := validateCustomVarName(capture); err != nil {
return err
}
if err := validateCustomVarName(reuse); err != nil {
return err
}
kindCount := 0
if len(packet) > 0 {
kindCount++
}
if rand > 0 {
kindCount++
}
if reuse != "" {
kindCount++
}
if transform != nil {
kindCount++
}
if kindCount > 1 {
return errors.New("exactly one item kind must be set")
}
if kindCount == 0 && capture != "" {
return errors.New("exactly one item kind must be set")
}
return nil
}
func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) {
if transform == nil {
return nil, nil
}
if transform.Op == "" {
return nil, errors.New("transform op is required")
}
if len(transform.Args) == 0 {
return nil, errors.New("transform args are required")
}
args := make([]*custom.ExprArg, 0, len(transform.Args))
for _, arg := range transform.Args {
parsedArg, err := buildCustomTransformArg(arg)
if err != nil {
return nil, err
}
args = append(args, parsedArg)
}
return &custom.Expr{
Op: transform.Op,
Args: args,
}, nil
}
func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) {
kindCount := 0
if len(arg.Bytes) > 0 {
kindCount++
}
if arg.U64 != nil {
kindCount++
}
if arg.Reuse != "" {
kindCount++
}
if arg.Metadata != "" {
kindCount++
}
if arg.Transform != nil {
kindCount++
}
if kindCount != 1 {
return nil, errors.New("transform arg must set exactly one value")
}
if len(arg.Bytes) > 0 {
value, err := PraseByteSlice(arg.Bytes, arg.Type)
if err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Bytes{
Bytes: value,
},
}, nil
}
if arg.U64 != nil {
return &custom.ExprArg{
Value: &custom.ExprArg_U64{
U64: *arg.U64,
},
}, nil
}
if arg.Reuse != "" {
if err := validateCustomVarName(arg.Reuse); err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Var{
Var: arg.Reuse,
},
}, nil
}
if arg.Metadata != "" {
return &custom.ExprArg{
Value: &custom.ExprArg_Metadata{
Metadata: arg.Metadata,
},
}, nil
}
parsedExpr, err := buildCustomTransform(arg.Transform)
if err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Expr{
Expr: parsedExpr,
},
}, nil
}
type HeaderCustomUDP struct {
Mode string `json:"mode"`
Client []UDPItem `json:"client"`
Server []UDPItem `json:"server"`
}
func (c *HeaderCustomUDP) Build() (proto.Message, error) {
switch c.Mode {
case "", "prefix", "standalone":
default:
return nil, errors.New("unknown udp mode")
}
for _, item := range c.Client {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
for _, item := range c.Server {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
client := make([]*custom.UDPItem, 0, len(c.Client))
for _, item := range c.Client {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errors.New("invalid randRange")
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
client = append(client, &custom.UDPItem{
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
server := make([]*custom.UDPItem, 0, len(c.Server))
for _, item := range c.Server {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errors.New("invalid randRange")
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
server = append(server, &custom.UDPItem{
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
if c.Mode == "standalone" {
return &custom.UDPStandaloneConfig{
Client: client,
Server: server,
}, nil
} else {
return &custom.UDPConfig{
Client: client,
Server: server,
}, nil
}
}
type MkcpLegacy struct {
Header string `json:"header"`
Value string `json:"value"`
}
func (c *MkcpLegacy) Build() (proto.Message, error) {
if len(c.Header) == 0 {
if len(c.Value) == 0 {
return &original.Config{}, nil
} else {
return &aes128gcm.Config{Password: c.Value}, nil
}
}
switch strings.ToLower(c.Header) {
case "dns":
domain := c.Value
if len(domain) == 0 {
domain = "www.baidu.com"
}
return &header.Config{ID: 0, Domain: domain}, nil
case "dtls":
return &header.Config{ID: 1}, nil
case "srtp":
return &header.Config{ID: 2}, nil
case "utp":
return &header.Config{ID: 3}, nil
case "wechat":
return &header.Config{ID: 4}, nil
case "wireguard":
return &header.Config{ID: 5}, nil
default:
return nil, errors.New("invalid header ", c.Header)
}
}
type Salamander struct {
Password string `json:"password"`
PacketSize Int32Range `json:"packetSize"`
}
func (c *Salamander) Build() (proto.Message, error) {
if c.PacketSize.To > 0 {
if c.PacketSize.From <= 0 || c.PacketSize.To > 2048 {
return nil, errors.New("gecko: invalid min/max packet size")
}
return &salamander.GeckoConfig{
Password: c.Password,
MinPacketSize: c.PacketSize.From,
MaxPacketSize: c.PacketSize.To,
}, nil
}
return &salamander.Config{
Password: c.Password,
}, nil
}
type Sudoku struct {
Password string `json:"password"`
ASCII string `json:"ascii"`
CustomTable string `json:"customTable"`
LegacyCustomTable string `json:"custom_table"`
CustomTables []string `json:"customTables"`
LegacyCustomSets []string `json:"custom_tables"`
PaddingMin uint32 `json:"paddingMin"`
LegacyPaddingMin uint32 `json:"padding_min"`
PaddingMax uint32 `json:"paddingMax"`
LegacyPaddingMax uint32 `json:"padding_max"`
}
func (c *Sudoku) Build() (proto.Message, error) {
customTable := c.CustomTable
if customTable == "" {
customTable = c.LegacyCustomTable
}
customTables := c.CustomTables
if len(customTables) == 0 {
customTables = c.LegacyCustomSets
}
paddingMin := c.PaddingMin
if paddingMin == 0 {
paddingMin = c.LegacyPaddingMin
}
paddingMax := c.PaddingMax
if paddingMax == 0 {
paddingMax = c.LegacyPaddingMax
}
return &sudoku.Config{
Password: c.Password,
Ascii: c.ASCII,
CustomTable: customTable,
CustomTables: customTables,
PaddingMin: paddingMin,
PaddingMax: paddingMax,
}, nil
}
type Xdns struct {
Domain json.RawMessage `json:"domain"`
Domains []string `json:"domains"`
Resolvers []string `json:"resolvers"`
}
func (c *Xdns) Build() (proto.Message, error) {
if c.Domain != nil {
return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)")
}
if len(c.Domains) == 0 && len(c.Resolvers) == 0 {
return nil, errors.New("empty domains & empty resolvers")
}
for _, r := range c.Resolvers {
if !strings.Contains(r, "+udp://") {
return nil, errors.New("invalid resolver ", r)
}
}
return &xdns.Config{
Domains: c.Domains,
Resolvers: c.Resolvers,
}, nil
}
type XMC struct {
Hostname string `json:"hostname"`
Profiles []XMCProfile `json:"profiles"`
Password string `json:"password"`
}
type XMCProfile struct {
// Resolve the UUID by username, then request the session profile with
// unsigned=false. Client and server must use the same signed profile.
Username string `json:"username"`
UUID string `json:"uuid"`
TexturesValue string `json:"texturesValue"`
TexturesSignature string `json:"texturesSignature"`
}
var xmcUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9_]{3,16}$`)
func (c *XMCProfile) Build() (*xmc.Profile, error) {
if !xmcUsernamePattern.MatchString(c.Username) {
return nil, fmt.Errorf("invalid minecraft profile username: %q", c.Username)
}
profileUUID, err := googleuuid.Parse(c.UUID)
if err != nil {
return nil, fmt.Errorf("invalid minecraft profile UUID: %w", err)
}
if c.TexturesValue == "" || c.TexturesSignature == "" {
return nil, fmt.Errorf("incomplete minecraft profile textures")
}
return &xmc.Profile{
Username: c.Username,
Uuid: append([]byte(nil), profileUUID[:]...),
TexturesValue: c.TexturesValue,
TexturesSignature: c.TexturesSignature,
}, nil
}
func (c *XMC) Build() (proto.Message, error) {
if len(c.Profiles) == 0 {
return nil, fmt.Errorf("minecraft profiles are required")
}
if c.Password == "" {
return nil, fmt.Errorf("empty password")
}
rsaPrivateKey, err := xmc.DeriveRSAKey(c.Password)
if err != nil {
return nil, fmt.Errorf("derive minecraft rsa key: %w", err)
}
rsaPublicKey, err := x509.MarshalPKIXPublicKey(&rsaPrivateKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err)
}
profiles := make([]*xmc.Profile, 0, len(c.Profiles))
for i := range c.Profiles {
profile, err := c.Profiles[i].Build()
if err != nil {
return nil, fmt.Errorf("build minecraft profile %d: %w", i, err)
}
profiles = append(profiles, profile)
}
return &xmc.Config{
Password: c.Password,
Hostname: c.Hostname,
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
RsaPublicKey: rsaPublicKey,
Profiles: profiles,
}, nil
}
type Xicmp struct {
DGRAM bool `json:"dgram"`
IPs []string `json:"ips"`
}
func (c *Xicmp) Build() (proto.Message, error) {
for _, ip := range c.IPs {
if _, err := netip.ParseAddr(ip); err != nil {
return nil, err
}
}
config := &xicmp.Config{
DGRAM: c.DGRAM,
IPs: c.IPs,
}
return config, nil
}
type Realm struct {
Url string `json:"url"`
StunServers []string `json:"stunServers"`
TlsConfig *TLSConfig `json:"tlsConfig"`
IPMode string `json:"ipMode"`
PortMapping *realm.PortMapping `json:"portMapping"`
}
func (c *Realm) Build() (proto.Message, error) {
var scheme, host, port, token, id string
var stunServers []string
var tlsConfig *tls.Config
u, err := url.Parse(c.Url)
if err != nil {
return nil, err
}
switch u.Scheme {
case "realm":
scheme = "https"
case "realm+http":
scheme = "http"
default:
return nil, errors.New("invalid scheme", u.Scheme)
}
host = u.Hostname()
if host == "" {
return nil, errors.New("invalid host", host)
}
port = u.Port()
if port == "" {
port = "443"
if scheme == "http" {
port = "80"
}
}
token, err = url.PathUnescape(u.User.String())
if err != nil {
return nil, err
}
if token == "" {
return nil, errors.New("invalid token", token)
}
id, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/"))
if err != nil {
return nil, err
}
if id == "" {
return nil, errors.New("invalid id", id)
}
if len(c.StunServers) == 0 {
return nil, errors.New("empty stunServers")
}
for _, s := range c.StunServers {
_, _, err = net.SplitHostPort(s)
if err != nil {
return nil, err
}
}
stunServers = c.StunServers
if c.TlsConfig != nil {
tc, err := c.TlsConfig.Build()
if err != nil {
return nil, err
}
tlsConfig = tc.(*tls.Config)
}
return &realm.Config{
Scheme: scheme,
Host: host,
Port: port,
Token: token,
ID: id,
StunServers: stunServers,
TlsConfig: tlsConfig,
IPMode: strings.ToLower(c.IPMode),
PortMapping: c.PortMapping,
}, nil
}
type Mask struct {
Type string `json:"type"`
Settings *json.RawMessage `json:"settings"`
}
func (c *Mask) Build(tcp bool) (proto.Message, error) {
loader := udpmaskLoader
if tcp {
loader = tcpmaskLoader
}
settings := []byte("{}")
if c.Settings != nil {
settings = ([]byte)(*c.Settings)
}
rawConfig, err := loader.LoadWithID(settings, c.Type)
if err != nil {
return nil, err
}
ts, err := rawConfig.(Buildable).Build()
if err != nil {
return nil, err
}
return ts, nil
}
type QuicParamsConfig struct {
Congestion string `json:"congestion"`
Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"`
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
UdpHop UdpHop `json:"udpHop"`
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
DisableChromeParrot bool `json:"disableChromeParrot"`
DisableGSO bool `json:"disableGSO"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
DisableStatelessReset bool `json:"disableStatelessReset"`
}
type FinalMask struct {
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
QuicParams *QuicParamsConfig `json:"quicParams"`
}
@@ -0,0 +1,36 @@
package conf
import (
"strings"
"testing"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
)
func TestXMCBuildProfile(t *testing.T) {
built, err := (&XMC{
Password: "test-password",
Profiles: []XMCProfile{
{
Username: "TestUser",
UUID: "00112233-4455-6677-8899-aabbccddeeff",
TexturesValue: "textures-value",
TexturesSignature: "textures-signature",
},
},
}).Build()
if err != nil {
t.Fatalf("build XMC config: %v", err)
}
config := built.(*xmc.Config)
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
t.Fatalf("unexpected profiles: %+v", config.Profiles)
}
}
func TestXMCBuildRequiresProfile(t *testing.T) {
_, err := (&XMC{Password: "test-password"}).Build()
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
t.Fatalf("expected required profiles error, got %v", err)
}
}
File diff suppressed because it is too large Load Diff
+816
View File
@@ -0,0 +1,816 @@
package conf
import (
"context"
"encoding/json"
"math/big"
"net/url"
"sort"
"strconv"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/grpc"
"github.com/xtls/xray-core/transport/internet/headers/http"
"github.com/xtls/xray-core/transport/internet/headers/noop"
"github.com/xtls/xray-core/transport/internet/httpupgrade"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/kcp"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/websocket"
"google.golang.org/protobuf/proto"
)
type NoOpConnectionAuthenticator struct{}
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
return new(noop.ConnectionConfig), nil
}
type AuthenticatorRequest struct {
Version string `json:"version"`
Method string `json:"method"`
Path StringList `json:"path"`
Headers map[string]*StringList `json:"headers"`
}
func sortMapKeys(m map[string]*StringList) []string {
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
config := &http.RequestConfig{
Uri: []string{"/"},
Header: []*http.Header{
{
Name: "Host",
Value: []string{"www.baidu.com", "www.bing.com"},
},
{
Name: "User-Agent",
Value: []string{utils.ChromeUA},
},
{
Name: "Sec-CH-UA",
Value: []string{utils.ChromeUACH},
},
{
Name: "Sec-CH-UA-Mobile",
Value: []string{"?0"},
},
{
Name: "Sec-CH-UA-Platform",
Value: []string{"Windows"},
},
{
Name: "Sec-Fetch-Mode",
Value: []string{"no-cors", "cors", "same-origin"},
},
{
Name: "Sec-Fetch-Dest",
Value: []string{"empty"},
},
{
Name: "Sec-Fetch-Site",
Value: []string{"none"},
},
{
Name: "Sec-Fetch-User",
Value: []string{"?1"},
},
{
Name: "Accept-Encoding",
Value: []string{"gzip, deflate"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Method) > 0 {
config.Method = &http.Method{Value: v.Method}
}
if len(v.Path) > 0 {
config.Uri = append([]string(nil), v.Path...)
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type AuthenticatorResponse struct {
Version string `json:"version"`
Status string `json:"status"`
Reason string `json:"reason"`
Headers map[string]*StringList `json:"headers"`
}
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
config := &http.ResponseConfig{
Header: []*http.Header{
{
Name: "Content-Type",
Value: []string{"application/octet-stream", "video/mpeg"},
},
{
Name: "Transfer-Encoding",
Value: []string{"chunked"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
{
Name: "Cache-Control",
Value: []string{"private", "no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Status) > 0 || len(v.Reason) > 0 {
config.Status = &http.Status{
Code: "200",
Reason: "OK",
}
if len(v.Status) > 0 {
config.Status.Code = v.Status
}
if len(v.Reason) > 0 {
config.Status.Reason = v.Reason
}
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type Authenticator struct {
Request AuthenticatorRequest `json:"request"`
Response AuthenticatorResponse `json:"response"`
}
func (v *Authenticator) Build() (proto.Message, error) {
config := new(http.Config)
requestConfig, err := v.Request.Build()
if err != nil {
return nil, err
}
config.Request = requestConfig
responseConfig, err := v.Response.Build()
if err != nil {
return nil, err
}
config.Response = responseConfig
return config, nil
}
var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{
"none": func() interface{} { return new(NoOpConnectionAuthenticator) },
"http": func() interface{} { return new(Authenticator) },
}, "type", "")
type TCPConfig struct {
HeaderConfig json.RawMessage `json:"header"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
}
// Build implements Buildable.
func (c *TCPConfig) Build() (proto.Message, error) {
config := new(tcp.Config)
if len(c.HeaderConfig) > 0 {
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
}
ts, err := headerConfig.(Buildable).Build()
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
}
config.HeaderSettings = serial.ToTypedMessage(ts)
}
if c.AcceptProxyProtocol {
config.AcceptProxyProtocol = c.AcceptProxyProtocol
}
return config, nil
}
type SplitHTTPConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Mode string `json:"mode"`
Headers map[string]string `json:"headers"`
XPaddingBytes Int32Range `json:"xPaddingBytes"`
XPaddingObfsMode bool `json:"xPaddingObfsMode"`
XPaddingKey string `json:"xPaddingKey"`
XPaddingHeader string `json:"xPaddingHeader"`
XPaddingPlacement string `json:"xPaddingPlacement"`
XPaddingMethod string `json:"xPaddingMethod"`
UplinkHTTPMethod string `json:"uplinkHTTPMethod"`
SessionIDPlacement string `json:"sessionIDPlacement"`
SessionIDKey string `json:"sessionIDKey"`
SessionIDTable string `json:"sessionIDTable"`
SessionIDLength Int32Range `json:"sessionIDLength"`
SeqPlacement string `json:"seqPlacement"`
SeqKey string `json:"seqKey"`
UplinkDataPlacement string `json:"uplinkDataPlacement"`
UplinkDataKey string `json:"uplinkDataKey"`
UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
NoGRPCHeader bool `json:"noGRPCHeader"`
NoSSEHeader bool `json:"noSSEHeader"`
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"`
Extra json.RawMessage `json:"extra"`
}
type XmuxConfig struct {
MaxConcurrency Int32Range `json:"maxConcurrency"`
MaxConnections Int32Range `json:"maxConnections"`
CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"`
HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"`
HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"`
HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"`
}
func newRangeConfig(input Int32Range) *splithttp.RangeConfig {
return &splithttp.RangeConfig{
From: input.From,
To: input.To,
}
}
// Build implements Buildable.
func (c *SplitHTTPConfig) Build() (proto.Message, error) {
if c.Extra != nil {
var extra SplitHTTPConfig
if err := json.Unmarshal(c.Extra, &extra); err != nil {
return nil, errors.New(`Failed to unmarshal "extra".`).Base(err)
}
extra.Host = c.Host
extra.Path = c.Path
extra.Mode = c.Mode
c = &extra
}
switch c.Mode {
case "":
c.Mode = "auto"
case "auto", "packet-up", "stream-up", "stream-one":
default:
return nil, errors.New("unsupported mode: " + c.Mode)
}
// Priority (client): host > serverName > address
for k := range c.Headers {
if strings.ToLower(k) == "host" {
return nil, errors.New(`"headers" can't contain "host"`)
}
}
if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
return nil, errors.New("xPaddingBytes cannot be disabled")
}
if c.XPaddingKey == "" {
c.XPaddingKey = "x_padding"
}
if c.XPaddingHeader == "" {
c.XPaddingHeader = "X-Padding"
}
switch c.XPaddingPlacement {
case "":
c.XPaddingPlacement = "queryInHeader"
case "cookie", "header", "query", "queryInHeader":
default:
return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement)
}
switch c.XPaddingMethod {
case "":
c.XPaddingMethod = "repeat-x"
case "repeat-x", "tokenish":
default:
return nil, errors.New("unsupported padding method: " + c.XPaddingMethod)
}
switch c.UplinkDataPlacement {
case "":
c.UplinkDataPlacement = splithttp.PlacementAuto
case splithttp.PlacementAuto, splithttp.PlacementBody:
case splithttp.PlacementCookie, splithttp.PlacementHeader:
if c.Mode != "packet-up" {
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
}
default:
return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement)
}
if c.UplinkHTTPMethod == "" {
c.UplinkHTTPMethod = "POST"
}
c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod)
if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" {
return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode")
}
switch c.SessionIDPlacement {
case "":
c.SessionIDPlacement = "path"
case "path", "cookie", "header", "query":
default:
return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement)
}
switch c.SeqPlacement {
case "":
c.SeqPlacement = "path"
case "path", "cookie", "header", "query":
default:
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
}
if c.SessionIDPlacement != "path" && c.SessionIDKey == "" {
switch c.SessionIDPlacement {
case "cookie", "query":
c.SessionIDKey = "x_session"
case "header":
c.SessionIDKey = "X-Session"
}
}
if c.SessionIDTable != "" {
if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok {
c.SessionIDTable = predefined
}
room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To)
// 2.1B possiblities should be enough
if room.Cmp(big.NewInt(2<<30)) < 0 {
return nil, errors.New("sessionIDTable or sessionIDLength is too small")
}
if c.SessionIDLength.From <= 0 {
return nil, errors.New("sessionIDLength.from must be greater than 0")
}
for i := 0; i < len(c.SessionIDTable); i++ {
if c.SessionIDTable[i] >= 0x80 {
return nil, errors.New("sessionIDTable must contain only ASCII characters")
}
}
}
if c.SeqPlacement != "path" && c.SeqKey == "" {
switch c.SeqPlacement {
case "cookie", "query":
c.SeqKey = "x_seq"
case "header":
c.SeqKey = "X-Seq"
}
}
if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" {
switch c.UplinkDataPlacement {
case splithttp.PlacementCookie:
c.UplinkDataKey = "x_data"
case splithttp.PlacementAuto, splithttp.PlacementHeader:
c.UplinkDataKey = "X-Data"
}
}
if c.ServerMaxHeaderBytes < 0 {
return nil, errors.New("invalid negative value of maxHeaderBytes")
}
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 = 3
c.Xmux.MaxConnections.To = 3
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
}
config := &splithttp.Config{
Host: c.Host,
Path: c.Path,
Mode: c.Mode,
Headers: c.Headers,
XPaddingBytes: newRangeConfig(c.XPaddingBytes),
XPaddingObfsMode: c.XPaddingObfsMode,
XPaddingKey: c.XPaddingKey,
XPaddingHeader: c.XPaddingHeader,
XPaddingPlacement: c.XPaddingPlacement,
XPaddingMethod: c.XPaddingMethod,
UplinkHTTPMethod: c.UplinkHTTPMethod,
SessionIDPlacement: c.SessionIDPlacement,
SeqPlacement: c.SeqPlacement,
SessionIDKey: c.SessionIDKey,
SeqKey: c.SeqKey,
UplinkDataPlacement: c.UplinkDataPlacement,
UplinkDataKey: c.UplinkDataKey,
UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
NoGRPCHeader: c.NoGRPCHeader,
NoSSEHeader: c.NoSSEHeader,
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
SessionIDTable: c.SessionIDTable,
SessionIDLength: newRangeConfig(c.SessionIDLength),
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.DownloadSettings != nil {
if c.Mode == "stream-one" {
return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`)
}
var err error
if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil {
return nil, errors.New(`Failed to build "downloadSettings".`).Base(err)
}
}
return config, nil
}
func roomSize(tableSize int, min, max int32) *big.Int {
base := big.NewInt(int64(tableSize))
sum := new(big.Int)
term := new(big.Int)
for k := min; k <= max; k++ {
term.Exp(base, big.NewInt(int64(k)), nil)
sum.Add(sum, term)
}
return sum
}
type KCPConfig struct {
Mtu *uint32 `json:"mtu"`
Tti *uint32 `json:"tti"`
UpCap *uint32 `json:"uplinkCapacity"`
DownCap *uint32 `json:"downlinkCapacity"`
CwndMultiplier *uint32 `json:"cwndMultiplier"`
MaxSendingWindow *uint32 `json:"maxSendingWindow"`
HeaderConfig json.RawMessage `json:"header"`
Seed *string `json:"seed"`
}
// Build implements Buildable.
func (c *KCPConfig) Build() (proto.Message, error) {
if c.HeaderConfig != nil || c.Seed != nil {
return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm")
}
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
if c.Mtu != nil {
config.Mtu = *c.Mtu
}
if c.Tti != nil {
config.Tti = *c.Tti
}
if c.UpCap != nil {
config.UplinkCapacity = *c.UpCap
}
if c.DownCap != nil {
config.DownlinkCapacity = *c.DownCap
}
if c.CwndMultiplier != nil {
config.CwndMultiplier = *c.CwndMultiplier
}
if c.MaxSendingWindow != nil {
config.MaxSendingWindow = *c.MaxSendingWindow
}
if config.Mtu < 21 {
return nil, errors.New("Mtu must be at least 21").AtError()
}
if config.Tti < 10 || config.Tti > 1000 {
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
}
if config.CwndMultiplier < 1 {
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
}
if config.GetSendingBufferSize() == 0 {
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
}
return config, nil
}
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
func (g *GRPCConfig) Build() (proto.Message, error) {
if g.IdleTimeout <= 0 {
g.IdleTimeout = 0
}
if g.HealthCheckTimeout <= 0 {
g.HealthCheckTimeout = 0
}
if g.InitialWindowsSize < 0 {
// default window size of gRPC-go
g.InitialWindowsSize = 0
}
return &grpc.Config{
Authority: g.Authority,
ServiceName: g.ServiceName,
MultiMode: g.MultiMode,
IdleTimeout: g.IdleTimeout,
HealthCheckTimeout: g.HealthCheckTimeout,
PermitWithoutStream: g.PermitWithoutStream,
InitialWindowsSize: g.InitialWindowsSize,
UserAgent: g.UserAgent,
}, nil
}
type WebSocketConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
HeartbeatPeriod uint32 `json:"heartbeatPeriod"`
}
// Build implements Buildable.
func (c *WebSocketConfig) Build() (proto.Message, error) {
path := c.Path
var ed uint32
if u, err := url.Parse(path); err == nil {
if q := u.Query(); q.Get("ed") != "" {
Ed, _ := strconv.Atoi(q.Get("ed"))
ed = uint32(Ed)
q.Del("ed")
u.RawQuery = q.Encode()
path = u.String()
}
}
// Priority (client): host > serverName > address
for k, v := range c.Headers {
if strings.ToLower(k) == "host" {
errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`)
if c.Host == "" {
c.Host = v
}
delete(c.Headers, k)
}
}
config := &websocket.Config{
Path: path,
Host: c.Host,
Header: c.Headers,
AcceptProxyProtocol: c.AcceptProxyProtocol,
Ed: ed,
HeartbeatPeriod: c.HeartbeatPeriod,
}
return config, nil
}
type HttpUpgradeConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
}
// Build implements Buildable.
func (c *HttpUpgradeConfig) Build() (proto.Message, error) {
path := c.Path
var ed uint32
if u, err := url.Parse(path); err == nil {
if q := u.Query(); q.Get("ed") != "" {
Ed, _ := strconv.Atoi(q.Get("ed"))
ed = uint32(Ed)
q.Del("ed")
u.RawQuery = q.Encode()
path = u.String()
}
}
// Priority (client): host > serverName > address
for k := range c.Headers {
if strings.ToLower(k) == "host" {
return nil, errors.New(`"headers" can't contain "host"`)
}
}
config := &httpupgrade.Config{
Path: path,
Host: c.Host,
Header: c.Headers,
AcceptProxyProtocol: c.AcceptProxyProtocol,
Ed: ed,
}
return config, nil
}
const (
Byte = 1
Kilobyte = 1024 * Byte
Megabyte = 1024 * Kilobyte
Gigabyte = 1024 * Megabyte
Terabyte = 1024 * Gigabyte
)
type Bandwidth string
func (b Bandwidth) Bps() (uint64, error) {
s := strings.TrimSpace(strings.ToLower(string(b)))
if s == "" {
return 0, nil
}
idx := len(s)
for i, c := range s {
if (c < '0' || c > '9') && c != '.' {
idx = i
break
}
}
numStr := s[:idx]
unit := strings.TrimSpace(s[idx:])
val, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, err
}
mul := uint64(1)
switch unit {
case "", "b", "bps":
mul = Byte
case "k", "kb", "kbps":
mul = Kilobyte
case "m", "mb", "mbps":
mul = Megabyte
case "g", "gb", "gbps":
mul = Gigabyte
case "t", "tb", "tbps":
mul = Terabyte
default:
return 0, errors.New("unsupported unit: " + unit)
}
return uint64(val*float64(mul)) / 8, nil
}
type UdpHop struct {
PortList PortList `json:"ports"`
Interval Int32Range `json:"interval"`
}
type Masquerade struct {
Type string `json:"type"`
Dir string `json:"dir"`
Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"`
XForwarded bool `json:"xForwarded"`
Insecure bool `json:"insecure"`
Content string `json:"content"`
Headers map[string]string `json:"headers"`
StatusCode int32 `json:"statusCode"`
}
type HysteriaConfig struct {
Version int32 `json:"version"`
Auth string `json:"auth"`
Congestion *string `json:"congestion"`
Up *Bandwidth `json:"up"`
Down *Bandwidth `json:"down"`
UdpHop *UdpHop `json:"udphop"`
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
Masquerade Masquerade `json:"masquerade"`
}
func (c *HysteriaConfig) Build() (proto.Message, error) {
if c.Version != 2 {
return nil, errors.New("version != 2")
}
if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil {
errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams")
}
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
}
config := &hysteria.Config{}
config.Auth = c.Auth
config.UdpIdleTimeout = c.UdpIdleTimeout
config.MasqType = c.Masquerade.Type
config.MasqFile = c.Masquerade.Dir
config.MasqUrl = c.Masquerade.Url
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
config.MasqUrlXForwarded = c.Masquerade.XForwarded
config.MasqUrlInsecure = c.Masquerade.Insecure
config.MasqString = c.Masquerade.Content
config.MasqStringHeaders = c.Masquerade.Headers
config.MasqStringStatusCode = c.Masquerade.StatusCode
if config.UdpIdleTimeout == 0 {
config.UdpIdleTimeout = 60
}
return config, nil
}
func readFileOrString(f string, s []string) ([]byte, error) {
if len(f) > 0 {
return filesystem.ReadCert(f)
}
if len(s) > 0 {
return []byte(strings.Join(s, "\n")), nil
}
return nil, errors.New("both file and bytes are empty.")
}
+408
View File
@@ -0,0 +1,408 @@
package conf
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/url"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
type LimitFallback struct {
AfterBytes uint64
BytesPerSec uint64
BurstBytesPerSec uint64
}
type REALITYConfig struct {
MasterKeyLog string `json:"masterKeyLog"`
Show bool `json:"show"`
Target json.RawMessage `json:"target"`
Dest json.RawMessage `json:"dest"`
Type string `json:"type"`
Xver uint64 `json:"xver"`
ServerNames []string `json:"serverNames"`
PrivateKey string `json:"privateKey"`
MinClientVer string `json:"minClientVer"`
MaxClientVer string `json:"maxClientVer"`
MaxTimeDiff uint64 `json:"maxTimeDiff"`
ShortIds []string `json:"shortIds"`
Mldsa65Seed string `json:"mldsa65Seed"`
LimitFallbackUpload LimitFallback `json:"limitFallbackUpload"`
LimitFallbackDownload LimitFallback `json:"limitFallbackDownload"`
Fingerprint string `json:"fingerprint"`
ServerName string `json:"serverName"`
Password string `json:"password"`
PublicKey string `json:"publicKey"`
ShortId string `json:"shortId"`
Mldsa65Verify string `json:"mldsa65Verify"`
SpiderX string `json:"spiderX"`
}
func (c *REALITYConfig) Build() (proto.Message, error) {
config := new(reality.Config)
config.MasterKeyLog = c.MasterKeyLog
config.Show = c.Show
var err error
if c.Target != nil {
c.Dest = c.Target
}
if c.Dest != nil {
var i uint16
var s string
if err = json.Unmarshal(c.Dest, &i); err == nil {
s = strconv.Itoa(int(i))
} else {
_ = json.Unmarshal(c.Dest, &s)
}
if c.Type == "" && s != "" {
switch s[0] {
case '@', '/':
c.Type = "unix"
if s[0] == '@' && len(s) > 1 && s[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") {
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
copy(fullAddr, s[1:])
s = string(fullAddr)
}
default:
if _, err = strconv.Atoi(s); err == nil {
s = "localhost:" + s
}
if _, _, err = net.SplitHostPort(s); err == nil {
c.Type = "tcp"
}
}
}
if c.Type == "" {
return nil, errors.New(`please fill in a valid value for "target"`)
}
if c.Xver > 2 {
return nil, errors.New(`invalid PROXY protocol version, "xver" only accepts 0, 1, 2`)
}
if len(c.ServerNames) == 0 {
return nil, errors.New(`empty "serverNames"`)
}
if c.PrivateKey == "" {
return nil, errors.New(`empty "privateKey"`)
}
if config.PrivateKey, err = base64.RawURLEncoding.DecodeString(c.PrivateKey); err != nil || len(config.PrivateKey) != 32 {
return nil, errors.New(`invalid "privateKey": `, c.PrivateKey)
}
if c.MinClientVer != "" {
config.MinClientVer = make([]byte, 3)
var u uint64
for i, s := range strings.Split(c.MinClientVer, ".") {
if i == 3 {
return nil, errors.New(`invalid "minClientVer": `, c.MinClientVer)
}
if u, err = strconv.ParseUint(s, 10, 8); err != nil {
return nil, errors.New(`"minClientVer[`, i, `]" should be less than 256`)
} else {
config.MinClientVer[i] = byte(u)
}
}
// errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
} else {
// config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
// errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
}
if c.MaxClientVer != "" {
config.MaxClientVer = make([]byte, 3)
var u uint64
for i, s := range strings.Split(c.MaxClientVer, ".") {
if i == 3 {
return nil, errors.New(`invalid "maxClientVer": `, c.MaxClientVer)
}
if u, err = strconv.ParseUint(s, 10, 8); err != nil {
return nil, errors.New(`"maxClientVer[`, i, `]" should be less than 256`)
} else {
config.MaxClientVer[i] = byte(u)
}
}
}
if len(c.ShortIds) == 0 {
return nil, errors.New(`empty "shortIds"`)
}
config.ShortIds = make([][]byte, len(c.ShortIds))
for i, s := range c.ShortIds {
if len(s) > 16 {
return nil, errors.New(`too long "shortIds[`, i, `]": `, s)
}
config.ShortIds[i] = make([]byte, 8)
if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil {
return nil, errors.New(`invalid "shortIds[`, i, `]": `, s)
}
}
config.Dest = s
config.Type = c.Type
config.Xver = c.Xver
config.ServerNames = c.ServerNames
config.MaxTimeDiff = c.MaxTimeDiff
if c.Mldsa65Seed != "" {
if c.Mldsa65Seed == c.PrivateKey {
return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed)
}
if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 {
return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed)
}
}
for _, sn := range config.ServerNames {
sn = strings.ToLower(sn)
if strings.HasSuffix(sn, ".ru") || strings.HasSuffix(sn, ".ir") || strings.HasSuffix(sn, ".cn") ||
strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") || strings.Contains(sn, "microsoft") {
errors.LogWarning(context.Background(), `REALITY: Choosing "`, sn, `" as the target will increase the likelihood of your server's IP being blocked by the GFW`)
}
}
config.LimitFallbackUpload = new(reality.LimitFallback)
config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes
config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec
config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec
config.LimitFallbackDownload = new(reality.LimitFallback)
config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes
config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec
config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec
} else {
config.Fingerprint = strings.ToLower(c.Fingerprint)
if config.Fingerprint == "unsafe" || config.Fingerprint == "hellogolang" {
return nil, errors.New(`invalid "fingerprint": `, config.Fingerprint)
}
if tls.GetFingerprint(config.Fingerprint) == nil {
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint)
}
if len(c.ServerNames) != 0 {
return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`)
}
if c.Password != "" {
c.PublicKey = c.Password
}
if c.PublicKey == "" {
return nil, errors.New(`empty "password"`)
}
if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 {
return nil, errors.New(`invalid "password": `, c.PublicKey)
}
if len(c.ShortIds) != 0 {
return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`)
}
if len(c.ShortId) > 16 {
return nil, errors.New(`too long "shortId": `, c.ShortId)
}
config.ShortId = make([]byte, 8)
if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil {
return nil, errors.New(`invalid "shortId": `, c.ShortId)
}
if c.Mldsa65Verify != "" {
if config.Mldsa65Verify, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Verify); err != nil || len(config.Mldsa65Verify) != 1952 {
return nil, errors.New(`invalid "mldsa65Verify": `, c.Mldsa65Verify)
}
}
if c.SpiderX == "" {
c.SpiderX = "/"
}
if c.SpiderX[0] != '/' {
return nil, errors.New(`invalid "spiderX": `, c.SpiderX)
}
config.SpiderY = make([]int64, 10)
u, _ := url.Parse(c.SpiderX)
q := u.Query()
parse := func(param string, index int) {
if q.Get(param) != "" {
s := strings.Split(q.Get(param), "-")
if len(s) == 1 {
config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64)
config.SpiderY[index+1], _ = strconv.ParseInt(s[0], 10, 64)
} else {
config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64)
config.SpiderY[index+1], _ = strconv.ParseInt(s[1], 10, 64)
}
}
q.Del(param)
}
parse("p", 0) // padding
parse("c", 2) // concurrency
parse("t", 4) // times
parse("i", 6) // interval
parse("r", 8) // return
u.RawQuery = q.Encode()
config.SpiderX = u.String()
config.ServerName = c.ServerName
}
return config, nil
}
type TLSCertConfig struct {
CertFile string `json:"certificateFile"`
CertStr []string `json:"certificate"`
KeyFile string `json:"keyFile"`
KeyStr []string `json:"key"`
Usage string `json:"usage"`
OcspStapling uint64 `json:"ocspStapling"`
OneTimeLoading bool `json:"oneTimeLoading"`
BuildChain bool `json:"buildChain"`
}
// Build implements Buildable.
func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
certificate := new(tls.Certificate)
cert, err := readFileOrString(c.CertFile, c.CertStr)
if err != nil {
return nil, errors.New("failed to parse certificate").Base(err)
}
certificate.Certificate = cert
certificate.CertificatePath = c.CertFile
if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 {
key, err := readFileOrString(c.KeyFile, c.KeyStr)
if err != nil {
return nil, errors.New("failed to parse key").Base(err)
}
certificate.Key = key
certificate.KeyPath = c.KeyFile
}
switch strings.ToLower(c.Usage) {
case "encipherment":
certificate.Usage = tls.Certificate_ENCIPHERMENT
case "verify":
certificate.Usage = tls.Certificate_AUTHORITY_VERIFY
case "issue":
certificate.Usage = tls.Certificate_AUTHORITY_ISSUE
default:
certificate.Usage = tls.Certificate_ENCIPHERMENT
}
if certificate.KeyPath == "" && certificate.CertificatePath == "" {
certificate.OneTimeLoading = true
} else {
certificate.OneTimeLoading = c.OneTimeLoading
}
certificate.OcspStapling = c.OcspStapling
certificate.BuildChain = c.BuildChain
return certificate, nil
}
type TLSConfig struct {
AllowInsecure bool `json:"allowInsecure"`
Certs []*TLSCertConfig `json:"certificates"`
ServerName string `json:"serverName"`
ALPN *StringList `json:"alpn"`
EnableSessionResumption bool `json:"enableSessionResumption"`
DisableSystemRoot bool `json:"disableSystemRoot"`
MinVersion string `json:"minVersion"`
MaxVersion string `json:"maxVersion"`
CipherSuites string `json:"cipherSuites"`
Fingerprint string `json:"fingerprint"`
RejectUnknownSNI bool `json:"rejectUnknownSni"`
CurvePreferences *StringList `json:"curvePreferences"`
MasterKeyLog string `json:"masterKeyLog"`
PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"`
VerifyPeerCertByName string `json:"verifyPeerCertByName"`
ECHServerKeys string `json:"echServerKeys"`
ECHConfigList string `json:"echConfigList"`
ECHSocketSettings *SocketConfig `json:"echSockopt"`
}
// Build implements Buildable.
func (c *TLSConfig) Build() (proto.Message, error) {
config := new(tls.Config)
config.Certificate = make([]*tls.Certificate, len(c.Certs))
for idx, certConf := range c.Certs {
cert, err := certConf.Build()
if err != nil {
return nil, err
}
config.Certificate[idx] = cert
}
serverName := c.ServerName
if len(c.ServerName) > 0 {
config.ServerName = serverName
}
if c.ALPN != nil && len(*c.ALPN) > 0 {
config.NextProtocol = []string(*c.ALPN)
}
if len(config.NextProtocol) > 1 {
for _, p := range config.NextProtocol {
if tls.IsFromMitm(p) {
return nil, errors.New(`only one element is allowed in "alpn" when using "fromMitm" in it`)
}
}
}
if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 {
config.CurvePreferences = []string(*c.CurvePreferences)
}
config.EnableSessionResumption = c.EnableSessionResumption
config.DisableSystemRoot = c.DisableSystemRoot
config.MinVersion = c.MinVersion
config.MaxVersion = c.MaxVersion
config.CipherSuites = c.CipherSuites
config.Fingerprint = strings.ToLower(c.Fingerprint)
if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil {
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint)
}
config.RejectUnknownSni = c.RejectUnknownSNI
config.MasterKeyLog = c.MasterKeyLog
if c.AllowInsecure {
return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`)
}
if c.PinnedPeerCertSha256 != "" {
for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") {
v = strings.TrimSpace(v)
if v == "" {
continue
}
// remove colons for OpenSSL format
hashValue, err := hex.DecodeString(strings.ReplaceAll(v, ":", ""))
if err != nil {
return nil, err
}
if len(hashValue) != 32 {
return nil, errors.New("incorrect pinnedPeerCertSha256 length: ", v)
}
config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue)
}
}
if c.VerifyPeerCertByName != "" {
for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") {
v = strings.TrimSpace(v)
if v == "" {
continue
}
config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v)
}
}
if c.ECHServerKeys != "" {
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys)
if err != nil {
return nil, errors.New("invalid ECH Config", c.ECHServerKeys)
}
config.EchServerKeys = EchPrivateKey
}
config.EchConfigList = c.ECHConfigList
if c.ECHSocketSettings != nil {
ss, err := c.ECHSocketSettings.Build()
if err != nil {
return nil, errors.New("Failed to build ech sockopt.").Base(err)
}
config.EchSocketSettings = ss
}
return config, nil
}
+187
View File
@@ -0,0 +1,187 @@
package conf
import (
"encoding/json"
"math"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet"
)
type CustomSockoptConfig struct {
System string `json:"system"`
Network string `json:"network"`
Level string `json:"level"`
Opt string `json:"opt"`
Value string `json:"value"`
Type string `json:"type"`
}
type HappyEyeballsConfig struct {
PrioritizeIPv6 bool `json:"prioritizeIPv6"`
TryDelayMs uint64 `json:"tryDelayMs"`
Interleave uint32 `json:"interleave"`
MaxConcurrentTry uint32 `json:"maxConcurrentTry"`
}
func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error {
innerHappyEyeballsConfig := struct {
PrioritizeIPv6 bool `json:"prioritizeIPv6"`
TryDelayMs uint64 `json:"tryDelayMs"`
Interleave uint32 `json:"interleave"`
MaxConcurrentTry uint32 `json:"maxConcurrentTry"`
}{PrioritizeIPv6: false, Interleave: 1, TryDelayMs: 0, MaxConcurrentTry: 4}
if err := json.Unmarshal(data, &innerHappyEyeballsConfig); err != nil {
return err
}
h.PrioritizeIPv6 = innerHappyEyeballsConfig.PrioritizeIPv6
h.TryDelayMs = innerHappyEyeballsConfig.TryDelayMs
h.Interleave = innerHappyEyeballsConfig.Interleave
h.MaxConcurrentTry = innerHappyEyeballsConfig.MaxConcurrentTry
return nil
}
type SocketConfig struct {
Mark int32 `json:"mark"`
TFO interface{} `json:"tcpFastOpen"`
TProxy string `json:"tproxy"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
DomainStrategy string `json:"domainStrategy"`
DialerProxy string `json:"dialerProxy"`
TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"`
TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"`
TCPCongestion string `json:"tcpCongestion"`
TCPWindowClamp int32 `json:"tcpWindowClamp"`
TCPMaxSeg int32 `json:"tcpMaxSeg"`
Penetrate bool `json:"penetrate"`
TCPUserTimeout int32 `json:"tcpUserTimeout"`
V6only bool `json:"v6only"`
Interface string `json:"interface"`
TcpMptcp bool `json:"tcpMptcp"`
CustomSockopt []*CustomSockoptConfig `json:"customSockopt"`
AddressPortStrategy string `json:"addressPortStrategy"`
HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"`
TrustedXForwardedFor []string `json:"trustedXForwardedFor"`
}
// Build implements Buildable.
func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
tfo := int32(0) // don't invoke setsockopt() for TFO
if c.TFO != nil {
switch v := c.TFO.(type) {
case bool:
if v {
tfo = 256
} else {
tfo = -1 // TFO need to be disabled
}
case float64:
tfo = int32(math.Min(v, math.MaxInt32))
default:
return nil, errors.New("tcpFastOpen: only boolean and integer value is acceptable")
}
}
var tproxy internet.SocketConfig_TProxyMode
switch strings.ToLower(c.TProxy) {
case "tproxy":
tproxy = internet.SocketConfig_TProxy
case "redirect":
tproxy = internet.SocketConfig_Redirect
default:
tproxy = internet.SocketConfig_Off
}
dStrategy := internet.DomainStrategy_AS_IS
switch strings.ToLower(c.DomainStrategy) {
case "asis", "":
dStrategy = internet.DomainStrategy_AS_IS
case "useip":
dStrategy = internet.DomainStrategy_USE_IP
case "useipv4":
dStrategy = internet.DomainStrategy_USE_IP4
case "useipv6":
dStrategy = internet.DomainStrategy_USE_IP6
case "useipv4v6":
dStrategy = internet.DomainStrategy_USE_IP46
case "useipv6v4":
dStrategy = internet.DomainStrategy_USE_IP64
case "forceip":
dStrategy = internet.DomainStrategy_FORCE_IP
case "forceipv4":
dStrategy = internet.DomainStrategy_FORCE_IP4
case "forceipv6":
dStrategy = internet.DomainStrategy_FORCE_IP6
case "forceipv4v6":
dStrategy = internet.DomainStrategy_FORCE_IP46
case "forceipv6v4":
dStrategy = internet.DomainStrategy_FORCE_IP64
default:
return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy)
}
var customSockopts []*internet.CustomSockopt
for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{
System: copt.System,
Network: copt.Network,
Level: copt.Level,
Opt: copt.Opt,
Value: copt.Value,
Type: copt.Type,
}
customSockopts = append(customSockopts, customSockopt)
}
addressPortStrategy := internet.AddressPortStrategy_None
switch strings.ToLower(c.AddressPortStrategy) {
case "none", "":
addressPortStrategy = internet.AddressPortStrategy_None
case "srvportonly":
addressPortStrategy = internet.AddressPortStrategy_SrvPortOnly
case "srvaddressonly":
addressPortStrategy = internet.AddressPortStrategy_SrvAddressOnly
case "srvportandaddress":
addressPortStrategy = internet.AddressPortStrategy_SrvPortAndAddress
case "txtportonly":
addressPortStrategy = internet.AddressPortStrategy_TxtPortOnly
case "txtaddressonly":
addressPortStrategy = internet.AddressPortStrategy_TxtAddressOnly
case "txtportandaddress":
addressPortStrategy = internet.AddressPortStrategy_TxtPortAndAddress
default:
return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy)
}
happyEyeballs := &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4}
if c.HappyEyeballsSettings != nil {
happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6
happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave
happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs
happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry
}
return &internet.SocketConfig{
Mark: c.Mark,
Tfo: tfo,
Tproxy: tproxy,
DomainStrategy: dStrategy,
AcceptProxyProtocol: c.AcceptProxyProtocol,
DialerProxy: c.DialerProxy,
TcpKeepAliveInterval: c.TCPKeepAliveInterval,
TcpKeepAliveIdle: c.TCPKeepAliveIdle,
TcpCongestion: c.TCPCongestion,
TcpWindowClamp: c.TCPWindowClamp,
TcpMaxSeg: c.TCPMaxSeg,
Penetrate: c.Penetrate,
TcpUserTimeout: c.TCPUserTimeout,
V6Only: c.V6only,
Interface: c.Interface,
TcpMptcp: c.TcpMptcp,
CustomSockopt: customSockopts,
AddressPortStrategy: addressPortStrategy,
HappyEyeballs: happyEyeballs,
TrustedXForwardedFor: c.TrustedXForwardedFor,
}, nil
}
+70 -1
View File
@@ -1,12 +1,19 @@
package conf package conf
import ( import (
"crypto/rand"
"fmt"
"math/big"
"net"
"strconv"
"github.com/xtls/xray-core/proxy/tun" "github.com/xtls/xray-core/proxy/tun"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
type TunConfig struct { type TunConfig struct {
Name string `json:"name"` Name string `json:"name"`
Desc string `json:"desc"`
MTU uint32 `json:"mtu"` MTU uint32 `json:"mtu"`
Gateway []string `json:"gateway"` Gateway []string `json:"gateway"`
DNS []string `json:"dns"` DNS []string `json:"dns"`
@@ -18,6 +25,7 @@ type TunConfig struct {
func (v *TunConfig) Build() (proto.Message, error) { func (v *TunConfig) Build() (proto.Message, error) {
config := &tun.Config{ config := &tun.Config{
Name: v.Name, Name: v.Name,
Desc: v.Desc,
MTU: v.MTU, MTU: v.MTU,
Gateway: v.Gateway, Gateway: v.Gateway,
DNS: v.DNS, DNS: v.DNS,
@@ -32,10 +40,71 @@ func (v *TunConfig) Build() (proto.Message, error) {
} }
if config.Name == "" { if config.Name == "" {
config.Name = "xray0" name, err := GetAvailableTunName()
if err != nil {
return nil, err
}
config.Name = name
}
if config.Desc == "" {
config.Desc = "Wintun"
} }
if config.MTU == 0 { if config.MTU == 0 {
config.MTU = 1500 config.MTU = 1500
} }
return config, nil return config, nil
} }
const (
tunNamePrefix = "utun"
minTunIndex = 10
maxTunIndex = 1024
)
func GetAvailableTunName() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("fail to get system interface information: %w", err)
}
usedNames := make(map[string]struct{}, len(interfaces))
for _, iface := range interfaces {
usedNames[iface.Name] = struct{}{}
}
startIndex, err := randomInt(minTunIndex, maxTunIndex)
if err != nil {
return "", fmt.Errorf("fail to generate valid tun name: %w", err)
}
rangeSize := maxTunIndex - minTunIndex + 1
for offset := 0; offset < rangeSize; offset++ {
index := minTunIndex + (startIndex-minTunIndex+offset)%rangeSize
name := tunNamePrefix + strconv.Itoa(index)
if _, exists := usedNames[name]; !exists {
return name, nil
}
}
return "", fmt.Errorf(
"no available TUN interface name in range %s%d-%s%d",
tunNamePrefix,
minTunIndex,
tunNamePrefix,
maxTunIndex,
)
}
func randomInt(min, max int) (int, error) {
value, err := rand.Int(
rand.Reader,
big.NewInt(int64(max-min+1)),
)
if err != nil {
return 0, err
}
return min + int(value.Int64()), nil
}
-4
View File
@@ -31,10 +31,6 @@ func (a *VMessAccount) Build() *vmess.Account {
st = protocol.SecurityType_CHACHA20_POLY1305 st = protocol.SecurityType_CHACHA20_POLY1305
case "auto": case "auto":
st = protocol.SecurityType_AUTO st = protocol.SecurityType_AUTO
case "none":
st = protocol.SecurityType_NONE
case "zero":
st = protocol.SecurityType_ZERO
default: default:
st = protocol.SecurityType_AUTO st = protocol.SecurityType_AUTO
} }
+2
View File
@@ -66,6 +66,7 @@ type WireGuardConfig struct {
MTU int32 `json:"mtu"` MTU int32 `json:"mtu"`
Reserved []byte `json:"reserved"` Reserved []byte `json:"reserved"`
DomainStrategy string `json:"domainStrategy"` DomainStrategy string `json:"domainStrategy"`
DNS []string `json:"remoteDNS"`
} }
func (c *WireGuardConfig) Build() (proto.Message, error) { func (c *WireGuardConfig) Build() (proto.Message, error) {
@@ -141,6 +142,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
config.IsClient = c.IsClient config.IsClient = c.IsClient
config.NoKernelTun = c.NoKernelTun config.NoKernelTun = c.NoKernelTun
config.DNS = c.DNS
return config, nil return config, nil
} }
+86 -30
View File
@@ -3,6 +3,7 @@ package conf
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -14,6 +15,7 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core" core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
) )
@@ -139,7 +141,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
// TUN inbound doesn't need port configuration as it uses network interface instead // TUN inbound doesn't need port configuration as it uses network interface instead
if strings.ToLower(c.Protocol) == "tun" { if strings.ToLower(c.Protocol) == "tun" {
// Skip port validation for TUN // Skip port validation for TUN
} else if c.ListenOn == nil { } else if c.ListenOn == nil || len(c.ListenOn.String()) == 0 {
// Listen on anyip, must set PortList // Listen on anyip, must set PortList
if c.PortList == nil { if c.PortList == nil {
return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.") return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.")
@@ -175,7 +177,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
receiverSettings.StreamSettings = ss receiverSettings.StreamSettings = ss
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil || if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) { len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`) errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
} }
} }
if c.SniffingConfig != nil { if c.SniffingConfig != nil {
@@ -215,23 +217,51 @@ type OutboundDetourConfig struct {
Tag string `json:"tag"` Tag string `json:"tag"`
Settings *json.RawMessage `json:"settings"` Settings *json.RawMessage `json:"settings"`
StreamSetting *StreamConfig `json:"streamSettings"` StreamSetting *StreamConfig `json:"streamSettings"`
ProxySettings *ProxyConfig `json:"proxySettings"` ProxySettings *json.RawMessage `json:"proxySettings"`
MuxSettings *MuxConfig `json:"mux"` MuxSettings *MuxConfig `json:"mux"`
TargetStrategy string `json:"targetStrategy"` TargetStrategy string `json:"targetStrategy"`
} }
func (c *OutboundDetourConfig) checkChainProxyConfig() error { func requiresTransportSecurity(address *Address) bool {
if c.StreamSetting == nil || c.ProxySettings == nil || c.StreamSetting.SocketSettings == nil { if address == nil || address.Address == nil {
return false
}
if address.Family().IsIP() {
return !geodata.GetPrivateIPMatcher().Match(address.IP())
}
domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".")
return !geodata.GetPrivateDomainMatcher().MatchAny(domain)
}
func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *proxyman.SenderConfig) error {
if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.GetSecurityType() != "" {
return nil return nil
} }
if len(c.ProxySettings.Tag) > 0 && len(c.StreamSetting.SocketSettings.DialerProxy) > 0 {
return errors.New("proxySettings.tag is conflicted with sockopt.dialerProxy").AtWarning() if vlessCfg, ok := rawConfig.(*VLessOutboundConfig); ok {
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
return nil
}
if requiresTransportSecurity(vlessCfg.Vnext[0].Address) {
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
}
} }
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
if requiresTransportSecurity(tjCfg.Servers[0].Address) {
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
}
}
return nil return nil
} }
// Build implements Buildable. // Build implements Buildable.
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.ProxySettings != nil {
return nil, errors.PrintRemovedFeatureError(`outbound "proxySettings"`, `"streamSettings.sockopt.dialerProxy"`)
}
senderSettings := &proxyman.SenderConfig{} senderSettings := &proxyman.SenderConfig{}
switch strings.ToLower(c.TargetStrategy) { switch strings.ToLower(c.TargetStrategy) {
case "asis", "": case "asis", "":
@@ -259,9 +289,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
default: default:
return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy) return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
} }
if err := c.checkChainProxyConfig(); err != nil {
return nil, err
}
if c.SendThrough != nil { if c.SendThrough != nil {
address := ParseSendThough(c.SendThrough) address := ParseSendThough(c.SendThrough)
@@ -287,26 +314,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
senderSettings.StreamSettings = ss senderSettings.StreamSettings = ss
} }
if c.ProxySettings != nil {
ps, err := c.ProxySettings.Build()
if err != nil {
return nil, errors.New("invalid outbound detour proxy settings").Base(err)
}
if ps.TransportLayerProxy {
if senderSettings.StreamSettings != nil {
if senderSettings.StreamSettings.SocketSettings != nil {
senderSettings.StreamSettings.SocketSettings.DialerProxy = ps.Tag
} else {
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{DialerProxy: ps.Tag}
}
} else {
senderSettings.StreamSettings = &internet.StreamConfig{SocketSettings: &internet.SocketConfig{DialerProxy: ps.Tag}}
}
ps = nil
}
senderSettings.ProxySettings = ps
}
if c.MuxSettings != nil { if c.MuxSettings != nil {
ms, err := c.MuxSettings.Build() ms, err := c.MuxSettings.Build()
if err != nil { if err != nil {
@@ -327,6 +334,34 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if err != nil { if err != nil {
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err) return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
} }
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
if fc, ok := ts.(*freedom.Config); ok {
if senderSettings.StreamSettings != nil &&
senderSettings.StreamSettings.SocketSettings != nil &&
senderSettings.StreamSettings.SocketSettings.AddressPortStrategy != internet.AddressPortStrategy_None {
return nil, errors.New(`freedom outbound does not support "sockopt.addressPortStrategy"`)
}
var strategy internet.DomainStrategy
if strategy = senderSettings.TargetStrategy; strategy != internet.DomainStrategy_AS_IS {
errors.LogWarning(context.Background(), `The "outbound.targetStrategy" setting is not supported directly by freedom and has been automatically migrated to "sockopt.domainStrategy" with no behavior change.`)
senderSettings.TargetStrategy = internet.DomainStrategy_AS_IS
} else if strategy = fc.DomainStrategy; strategy != internet.DomainStrategy_AS_IS {
errors.LogWarning(context.Background(), `The "freedom.domainStrategy" setting is deprecated and will be removed. For compatibility, its value has been automatically migrated to "sockopt.domainStrategy". Please update your config before removal.`)
}
if strategy != internet.DomainStrategy_AS_IS {
if senderSettings.StreamSettings == nil {
senderSettings.StreamSettings = &internet.StreamConfig{}
}
if senderSettings.StreamSettings.SocketSettings == nil {
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{}
}
senderSettings.StreamSettings.SocketSettings.DomainStrategy = strategy
}
}
return &core.OutboundHandlerConfig{ return &core.OutboundHandlerConfig{
SenderSettings: serial.ToTypedMessage(senderSettings), SenderSettings: serial.ToTypedMessage(senderSettings),
@@ -342,11 +377,20 @@ func (c *StatsConfig) Build() (*stats.Config, error) {
return &stats.Config{}, nil return &stats.Config{}, nil
} }
type EnvConfig map[string]string
func (c EnvConfig) Override(o EnvConfig) {
for key, value := range o {
c[key] = value
}
}
type Config struct { type Config struct {
// Deprecated: Global transport config is no longer used // Deprecated: Global transport config is no longer used
// left for returning error // left for returning error
Transport map[string]json.RawMessage `json:"transport"` Transport map[string]json.RawMessage `json:"transport"`
Env EnvConfig `json:"env"`
LogConfig *LogConfig `json:"log"` LogConfig *LogConfig `json:"log"`
RouterConfig *RouterConfig `json:"routing"` RouterConfig *RouterConfig `json:"routing"`
DNSConfig *DNSConfig `json:"dns"` DNSConfig *DNSConfig `json:"dns"`
@@ -402,6 +446,12 @@ func (c *Config) Override(o *Config, fn string) {
if o.Transport != nil { if o.Transport != nil {
c.Transport = o.Transport c.Transport = o.Transport
} }
if o.Env != nil {
if c.Env == nil {
c.Env = EnvConfig{}
}
c.Env.Override(o.Env)
}
if o.Policy != nil { if o.Policy != nil {
c.Policy = o.Policy c.Policy = o.Policy
} }
@@ -477,6 +527,12 @@ func (c *Config) Override(o *Config, fn string) {
// Build implements Buildable. // Build implements Buildable.
func (c *Config) Build() (*core.Config, error) { func (c *Config) Build() (*core.Config, error) {
for key, value := range c.Env {
if err := os.Setenv(key, value); err != nil {
return nil, errors.New("failed to apply environment configuration").Base(err)
}
}
if err := PostProcessConfigureFile(c); err != nil { if err := PostProcessConfigureFile(c); err != nil {
return nil, errors.New("failed to post-process configuration file").Base(err) return nil, errors.New("failed to post-process configuration file").Base(err)
} }
+273 -139
View File
@@ -1,15 +1,18 @@
package main package main
import ( import (
"errors" "bytes"
"flag" "flag"
"fmt" "fmt"
"go/build"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort"
"strings" "strings"
"sync"
"sync/atomic"
"mvdan.cc/gofumpt/format"
) )
var ( var (
@@ -23,101 +26,27 @@ var (
isFormat bool isFormat bool
) )
// envFile returns the name of the Go environment configuration file. func getModuleInfo(pwd string) (modPath, langVersion string, err error) {
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 data, err := os.ReadFile(filepath.Join(pwd, "go.mod"))
func envFile() (string, error) {
if file := os.Getenv("GOENV"); file != "" {
if file == "off" {
return "", errors.New("GOENV=off")
}
return file, nil
}
dir, err := os.UserConfigDir()
if err != nil { if err != nil {
return "", err return "", "", err
} }
if dir == "" { for _, line := range strings.Split(string(data), "\n") {
return "", errors.New("missing user-config dir") fields := strings.Fields(line)
} if len(fields) >= 2 {
return filepath.Join(dir, "go", "env"), nil switch fields[0] {
} case "module":
modPath = fields[1]
// GetRuntimeEnv returns the value of runtime environment variable, case "go":
// that is set by running following command: `go env -w key=value`. langVersion = "go" + strings.TrimPrefix(fields[1], "go")
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 len(envKeyValue) == 2 && strings.TrimSpace(envKeyValue[0]) == key {
runtimeEnv = strings.TrimSpace(envKeyValue[1])
}
}
return runtimeEnv, nil
}
// 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 Run(binary string, args []string) ([]byte, error) {
cmd := exec.Command(binary, args...)
cmd.Env = append(cmd.Env, os.Environ()...)
output, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return nil, cmdErr
}
return output, nil
}
func RunMany(binary string, args, files []string) bool {
fmt.Println("Processing with", binary, args, "...")
formatRequired := false
maxTasks := make(chan struct{}, runtime.NumCPU())
for _, file := range files {
maxTasks <- struct{}{}
go func(file string) {
output, err := Run(binary, append(args, file))
if err != nil {
fmt.Println(err)
} else if len(output) > 0 {
fmt.Println(string(output))
formatRequired = true
} }
<-maxTasks }
}(file)
} }
return formatRequired return modPath, langVersion, nil
}
func formatGoSource(src []byte, opts format.Options) ([]byte, error) {
return format.Source(src, opts)
} }
func main() { func main() {
@@ -150,26 +79,76 @@ func main() {
} }
pwd := *directory pwd := *directory
GOBIN := GetGOBIN() modPath, langVersion, modErr := getModuleInfo(pwd)
binPath := os.Getenv("PATH") if modErr != nil {
pathSlice := []string{pwd, GOBIN, binPath} fmt.Println("Error reading go.mod:", modErr)
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
os.Setenv("PATH", binPath)
suffix := ""
if runtime.GOOS == "windows" {
suffix = ".exe"
}
gofmt := "gofumpt" + suffix
if gofmtPath, err := exec.LookPath(gofmt); err != nil {
fmt.Println("Can not find", gofmt, "in system path or current working directory.")
os.Exit(1) os.Exit(1)
} else { }
gofmt = gofmtPath opts := format.Options{
LangVersion: langVersion,
ModulePath: modPath,
}
if isFormat {
fmt.Println("Formatting Go source files...")
} else if isCheck {
fmt.Println("Checking files thar are not properly formatted...")
}
jobs := make(chan string, runtime.NumCPU())
var wg sync.WaitGroup
var formatRequired atomic.Bool
var hasErrors atomic.Bool
for i := 0; i < runtime.NumCPU(); i++ {
wg.Go(func() {
for path := range jobs {
src, err := os.ReadFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err)
hasErrors.Store(true)
continue
}
formatted, err := formatGoSource(src, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Error formatting %s: %v\n", path, err)
hasErrors.Store(true)
continue
}
if !bytes.Equal(src, formatted) {
var diffText []byte
if isDryrun {
newName := filepath.ToSlash(path)
oldName := newName + ".orig"
diffText = diff(oldName, src, newName, formatted)
}
if isFormat {
info, statErr := os.Stat(path)
if statErr != nil {
fmt.Fprintf(os.Stderr, "Error stating %s: %v\n", path, statErr)
hasErrors.Store(true)
continue
}
if writeErr := os.WriteFile(path, formatted, info.Mode().Perm()); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", path, writeErr)
hasErrors.Store(true)
continue
}
}
formatRequired.Store(true)
if isDryrun && len(diffText) > 0 {
fmt.Printf("%s\n%s", path, diffText)
} else {
fmt.Println(path)
}
}
}
})
} }
rawFilesSlice := make([]string, 0, 1000)
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -186,51 +165,206 @@ func main() {
!strings.HasSuffix(filename, ".pb.go") && !strings.HasSuffix(filename, ".pb.go") &&
!strings.Contains(dir, filepath.Join("testing", "mocks")) && !strings.Contains(dir, filepath.Join("testing", "mocks")) &&
!strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) { !strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) {
rawFilesSlice = append(rawFilesSlice, path) jobs <- path
} }
return nil return nil
}) })
close(jobs)
wg.Wait()
if walkErr != nil { if walkErr != nil {
fmt.Println(walkErr) fmt.Println(walkErr)
os.Exit(1) os.Exit(1)
} }
if isFormat { if hasErrors.Load() {
gofmtArgs := []string{ os.Exit(1)
"-l", "-e", "-w", }
}
fmt.Println("Formatting Go source files...") if isFormat {
RunMany(gofmt, gofmtArgs, rawFilesSlice) if formatRequired.Load() {
fmt.Println("Do NOT forget to commit file changes.") fmt.Println("Do NOT forget to commit file changes.")
}
} }
if isCheck { if isCheck {
gofmtListArgs := []string{ if formatRequired.Load() {
"-l", "-e",
}
fmt.Println("Checking files thar are not properly formatted...")
formatRequired := RunMany(gofmt, gofmtListArgs, rawFilesSlice)
if formatRequired {
fmt.Println("Format problem(s) found.") fmt.Println("Format problem(s) found.")
} fmt.Println("Please run 'go run ./infra/vformat/main.go' to format the Go source files.")
if isDryrun {
if formatRequired {
gofmtShowArgs := []string{
"-d", "-e",
}
RunMany(gofmt, gofmtShowArgs, rawFilesSlice)
}
}
if formatRequired {
fmt.Println("Please run 'go install -v mvdan.cc/gofumpt@latest', then run 'go run ./infra/vformat/main.go' to format the Go source files.")
os.Exit(1) os.Exit(1)
} else { } else {
fmt.Println("All Go source file format check has been passed.") fmt.Println("All Go source file format check has been passed.")
} }
} }
} }
// diff algorithm copied from mvdan.cc/gofumpt/internal/govendor/diff
type pair struct{ x, y int }
func diff(oldName string, old []byte, newName string, new []byte) []byte {
if bytes.Equal(old, new) {
return nil
}
x := diffLines(old)
y := diffLines(new)
var out bytes.Buffer
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
fmt.Fprintf(&out, "--- %s\n", oldName)
fmt.Fprintf(&out, "+++ %s\n", newName)
var (
done pair
chunk pair
count pair
ctext []string
)
for _, m := range diffTgs(x, y) {
if m.x < done.x {
continue
}
start := m
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
start.x--
start.y--
}
end := m
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
end.x++
end.y++
}
for _, s := range x[done.x:start.x] {
ctext = append(ctext, "-"+s)
count.x++
}
for _, s := range y[done.y:start.y] {
ctext = append(ctext, "+"+s)
count.y++
}
const C = 3
if (end.x < len(x) || end.y < len(y)) &&
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
for _, s := range x[start.x:end.x] {
ctext = append(ctext, " "+s)
count.x++
count.y++
}
done = end
continue
}
if len(ctext) > 0 {
n := end.x - start.x
if n > C {
n = C
}
for _, s := range x[start.x : start.x+n] {
ctext = append(ctext, " "+s)
count.x++
count.y++
}
done = pair{start.x + n, start.y + n}
if count.x > 0 {
chunk.x++
}
if count.y > 0 {
chunk.y++
}
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
for _, s := range ctext {
out.WriteString(s)
}
count.x = 0
count.y = 0
ctext = ctext[:0]
}
if end.x >= len(x) && end.y >= len(y) {
break
}
chunk = pair{end.x - C, end.y - C}
for _, s := range x[chunk.x:end.x] {
ctext = append(ctext, " "+s)
count.x++
count.y++
}
done = end
}
return out.Bytes()
}
func diffLines(x []byte) []string {
l := strings.SplitAfter(string(x), "\n")
if l[len(l)-1] == "" {
l = l[:len(l)-1]
} else {
l[len(l)-1] += "\n\\ No newline at end of file\n"
}
return l
}
func diffTgs(x, y []string) []pair {
m := make(map[string]int)
for _, s := range x {
if c := m[s]; c > -2 {
m[s] = c - 1
}
}
for _, s := range y {
if c := m[s]; c > -8 {
m[s] = c - 4
}
}
var xi, yi, inv []int
for i, s := range y {
if m[s] == -5 {
m[s] = len(yi)
yi = append(yi, i)
}
}
for i, s := range x {
if j, ok := m[s]; ok && j >= 0 {
xi = append(xi, i)
inv = append(inv, j)
}
}
J := inv
n := len(xi)
T := make([]int, n)
L := make([]int, n)
for i := range T {
T[i] = n + 1
}
for i := 0; i < n; i++ {
k := sort.Search(n, func(k int) bool {
return T[k] >= J[i]
})
T[k] = J[i]
L[i] = k + 1
}
k := 0
for _, v := range L {
if k < v {
k = v
}
}
seq := make([]pair, 2+k)
seq[1+k] = pair{len(x), len(y)}
lastj := n
for i := n - 1; i >= 0; i-- {
if L[i] == k && J[i] < lastj {
seq[k] = pair{xi[i], yi[J[i]]}
k--
}
}
seq[0] = pair{0, 0}
return seq
}
+4
View File
@@ -36,6 +36,10 @@ The -confdir=dir flag sets a dir with multiple json config
The -format=json flag sets the format of config files. The -format=json flag sets the format of config files.
Default "auto". 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, The -test flag tells Xray to test config files only,
without launching the server. without launching the server.
+31 -6
View File
@@ -2,12 +2,15 @@
package blackhole package blackhole
import ( import (
"bytes"
"context" "context"
"net/http"
"time" "time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/signal"
@@ -17,14 +20,34 @@ import (
// Handler is an outbound connection that silently swallow the entire payload. // Handler is an outbound connection that silently swallow the entire payload.
type Handler struct { type Handler struct {
response ResponseConfig response []byte
}
var http403response = http.Response{
StatusCode: 403,
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"Connection": {"close"},
"Cache-Control": {"max-age=3600, public"},
},
} }
// New creates a new blackhole handler. // New creates a new blackhole handler.
func New(ctx context.Context, config *Config) (*Handler, error) { func New(ctx context.Context, config *Config) (*Handler, error) {
response, err := config.GetInternalResponse() response := []byte{}
if err != nil { if config.Response != nil {
return nil, err switch config.Response.Type {
case "", "none":
case "http":
var data bytes.Buffer
common.Must(http403response.Write(&data))
response = data.Bytes()
case "custom":
response = config.Response.CustomResponseData
default:
return nil, errors.New("unknown blackhole response: " + config.Response.Type)
}
} }
return &Handler{ return &Handler{
response: response, response: response,
@@ -37,8 +60,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
ob.Name = "blackhole" ob.Name = "blackhole"
nBytes := h.response.WriteTo(link.Writer) if len(h.response) > 0 {
if nBytes > 0 { mbc := buf.MultiBufferContainer{}
common.Must2(mbc.Write(h.response))
link.Writer.WriteMultiBuffer(mbc.MultiBuffer)
// Sleep a little here to make sure the response is sent to client. // Sleep a little here to make sure the response is sent to client.
time.Sleep(time.Second) time.Sleep(time.Second)
} }
+45 -11
View File
@@ -1,12 +1,15 @@
package blackhole_test package blackhole_test
import ( import (
"bufio"
"bytes"
"context" "context"
"crypto/rand"
"net/http"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/proxy/blackhole" "github.com/xtls/xray-core/proxy/blackhole"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
@@ -16,27 +19,58 @@ import (
func TestBlackholeHTTPResponse(t *testing.T) { func TestBlackholeHTTPResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}}) ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
handler, err := blackhole.New(ctx, &blackhole.Config{ handler, err := blackhole.New(ctx, &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), Response: &blackhole.Response{Type: "http"},
}) })
common.Must(err) common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit()) reader, writer := pipe.New(pipe.WithoutSizeLimit())
var mb buf.MultiBuffer dataCh := make(chan buf.MultiBuffer, 1)
var rerr error
go func() { go func() {
b, e := reader.ReadMultiBuffer() mb := common.Must2(reader.ReadMultiBuffer())
mb = b dataCh <- mb
rerr = e
}() }()
link := transport.Link{ link := transport.Link{
Reader: reader, Reader: reader,
Writer: writer, Writer: writer,
} }
common.Must(handler.Process(ctx, &link, nil)) common.Must(handler.Process(ctx, &link, nil))
common.Must(rerr) mb := <-dataCh
if mb.IsEmpty() { data := make([]byte, mb.Len())
t.Error("expect http response, but nothing") mb.Copy(data)
resp := common.Must2(http.ReadResponse(bufio.NewReader(bytes.NewBuffer(data)), nil))
if resp.StatusCode != 403 {
t.Errorf("expected 403 response, got %d", resp.StatusCode)
}
}
func TestBlackholeCustomResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
// slightly bigger than a buffer
expected := make([]byte, buf.Size+1000)
if _, err := rand.Read(expected); err != nil {
t.Fatal(err)
}
handler, err := blackhole.New(ctx, &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: expected,
},
})
common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit())
var actual buf.MultiBuffer
var rerr error
go func() {
actual, rerr = reader.ReadMultiBuffer()
}()
link := transport.Link{Reader: reader, Writer: writer}
common.Must(handler.Process(ctx, &link, nil))
common.Must(rerr)
if actual.String() != string(expected) {
t.Errorf("custom response mismatch")
} }
} }
-47
View File
@@ -1,47 +0,0 @@
package blackhole
import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
const (
http403response = `HTTP/1.1 403 Forbidden
Connection: close
Cache-Control: max-age=3600, public
Content-Length: 0
`
)
// ResponseConfig is the configuration for blackhole responses.
type ResponseConfig interface {
// WriteTo writes a predefined response to the specified buffer.
WriteTo(buf.Writer) int32
}
// WriteTo implements ResponseConfig.WriteTo().
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }
// WriteTo implements ResponseConfig.WriteTo().
func (*HTTPResponse) WriteTo(writer buf.Writer) int32 {
b := buf.New()
common.Must2(b.WriteString(http403response))
n := b.Len()
writer.WriteMultiBuffer(buf.MultiBuffer{b})
return n
}
// GetInternalResponse converts response settings from proto to internal data structure.
func (c *Config) GetInternalResponse() (ResponseConfig, error) {
if c.GetResponse() == nil {
return new(NoneResponse), nil
}
config, err := c.GetResponse().GetInstance()
if err != nil {
return nil, err
}
return config.(ResponseConfig), nil
}
+37 -59
View File
@@ -7,7 +7,6 @@
package blackhole package blackhole
import ( import (
serial "github.com/xtls/xray-core/common/serial"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
@@ -22,26 +21,28 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type NoneResponse struct { type Response struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
sizeCache protoimpl.SizeCache CustomResponseData []byte `protobuf:"bytes,2,opt,name=custom_response_data,json=customResponseData,proto3" json:"custom_response_data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *NoneResponse) Reset() { func (x *Response) Reset() {
*x = NoneResponse{} *x = Response{}
mi := &file_proxy_blackhole_config_proto_msgTypes[0] mi := &file_proxy_blackhole_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *NoneResponse) String() string { func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*NoneResponse) ProtoMessage() {} func (*Response) ProtoMessage() {}
func (x *NoneResponse) ProtoReflect() protoreflect.Message { func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[0] mi := &file_proxy_blackhole_config_proto_msgTypes[0]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -53,57 +54,35 @@ func (x *NoneResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use NoneResponse.ProtoReflect.Descriptor instead. // Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*NoneResponse) Descriptor() ([]byte, []int) { func (*Response) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0} return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0}
} }
type HTTPResponse struct { func (x *Response) GetType() string {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HTTPResponse) Reset() {
*x = HTTPResponse{}
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HTTPResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HTTPResponse) ProtoMessage() {}
func (x *HTTPResponse) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) return x.Type
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
} }
return mi.MessageOf(x) return ""
} }
// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead. func (x *Response) GetCustomResponseData() []byte {
func (*HTTPResponse) Descriptor() ([]byte, []int) { if x != nil {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1} return x.CustomResponseData
}
return nil
} }
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Response *serial.TypedMessage `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` Response *Response `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_proxy_blackhole_config_proto_msgTypes[2] mi := &file_proxy_blackhole_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -115,7 +94,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[2] mi := &file_proxy_blackhole_config_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -128,10 +107,10 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{2} return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
} }
func (x *Config) GetResponse() *serial.TypedMessage { func (x *Config) GetResponse() *Response {
if x != nil { if x != nil {
return x.Response return x.Response
} }
@@ -142,11 +121,12 @@ var File_proxy_blackhole_config_proto protoreflect.FileDescriptor
const file_proxy_blackhole_config_proto_rawDesc = "" + const file_proxy_blackhole_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n" + "\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\"P\n" +
"\fNoneResponse\"\x0e\n" + "\bResponse\x12\x12\n" +
"\fHTTPResponse\"F\n" + "\x04type\x18\x01 \x01(\tR\x04type\x120\n" +
"\x06Config\x12<\n" + "\x14custom_response_data\x18\x02 \x01(\fR\x12customResponseData\"D\n" +
"\bresponse\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\bresponseB^\n" + "\x06Config\x12:\n" +
"\bresponse\x18\x01 \x01(\v2\x1e.xray.proxy.blackhole.ResponseR\bresponseB^\n" +
"\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3" "\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3"
var ( var (
@@ -161,15 +141,13 @@ func file_proxy_blackhole_config_proto_rawDescGZIP() []byte {
return file_proxy_blackhole_config_proto_rawDescData return file_proxy_blackhole_config_proto_rawDescData
} }
var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_blackhole_config_proto_goTypes = []any{ var file_proxy_blackhole_config_proto_goTypes = []any{
(*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse (*Response)(nil), // 0: xray.proxy.blackhole.Response
(*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse (*Config)(nil), // 1: xray.proxy.blackhole.Config
(*Config)(nil), // 2: xray.proxy.blackhole.Config
(*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage
} }
var file_proxy_blackhole_config_proto_depIdxs = []int32{ var file_proxy_blackhole_config_proto_depIdxs = []int32{
3, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.common.serial.TypedMessage 0, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.proxy.blackhole.Response
1, // [1:1] is the sub-list for method output_type 1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type 1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension type_name
@@ -188,7 +166,7 @@ func file_proxy_blackhole_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 3, NumMessages: 2,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+5 -6
View File
@@ -6,12 +6,11 @@ option go_package = "github.com/xtls/xray-core/proxy/blackhole";
option java_package = "com.xray.proxy.blackhole"; option java_package = "com.xray.proxy.blackhole";
option java_multiple_files = true; option java_multiple_files = true;
import "common/serial/typed_message.proto"; message Response {
string type = 1;
message NoneResponse {} bytes custom_response_data = 2;
}
message HTTPResponse {}
message Config { message Config {
xray.common.serial.TypedMessage response = 1; Response response = 1;
} }
+7 -14
View File
@@ -1,26 +1,19 @@
package blackhole_test package blackhole_test
import ( import (
"bufio" "context"
"net/http"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/proxy/blackhole"
. "github.com/xtls/xray-core/proxy/blackhole"
) )
func TestHTTPResponse(t *testing.T) { func TestHTTPResponse(t *testing.T) {
buffer := buf.New() handler, err := blackhole.New(context.Background(), &blackhole.Config{
Response: &blackhole.Response{Type: "http"},
httpResponse := new(HTTPResponse) })
httpResponse.WriteTo(buf.NewWriter(buffer))
reader := bufio.NewReader(buffer)
response, err := http.ReadResponse(reader, nil)
common.Must(err) common.Must(err)
if handler == nil {
if response.StatusCode != 403 { t.Error("expected HTTP response handler")
t.Error("expected status code 403, but got ", response.StatusCode)
} }
} }
+78
View File
@@ -0,0 +1,78 @@
//go:build openbsd
// +build openbsd
package dokodemo
import (
"fmt"
"net"
"os"
"golang.org/x/sys/unix"
)
func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) {
domain := unix.AF_INET6
var sockaddr unix.Sockaddr
if ip4 := addr.IP.To4(); ip4 != nil {
domain = unix.AF_INET
sa := &unix.SockaddrInet4{Port: addr.Port}
copy(sa.Addr[:], ip4)
sockaddr = sa
} else if ip6 := addr.IP.To16(); ip6 != nil {
sa := &unix.SockaddrInet6{Port: addr.Port}
copy(sa.Addr[:], ip6)
if addr.Zone != "" {
iface, err := net.InterfaceByName(addr.Zone)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("resolve zone %s: %w", addr.Zone, err)}
}
sa.ZoneId = uint32(iface.Index)
}
sockaddr = sa
} else {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("unsupported address %v", addr.IP)}
}
fd, err := unix.Socket(domain, unix.SOCK_DGRAM, 0)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)}
}
closeFD := true
defer func() {
if closeFD {
unix.Close(fd)
}
}()
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BINDANY, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_BINDANY: %w", err)}
}
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)}
}
// Several client sessions can be answered from the same original
// destination at the same time, so the address has to be shareable.
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEPORT: %w", err)}
}
if err = unix.Bind(fd, sockaddr); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)}
}
fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-bindany-%s", addr.String()))
if fdFile == nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to file")}
}
defer fdFile.Close()
packetConn, err := net.FilePacketConn(fdFile)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert descriptor to packet connection: %w", err)}
}
closeFD = false
return packetConn, nil
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build !linux //go:build !linux && !openbsd
// +build !linux // +build !linux,!openbsd
package dokodemo package dokodemo
+107 -129
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"io" "io"
"strings" "strings"
"sync/atomic"
"time" "time"
"github.com/pires/go-proxyproto" "github.com/pires/go-proxyproto"
@@ -31,15 +32,31 @@ import (
) )
var ( var (
useSplice bool useSplice atomic.Bool
allNetworks [8]bool allNetworks [8]bool
defaultBlockPrivateRule *FinalRule defaultBlockPrivateRule *FinalRule
defaultBlockAllRule *FinalRule defaultBlockAllRule *FinalRule
) )
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
enabled := false
switch value {
case defaultFlagValue, "auto", "enable":
enabled = true
}
useSplice.Store(enabled)
return nil
}
func init() { func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
h := new(Handler) h := new(Handler)
if streamSettings, ok := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig); ok && streamSettings.SocketSettings != nil {
h.resolveStrategy = streamSettings.SocketSettings.DomainStrategy
h.usesDialerProxy = len(streamSettings.SocketSettings.DialerProxy) > 0
}
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error { if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
return h.Init(config.(*Config), pm) return h.Init(config.(*Config), pm)
}); err != nil { }); err != nil {
@@ -48,12 +65,7 @@ func init() {
return h, nil return h, nil
})) }))
const defaultFlagValue = "NOT_DEFINED_AT_ALL" platform.RegisterEnvReload(reloadEnvSettings)
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useSplice = true
}
for i := range allNetworks { for i := range allNetworks {
allNetworks[i] = true allNetworks[i] = true
@@ -62,26 +74,7 @@ func init() {
defaultBlockPrivateRule = &FinalRule{ defaultBlockPrivateRule = &FinalRule{
action: RuleAction_Block, action: RuleAction_Block,
network: allNetworks, network: allNetworks,
ip: common.Must2(geodata.IPReg.BuildIPMatcher(common.Must2(geodata.ParseIPRules([]string{ ip: geodata.GetPrivateIPMatcher(),
"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",
"ff00::/8",
})))),
} }
defaultBlockAllRule = &FinalRule{ defaultBlockAllRule = &FinalRule{
@@ -100,9 +93,11 @@ type FinalRule struct {
// Handler handles Freedom connections. // Handler handles Freedom connections.
type Handler struct { type Handler struct {
policyManager policy.Manager policyManager policy.Manager
config *Config config *Config
finalRules []*FinalRule finalRules []*FinalRule
resolveStrategy internet.DomainStrategy
usesDialerProxy bool
} }
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) { func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
@@ -179,22 +174,6 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
return nil return nil
} }
func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool {
if !dialDest.Address.Family().IsDomain() {
return false
}
if len(h.finalRules) > 0 {
rule := h.finalRules[0]
if rule.action == RuleAction_Allow && rule.network[dialDest.Network] && len(rule.port) == 0 && rule.ip == nil {
return false
}
}
if defaultRule != nil || len(h.finalRules) > 0 {
return true
}
return false
}
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule { func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
for _, rule := range h.finalRules { for _, rule := range h.finalRules {
if rule.Apply(network, address, port) { if rule.Apply(network, address, port) {
@@ -207,13 +186,6 @@ func (h *Handler) matchFinalRule(network net.Network, address net.Address, port
return nil return nil
} }
func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction {
if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil {
return rule.action
}
return RuleAction_Allow
}
// Init initializes the Handler with necessary parameters. // Init initializes the Handler with necessary parameters.
func (h *Handler) Init(config *Config, pm policy.Manager) error { func (h *Handler) Init(config *Config, pm policy.Manager) error {
h.config = config h.config = config
@@ -248,6 +220,20 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
} }
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() {
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
}
func isValidAddress(addr *net.IPOrDomain) bool { func isValidAddress(addr *net.IPOrDomain) bool {
if addr == nil { if addr == nil {
return false return false
@@ -295,61 +281,53 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
var conn stat.Connection var conn stat.Connection
var blockedDest *net.Destination var blockedDest *net.Destination
var blockedRule *FinalRule var blockedRule *FinalRule
firstResolve := true
err := retry.ExponentialBackoff(5, 100).On(func() error { err := retry.ExponentialBackoff(5, 100).On(func() error {
dialDest := destination if destination.Address.Family().IsDomain() {
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() { if defaultRule != nil || len(h.finalRules) > 0 {
strategy := h.config.DomainStrategy if strategy := h.resolveStrategy; strategy.HasStrategy() {
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil { ips, err := internet.LookupForIP(destination.Address.Domain(), strategy, outGateway)
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family()) if err != nil { // non-force may still dial with system DNS
} errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway) if strategy.ForceIP() {
if err != nil { return err // retry
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain()) }
if h.config.DomainStrategy.ForceIP() || h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { }
return err for _, ip := range ips {
if addr := net.IPAddress(ip); addr != nil {
if rule := h.matchFinalRule(destination.Network, addr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &destination
blockedDest.Address = addr
blockedRule = rule
return nil
}
}
}
} else {
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, destination.Address.Domain())
if err != nil { // dialer may retry DNS
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
}
for _, addr := range addrs {
if ipAddr := net.IPAddress(addr.IP); ipAddr != nil {
if rule := h.matchFinalRule(destination.Network, ipAddr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &destination
blockedDest.Address = ipAddr
blockedRule = rule
return nil
}
}
}
} }
} else {
dialDest = net.Destination{
Network: dialDest.Network,
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
Port: dialDest.Port,
}
errors.LogInfo(ctx, "dialing to ", dialDest)
} }
} else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules } else {
domain := dialDest.Address.Domain() if rule := h.matchFinalRule(destination.Network, destination.Address, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
var ips []net.IP blockedDest = &destination
if firstResolve { blockedRule = rule
firstResolve = false return nil
supportIPv4, supportIPv6 := utils.CheckRoutes()
if supportIPv4 {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip4", domain)
}
if len(ips) == 0 && supportIPv6 {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip6", domain)
}
if len(ips) == 0 {
return errors.New("failed to get IP address for domain ", domain)
}
} else {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip", domain)
} }
if len(ips) == 0 { // SRV/TXT, lookup failed
return errors.New("failed to get IP address for domain ", domain)
}
if addr := net.IPAddress(ips[dice.Roll(len(ips))]); addr != nil {
dialDest.Address = addr
errors.LogInfo(ctx, "dialing to ", dialDest)
}
}
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &dialDest
blockedRule = rule
return nil
} }
rawConn, err := dialer.Dial(ctx, dialDest) rawConn, err := dialer.Dial(ctx, destination)
if err != nil { if err != nil {
return err return err
} }
@@ -361,20 +339,21 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return errors.New("failed to open connection to ", destination).Base(err) return errors.New("failed to open connection to ", destination).Base(err)
} }
if blockedDest != nil { if blockedDest != nil {
delay := h.blockDelay(blockedRule) return h.blackhole(ctx, input, output, blockedRule, blockedDest)
errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() {
common.Interrupt(input)
common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest)
})
defer timer.Stop()
defer common.Close(output)
if err := buf.Copy(input, buf.Discard); err != nil {
return nil
}
return nil
} }
if destination.Address.Family().IsDomain() && (defaultRule != nil || len(h.finalRules) > 0) {
if h.usesDialerProxy {
errors.LogInfo(ctx, "skipping final rule check for proxied remote endpoint, original target: ", destination)
} else {
// pre-check may fail or dialer may select another IP
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
conn.Close()
return h.blackhole(ctx, input, output, rule, &remoteDest)
}
}
}
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 { if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol) version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr() srcAddr := inbound.Source.RawNetAddr()
@@ -419,7 +398,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
writer = buf.NewWriter(conn) writer = buf.NewWriter(conn)
} }
} else { } else {
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination) writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
if h.config.Noises != nil { if h.config.Noises != nil {
errors.LogDebug(ctx, "NOISE", h.config.Noises) errors.LogDebug(ctx, "NOISE", h.config.Noises)
writer = &NoisePacketWriter{ writer = &NoisePacketWriter{
@@ -441,7 +420,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
responseDone := func() error { responseDone := func() error {
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
var writeConn net.Conn var writeConn net.Conn
var inTimer *signal.ActivityTimer var inTimer *signal.ActivityTimer
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil { if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil {
@@ -523,7 +502,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
} }
udpAddr := d.(*net.UDPAddr) udpAddr := d.(*net.UDPAddr)
sourceAddr := net.IPAddress(udpAddr.IP) sourceAddr := net.IPAddress(udpAddr.IP)
if r.Handler.applyFinalRules(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule) == RuleAction_Block { if rule := r.Handler.matchFinalRule(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule); rule != nil && rule.action == RuleAction_Block {
continue continue
} }
b.Resize(0, int32(n)) b.Resize(0, int32(n))
@@ -548,7 +527,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
} }
// DialDest means the dial target used in the dialer when creating conn // DialDest means the dial target used in the dialer when creating conn
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Writer { func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination, outGateway net.Address) buf.Writer {
iConn := conn iConn := conn
statConn, ok := iConn.(*stat.CounterConnection) statConn, ok := iConn.(*stat.CounterConnection)
if ok { if ok {
@@ -572,9 +551,8 @@ func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
DefaultRule: defaultRule, DefaultRule: defaultRule,
UDPOverride: UDPOverride, UDPOverride: UDPOverride,
ResolvedUDPAddr: resolvedUDPAddr, ResolvedUDPAddr: resolvedUDPAddr,
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address, OutGateway: outGateway,
} }
} }
return &buf.SequentialWriter{Writer: conn} return &buf.SequentialWriter{Writer: conn}
} }
@@ -591,7 +569,7 @@ type PacketWriter struct {
// Resulting in these packets being sent to many different IPs randomly // Resulting in these packets being sent to many different IPs randomly
// So, cache and keep the resolve result // So, cache and keep the resolve result
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address] ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
LocalAddr net.Address OutGateway net.Address
} }
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
@@ -614,21 +592,21 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok { if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
b.UDP.Address = ip b.UDP.Address = ip
} else { } else {
ShouldUseSystemResolver := true shouldUseSystemResolver := true
if w.Handler.config.DomainStrategy.HasStrategy() { if strategy := w.Handler.resolveStrategy; strategy.HasStrategy() {
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr) ips, err := internet.LookupForIP(b.UDP.Address.Domain(), strategy, w.OutGateway)
if err != nil { if err != nil {
// drop packet if resolve failed when forceIP // drop packet if resolve failed when forceIP
if w.Handler.config.DomainStrategy.ForceIP() { if strategy.ForceIP() {
b.Release() b.Release()
continue continue
} }
} else { } else {
ip = net.IPAddress(ips[dice.Roll(len(ips))]) ip = net.IPAddress(ips[dice.Roll(len(ips))])
ShouldUseSystemResolver = false shouldUseSystemResolver = false
} }
} }
if ShouldUseSystemResolver { if shouldUseSystemResolver {
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr()) udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
if err != nil { if err != nil {
b.Release() b.Release()
@@ -642,7 +620,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
} }
} }
} }
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block { if rule := w.matchFinalRule(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule); rule != nil && rule.action == RuleAction_Block {
b.Release() b.Release()
continue continue
} }
+6 -3
View File
@@ -173,15 +173,18 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
outbounds := session.OutboundsFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
if inbound == nil || ob == nil { var src net.Destination
return nil, errors.New("missing inbound or outbound metadata from context") if inbound != nil {
src = inbound.Source
} else {
src = net.TCPDestination(net.AnyIP, 0)
} }
data := struct { data := struct {
Source net.Destination Source net.Destination
Target net.Destination Target net.Destination
}{ }{
Source: inbound.Source, Source: src,
Target: ob.Target, Target: ob.Target,
} }
+1 -1
View File
@@ -332,7 +332,7 @@ func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer
return nil, errors.New("failed to read http 1xx response").Base(err) return nil, errors.New("failed to read http 1xx response").Base(err)
} }
ResponseHeader1xx = append(ResponseHeader1xx, data...) ResponseHeader1xx = append(ResponseHeader1xx, data...)
if bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) { if len(ResponseHeader1xx) >= 4 && bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
break break
} }
if len(ResponseHeader1xx) > 1024 { if len(ResponseHeader1xx) > 1024 {
+49
View File
@@ -0,0 +1,49 @@
package http
import (
"bufio"
"bytes"
"io"
"net/http"
"strings"
"testing"
)
// A malformed upstream response containing a bare '\n' before the real
// status line used to crash readResponseAndHandle100Continue: the first
// ReadSlice('\n') returns fewer than 4 bytes, and slicing
// ResponseHeader1xx[len(ResponseHeader1xx)-4:] panicked with a negative
// index instead of returning an error.
func TestReadResponseAndHandle100ContinueDoesNotPanicOnEarlyNewline(t *testing.T) {
payload := "X\nHTTP/1.1 100 Continue\r\n\r\n" + strings.Repeat("A", 40)
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
req, err := http.NewRequest("GET", "http://example.com/", nil)
if err != nil {
t.Fatal(err)
}
// Must not panic; a parse error for the garbage trailing bytes is fine.
_, _ = readResponseAndHandle100Continue(r, req, io.Discard)
}
func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) {
payload := "HTTP/1.1 100 Continue\r\n\r\n" +
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
req, err := http.NewRequest("GET", "http://example.com/", nil)
if err != nil {
t.Fatal(err)
}
var forwarded bytes.Buffer
resp, err := readResponseAndHandle100Continue(r, req, &forwarded)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected status 200, got %d", resp.StatusCode)
}
if !strings.Contains(forwarded.String(), "100 Continue") {
t.Fatalf("expected 1xx response to be forwarded, got %q", forwarded.String())
}
}
+53 -11
View File
@@ -5,14 +5,14 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/uuid"
"github.com/google/uuid"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
func (a *Account) AsAccount() (protocol.Account, error) { func (a *Account) AsAccount() (protocol.Account, error) {
var VR net.Port var VR net.Port
if id, err := uuid.ParseString(a.Auth); err == nil { if id, err := uuid.Parse(a.Auth); err == nil {
VR = net.PortFromBytes(id[6:8]) VR = net.PortFromBytes(id[6:8])
} }
return &MemoryAccount{ return &MemoryAccount{
@@ -41,29 +41,71 @@ func (a *MemoryAccount) ToProto() proto.Message {
type Validator struct { type Validator struct {
users sync.Map users sync.Map
ids sync.Map
mu sync.Mutex
} }
func NewValidator() *Validator { func NewValidator() *Validator {
return &Validator{} return &Validator{}
} }
func (v *Validator) Add(user *protocol.MemoryUser) error { func (v *Validator) Add(user *protocol.MemoryUser) (err error) {
v.mu.Lock()
v.users.Store(user.Account.(*MemoryAccount).Auth, user) v.users.Store(user.Account.(*MemoryAccount).Auth, user)
return nil if id, err := uuid.Parse(user.Account.(*MemoryAccount).Auth); err == nil {
id[6] = 0
id[7] = 0
v.ids.Store(id, user)
}
v.mu.Unlock()
return
} }
func (v *Validator) DelByEmail(email string) error { func (v *Validator) DelByEmail(email string) (err error) {
v.mu.Lock()
if user := v.GetByEmail(email); user != nil { if user := v.GetByEmail(email); user != nil {
v.users.Delete(user.Account.(*MemoryAccount).Auth) auth := user.Account.(*MemoryAccount).Auth
v.users.Delete(auth)
if id, err := uuid.Parse(auth); err == nil {
id[6] = 0
id[7] = 0
v.ids.Delete(id)
}
} }
return nil v.mu.Unlock()
return
} }
func (v *Validator) Get(auth string) *protocol.MemoryUser { func (v *Validator) Get(auth string) (user *protocol.MemoryUser) {
if value, ok := v.users.Load(auth); ok { if id, err := uuid.Parse(auth); err == nil {
return value.(*protocol.MemoryUser) if user = v.GetByID(id); user != nil {
VR := net.PortFromBytes(id[6:8])
if user.Account.(*MemoryAccount).VR != VR {
user = &protocol.MemoryUser{
Email: user.Email,
Level: user.Level,
Account: &MemoryAccount{
Auth: auth,
VR: VR,
},
}
}
}
return
} }
return nil if value, ok := v.users.Load(auth); ok {
user = value.(*protocol.MemoryUser)
}
return
}
func (v *Validator) GetByID(id uuid.UUID) (user *protocol.MemoryUser) {
id[6] = 0
id[7] = 0
if value, ok := v.ids.Load(id); ok {
user = value.(*protocol.MemoryUser)
}
return
} }
func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) { func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) {
-26
View File
@@ -85,8 +85,6 @@ func (a *Account) getCipher() (Cipher, error) {
IVBytes: 32, IVBytes: 32,
AEADAuthCreator: createXChaCha20Poly1305, AEADAuthCreator: createXChaCha20Poly1305,
}, nil }, nil
case CipherType_NONE:
return NoneCipher{}, nil
default: default:
return nil, errors.New("Unsupported cipher.") return nil, errors.New("Unsupported cipher.")
} }
@@ -186,30 +184,6 @@ func (c *AEADCipher) DecodePacket(key []byte, b *buf.Buffer) error {
return nil return nil
} }
type NoneCipher struct{}
func (NoneCipher) KeySize() int32 { return 0 }
func (NoneCipher) IVSize() int32 { return 0 }
func (NoneCipher) IsAEAD() bool {
return false
}
func (NoneCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) {
return buf.NewReader(reader), nil
}
func (NoneCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) {
return buf.NewWriter(writer), nil
}
func (NoneCipher) EncodePacket(key []byte, b *buf.Buffer) error {
return nil
}
func (NoneCipher) DecodePacket(key []byte, b *buf.Buffer) error {
return nil
}
func passwordToCipherKey(password []byte, keySize int32) []byte { func passwordToCipherKey(password []byte, keySize int32) []byte {
key := make([]byte, 0, keySize) key := make([]byte, 0, keySize)
+2 -6
View File
@@ -31,7 +31,6 @@ const (
CipherType_AES_256_GCM CipherType = 6 CipherType_AES_256_GCM CipherType = 6
CipherType_CHACHA20_POLY1305 CipherType = 7 CipherType_CHACHA20_POLY1305 CipherType = 7
CipherType_XCHACHA20_POLY1305 CipherType = 8 CipherType_XCHACHA20_POLY1305 CipherType = 8
CipherType_NONE CipherType = 9
) )
// Enum value maps for CipherType. // Enum value maps for CipherType.
@@ -42,7 +41,6 @@ var (
6: "AES_256_GCM", 6: "AES_256_GCM",
7: "CHACHA20_POLY1305", 7: "CHACHA20_POLY1305",
8: "XCHACHA20_POLY1305", 8: "XCHACHA20_POLY1305",
9: "NONE",
} }
CipherType_value = map[string]int32{ CipherType_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
@@ -50,7 +48,6 @@ var (
"AES_256_GCM": 6, "AES_256_GCM": 6,
"CHACHA20_POLY1305": 7, "CHACHA20_POLY1305": 7,
"XCHACHA20_POLY1305": 8, "XCHACHA20_POLY1305": 8,
"NONE": 9,
} }
) )
@@ -251,15 +248,14 @@ const file_proxy_shadowsocks_config_proto_rawDesc = "" +
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" +
"\anetwork\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"L\n" + "\anetwork\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"L\n" +
"\fClientConfig\x12<\n" + "\fClientConfig\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*t\n" + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*j\n" +
"\n" + "\n" +
"CipherType\x12\v\n" + "CipherType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\x0f\n" + "\aUNKNOWN\x10\x00\x12\x0f\n" +
"\vAES_128_GCM\x10\x05\x12\x0f\n" + "\vAES_128_GCM\x10\x05\x12\x0f\n" +
"\vAES_256_GCM\x10\x06\x12\x15\n" + "\vAES_256_GCM\x10\x06\x12\x15\n" +
"\x11CHACHA20_POLY1305\x10\a\x12\x16\n" + "\x11CHACHA20_POLY1305\x10\a\x12\x16\n" +
"\x12XCHACHA20_POLY1305\x10\b\x12\b\n" + "\x12XCHACHA20_POLY1305\x10\bBd\n" +
"\x04NONE\x10\tBd\n" +
"\x1acom.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3" "\x1acom.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3"
var ( var (
-1
View File
@@ -23,7 +23,6 @@ enum CipherType {
AES_256_GCM = 6; AES_256_GCM = 6;
CHACHA20_POLY1305 = 7; CHACHA20_POLY1305 = 7;
XCHACHA20_POLY1305 = 8; XCHACHA20_POLY1305 = 8;
NONE = 9;
} }
message ServerConfig { message ServerConfig {
-17
View File
@@ -38,19 +38,6 @@ func TestUDPEncodingDecoding(t *testing.T) {
}), }),
}, },
}, },
{
Version: Version,
Command: protocol.RequestCommandUDP,
Address: net.LocalHostIP,
Port: 1234,
User: &protocol.MemoryUser{
Email: "love@example.com",
Account: toAccount(&Account{
Password: "123",
CipherType: CipherType_NONE,
}),
},
},
} }
for _, request := range testRequests { for _, request := range testRequests {
@@ -80,10 +67,6 @@ func TestUDPDecodingWithPayloadTooShort(t *testing.T) {
Password: "password", Password: "password",
CipherType: CipherType_AES_128_GCM, CipherType: CipherType_AES_128_GCM,
}), }),
toAccount(&Account{
Password: "password",
CipherType: CipherType_NONE,
}),
} }
for _, account := range testAccounts { for _, account := range testAccounts {
-1
View File
@@ -145,7 +145,6 @@ func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol
} else { } else {
u = user u = user
ivLen = user.Account.(*MemoryAccount).Cipher.IVSize() ivLen = user.Account.(*MemoryAccount).Cipher.IVSize()
// err = user.Account.(*MemoryAccount).CheckIV(bs[:ivLen]) // The IV size of None Cipher is 0.
return return
} }
} }
+1
View File
@@ -82,6 +82,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
if err != nil { if err != nil {
return errors.New("failed to connect to server").Base(err) return errors.New("failed to connect to server").Base(err)
} }
defer connection.Close()
if session.TimeoutOnlyFromContext(ctx) { if session.TimeoutOnlyFromContext(ctx) {
ctx, _ = context.WithCancel(context.Background()) ctx, _ = context.WithCancel(context.Background())
+18 -7
View File
@@ -14,11 +14,12 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
## DETAILS ## DETAILS
Current implementation does not contain options to configure network level addresses, routing or rules. By default, enabling the feature will only bring the tun interface up. \
Enabling the feature will result only tun interface up, and that's it. \ When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \
This is explicit decision, significantly simplifying implementation, and allowing any number of custom configurations, consumers could come up with. Network interface is OS level entity, and OS is what should manage it. \ Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
Working configuration, is tun enabled in Xray config with specific name (e.g. xray0), and OS level configuration to manage "xray0" interface, applying routing and rules on interface up. Linux and macOS do not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \
This way consistency of system level routing and rules is ensured from single place of responsibility - the OS itself. \ For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README. Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \ Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \
@@ -30,13 +31,17 @@ Here is simple Xray config snippet to enable the inbound:
"port": 0, "port": 0,
"protocol": "tun", "protocol": "tun",
"settings": { "settings": {
"name": "xray0", "name": "utun10",
"MTU": 1492 "desc": "Wintun",
"mtu": 1500
} }
} }
], ],
``` ```
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
It is ignored on other platforms.
## SUPPORTED FEATURES ## SUPPORTED FEATURES
- IPv4 and IPv6 - IPv4 and IPv6
@@ -206,6 +211,11 @@ ifconfig
Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists. Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists.
It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number. It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number.
macOS requires the utun interface to have a point-to-point IPv4 address before IPv4 routes can use it. \
By default Xray uses `169.254.10.1/30` as the remote gateway address and assigns the next address in the prefix to the local utun side. \
You can override this by setting `gateway`; macOS uses the first IPv4 prefix in the list. IPv6 `gateway` entries are not used for utun addressing, and IPv6 routes use the interface route instead. \
The `dns` field does not change macOS system DNS.
To attach routing to the interface, route command like following can be executed: To attach routing to the interface, route command like following can be executed:
``` ```
sudo route add -net 1.1.1.0/24 -iface utun10 sudo route add -net 1.1.1.0/24 -iface utun10
@@ -214,6 +224,7 @@ sudo route add -net 1.1.1.0/24 -iface utun10
sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10 sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10
sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10 sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10
``` ```
Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running.
Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure. Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure.
## ANDROID SUPPORT ## ANDROID SUPPORT
+14 -4
View File
@@ -7,11 +7,12 @@
package tun package tun
import ( import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe" unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
) )
const ( const (
@@ -30,6 +31,7 @@ type Config struct {
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"` AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"` AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -113,11 +115,18 @@ func (x *Config) GetAutoOutboundsInterface() string {
return "" return ""
} }
func (x *Config) GetDesc() string {
if x != nil {
return x.Desc
}
return ""
}
var File_proxy_tun_config_proto protoreflect.FileDescriptor var File_proxy_tun_config_proto protoreflect.FileDescriptor
const file_proxy_tun_config_proto_rawDesc = "" + const file_proxy_tun_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" + "\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
"\x06Config\x12\x12\n" + "\x06Config\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" + "\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
@@ -126,7 +135,8 @@ const file_proxy_tun_config_proto_rawDesc = "" +
"\n" + "\n" +
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" + "user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" + "\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" + "\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3" "\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
var ( var (
+1
View File
@@ -14,4 +14,5 @@ message Config {
uint32 user_level = 5; uint32 user_level = 5;
repeated string auto_system_routing_table = 6; repeated string auto_system_routing_table = 6;
string auto_outbounds_interface = 7; string auto_outbounds_interface = 7;
string desc = 8;
} }
+2 -2
View File
@@ -65,7 +65,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink { if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager) statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink" name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name) c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil { if c != nil {
t.uplinkCounter = c t.uplinkCounter = c
} }
@@ -73,7 +73,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink { if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager) statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink" name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name) c, _ := statsManager.GetOrRegisterCounter(name)
if c != nil { if c != nil {
t.downlinkCounter = c t.downlinkCounter = c
} }
+196 -23
View File
@@ -11,6 +11,8 @@ import (
"os" "os"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time"
"unsafe" "unsafe"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
@@ -24,11 +26,11 @@ import (
) )
const ( const (
utunControlName = "com.apple.net.utun_control" utunControlName = "com.apple.net.utun_control"
sysprotoControl = 2 sysprotoControl = 2
gateway = "169.254.10.1/30" defaultDarwinGateway = "169.254.10.1/30"
utunHeaderSize = 4 utunHeaderSize = 4
UTUN_OPT_IFNAME = 2 UTUN_OPT_IFNAME = 2
) )
const ( const (
@@ -38,18 +40,109 @@ const (
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
) )
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
type DarwinTun struct { type DarwinTun struct {
tunFile *os.File tunFile *os.File
options *Config options *Config
tunFd int tunFd int
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
// Genuinely blocks Wait() until tunFd is readable, instead of the
// previous procyield-only busy-spin (dispatchLoop in
// stack_gvisor_endpoint.go calls ReadPacket() then Wait() in a tight
// loop with no other throttling whenever the queue is empty -- with
// only procyield(1), that pins a full CPU core for as long as the
// tunnel is up, observed causing severe device heating/thermal
// shutdown). nil if kqueue setup failed, in which case Wait() falls
// back to a bounded time.Sleep instead. See waitKqueue's own doc
// comment for why this is a dedicated type rather than a bare fd.
waitKq *waitKqueue
routeMonitor *os.File routeMonitor *os.File
routeMonitorOnce sync.Once routeMonitorOnce sync.Once
systemRoutes []netip.Prefix systemRoutes []netip.Prefix
gateway netip.Prefix
}
// waitKqueue owns a kqueue fd used by DarwinTun.Wait() to block on
// read-readiness. Closing and waiting can race from different goroutines
// (Close() from the caller that tears down the tunnel, Wait() from
// dispatchLoop's own goroutine) -- reviewer feedback on XTLS/Xray-core#6580
// found that a bare `int` fd field let Close() race Wait()'s use of the
// same fd number, and on Darwin a closed fd number can be reused by an
// unrelated concurrent open() before Wait() gets to call Kevent on it,
// so Wait() could end up polling (or Close() could end up closing) a
// completely unrelated file descriptor. This type makes closing
// idempotent (sync.Once) and gates every Kevent call behind an atomic
// "closed" flag checked immediately before the syscall, so Wait() never
// issues a kevent syscall against a fd number that Close() has already
// (or is concurrently) invalidated -- there's still a narrow window where
// Wait() checks-then-uses the fd, but Close() only actually closes it
// after Wait() cannot start a new syscall on it (the flag is set first,
// synchronized with acquire/release semantics), which is sufficient since
// Wait()'s Kevent call itself is what's being raced, not a fd read/write.
type waitKqueue struct {
fd int
closed atomic.Bool
once sync.Once
}
// newWaitKqueue creates a kqueue registered for read-readiness on fd, for
// Wait() to block on. Returns nil if anything fails, so callers can fall
// back to a bounded sleep rather than error out of NewTun over what is
// purely a CPU-efficiency concern.
func newWaitKqueue(fd int) *waitKqueue {
kq, err := unix.Kqueue()
if err != nil {
return nil
}
_, err = unix.Kevent(kq, []unix.Kevent_t{{
Ident: uint64(fd),
Filter: unix.EVFILT_READ,
Flags: unix.EV_ADD | unix.EV_ENABLE,
}}, nil, nil)
if err != nil {
_ = unix.Close(kq)
return nil
}
return &waitKqueue{fd: kq}
}
// wait blocks until the registered fd is readable, timeout elapses, or a
// benign interrupt occurs -- all three are "this kqueue is still healthy,
// the caller should just try again" and return true; the caller
// (DarwinTun.Wait) doesn't need to distinguish them since it always calls
// ReadPacket() right after anyway, and that already handles "nothing was
// actually there" via ErrQueueEmpty. Returns false only when the kqueue
// itself is no longer usable -- already closed, or the kevent syscall
// failed for a reason other than EINTR -- see its own call site in
// DarwinTun.Wait for why a persistent failure must not be silently
// retried forever (reviewer feedback, XTLS/Xray-core#6580 P2).
func (w *waitKqueue) wait(timeout time.Duration) (ok bool) {
if w.closed.Load() {
return false
}
events := make([]unix.Kevent_t, 1)
ts := unix.NsecToTimespec(timeout.Nanoseconds())
_, err := unix.Kevent(w.fd, nil, events, &ts)
if err != nil {
return errors.Is(err, unix.EINTR)
}
return true
}
// close marks the kqueue as unusable (so any Wait() call that hasn't yet
// entered the kevent syscall bails out instead) and closes the underlying
// fd exactly once, regardless of how many times close is called or
// whether it races a Wait() already inside its kevent syscall (that call
// either completes against the still-open fd or returns an error safely
// -- either way, no other goroutine can be handed this fd number in
// between the atomic flag flip and the actual close, since nothing else
// in this type ever creates a new kqueue with the same field).
func (w *waitKqueue) close() {
w.once.Do(func() {
w.closed.Store(true)
_ = unix.Close(w.fd)
})
} }
var ( var (
@@ -76,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
options: options, options: options,
tunFd: fd, tunFd: fd,
ownsFd: false, ownsFd: false,
waitKq: newWaitKqueue(fd),
}, nil }, nil
} }
@@ -85,7 +179,13 @@ func NewTun(options *Config) (Tun, error) {
return nil, err return nil, err
} }
err = setup(options.Name, options.MTU) gateway, err := selectDarwinGateway(options.Gateway)
if err != nil {
_ = tunFile.Close()
return nil, err
}
err = setup(options.Name, options.MTU, gateway)
if err != nil { if err != nil {
_ = tunFile.Close() _ = tunFile.Close()
return nil, err return nil, err
@@ -96,6 +196,8 @@ func NewTun(options *Config) (Tun, error) {
options: options, options: options,
tunFd: int(tunFile.Fd()), tunFd: int(tunFile.Fd()),
ownsFd: true, ownsFd: true,
waitKq: newWaitKqueue(int(tunFile.Fd())),
gateway: gateway,
}, nil }, nil
} }
@@ -126,6 +228,9 @@ func (t *DarwinTun) Close() error {
_ = t.routeMonitor.Close() _ = t.routeMonitor.Close()
} }
}) })
if t.waitKq != nil {
t.waitKq.close()
}
routeErr := t.unsetSystemRoutes() routeErr := t.unsetSystemRoutes()
if t.ownsFd { if t.ownsFd {
return xerrors.Combine(routeErr, t.tunFile.Close()) return xerrors.Combine(routeErr, t.tunFile.Close())
@@ -234,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
}), nil }), nil
} }
// Wait some cpu cycles // Wait blocks until tunFd is readable (or a short timeout elapses), rather
// than spinning the CPU -- see the waitKq field's own doc comment. A bounded
// timeout (not an indefinite wait) keeps this responsive to a Close() that
// happens to race a call already parked here.
//
// Reviewer feedback (XTLS/Xray-core#6580, P2): the original version
// discarded every error from the underlying kevent syscall. dispatchLoop
// (stack_gvisor_endpoint.go) calls ReadPacket() then Wait() in an
// unconditional tight loop -- if kevent started failing at runtime for a
// persistent reason (not just a benign EINTR), Wait() returning
// immediately every time reintroduces exactly the busy-spin this whole
// change exists to remove, just routed through a failing syscall instead
// of procyield. waitKq.wait's own bool return distinguishes "genuinely
// interrupted, try again" from "this kqueue is unusable now" -- Wait()
// permanently falls back to the sleep path once that happens, rather than
// retrying the same broken kqueue forever.
func (t *DarwinTun) Wait() { func (t *DarwinTun) Wait() {
procyield(1) if t.waitKq != nil && t.waitKq.wait(time.Second) {
return
}
if t.waitKq != nil {
// Persistent kevent failure (not a benign EINTR, and not just
// "the 1s timeout elapsed with nothing to read" -- wait() already
// returned true for both of those cases above). Stop trusting
// this kqueue for the rest of this DarwinTun's lifetime instead of
// re-attempting a syscall that's already shown it won't succeed.
t.waitKq.close()
t.waitKq = nil
}
// Reviewer feedback (XTLS/Xray-core#6580): procyield here is the same
// busy-spin this whole change exists to remove, just gated behind an
// edge case (kqueue setup failing, which practically never happens on
// real Darwin systems, or having just failed permanently above)
// instead of always -- a genuine bounded sleep actually yields the CPU
// instead of being a near-instant scheduler hint that lets the tight
// dispatchLoop caller spin just as hot as before.
time.Sleep(time.Millisecond)
} }
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
@@ -281,24 +420,56 @@ func open(name string) (*os.File, error) {
} }
// setup the interface by name // setup the interface by name
func setup(name string, MTU uint32) error { func setup(name string, MTU uint32, gateway netip.Prefix) error {
if err := setMTU(name, MTU); err != nil { if err := setMTU(name, MTU); err != nil {
return err return err
} }
/* /*
* Darwin routing require tunnel type interface to have local and remote address, to be routable. * Darwin routing require tunnel type interface to have local and remote address, to be routable.
* To simplify inevitable task, assign the interface static ip address, which in current implementation * To simplify inevitable task, assign the interface static ip address.
* is just some random ip from link-local pool, allowing to not bother about existing routing intersection.
*/ */
syntheticIP, _ := netip.ParsePrefix(gateway) if err := setIPAddress(name, gateway); err != nil {
if err := setIPAddress(name, syntheticIP); err != nil {
return err return err
} }
return nil return nil
} }
func selectDarwinGateway(configured []string) (netip.Prefix, error) {
if len(configured) == 0 {
return netip.ParsePrefix(defaultDarwinGateway)
}
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return netip.Prefix{}, xerrors.New("invalid macOS gateway ", value).Base(err)
}
if !prefix.Addr().Is4() {
continue
}
local, ok := nextDarwinLocalIPv4(prefix)
if !ok || !prefix.Contains(local) {
return netip.Prefix{}, xerrors.New("macOS gateway ", value, " must contain at least one usable local IPv4 address after the gateway address")
}
return prefix, nil
}
return netip.Prefix{}, xerrors.New("macOS gateway requires at least one IPv4 prefix")
}
func nextDarwinLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
local4 := gateway.Addr().As4()
for i := len(local4) - 1; i >= 0; i-- {
local4[i]++
if local4[i] != 0 {
return netip.AddrFrom4(local4), true
}
}
return netip.Addr{}, false
}
// setMTU sets MTU on the interface by given name // setMTU sets MTU on the interface by given name
func setMTU(name string, mtu uint32) error { func setMTU(name string, mtu uint32) error {
socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
@@ -344,8 +515,11 @@ func setIPAddress(name string, gateway netip.Prefix) error {
defer unix.Close(socket4) defer unix.Close(socket4)
// assume local ip address is next one from the remote address // assume local ip address is next one from the remote address
local4 := gateway.Addr().As4() local, ok := nextDarwinLocalIPv4(gateway)
local4[3]++ if !ok || !gateway.Contains(local) {
return xerrors.New("macOS gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
}
local4 := local.As4()
// fill the configuration for ipv4 // fill the configuration for ipv4
ifReq4 := ifAliasReq4{ ifReq4 := ifAliasReq4{
@@ -534,7 +708,7 @@ func (t *DarwinTun) setSystemRoutes() error {
return err return err
} }
for _, destination := range routes { for _, destination := range routes {
if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination); err != nil { if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination, t.gateway); err != nil {
_ = t.unsetSystemRoutes() _ = t.unsetSystemRoutes()
return xerrors.New("failed to add system route ", destination).Base(err) return xerrors.New("failed to add system route ", destination).Base(err)
} }
@@ -551,7 +725,7 @@ func (t *DarwinTun) unsetSystemRoutes() error {
} }
for i := len(t.systemRoutes) - 1; i >= 0; i-- { for i := len(t.systemRoutes) - 1; i >= 0; i-- {
destination := t.systemRoutes[i] destination := t.systemRoutes[i]
if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination); err != nil && !errors.Is(err, unix.ESRCH) { if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination, t.gateway); err != nil && !errors.Is(err, unix.ESRCH) {
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err)) errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
} }
} }
@@ -606,7 +780,7 @@ func darwinProtectedDefaultRoutes(ipv4 bool) []netip.Prefix {
return routes return routes
} }
func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix) error { func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Prefix) error {
message := route.RouteMessage{ message := route.RouteMessage{
Type: messageType, Type: messageType,
Version: unix.RTM_VERSION, Version: unix.RTM_VERSION,
@@ -618,11 +792,10 @@ func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Pref
} }
if destination.Addr().Is4() { if destination.Addr().Is4() {
gatewayPrefix := netip.MustParsePrefix(gateway)
message.Addrs = []route.Addr{ message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()}, unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())}, unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gatewayPrefix.Addr().As4()}, unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gateway.Addr().As4()},
} }
} else { } else {
message.Flags &^= unix.RTF_GATEWAY message.Flags &^= unix.RTF_GATEWAY
+233
View File
@@ -0,0 +1,233 @@
//go:build darwin
package tun
import (
"sync"
"testing"
"time"
"golang.org/x/sys/unix"
)
func TestSelectDarwinGatewayDefault(t *testing.T) {
gateway, err := selectDarwinGateway(nil)
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != defaultDarwinGateway {
t.Fatalf("unexpected default gateway: got %s, want %s", got, defaultDarwinGateway)
}
}
func TestSelectDarwinGatewayConfiguredIPv4(t *testing.T) {
gateway, err := selectDarwinGateway([]string{"198.18.0.1/15"})
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != "198.18.0.1/15" {
t.Fatalf("unexpected gateway: got %s", got)
}
}
func TestSelectDarwinGatewaySkipsIPv6(t *testing.T) {
gateway, err := selectDarwinGateway([]string{"fc00::1/64", "198.18.0.1/15"})
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != "198.18.0.1/15" {
t.Fatalf("unexpected gateway: got %s", got)
}
}
func TestSelectDarwinGatewayRequiresIPv4(t *testing.T) {
if _, err := selectDarwinGateway([]string{"fc00::1/64"}); err == nil {
t.Fatal("expected error")
}
}
func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
if _, err := selectDarwinGateway([]string{"198.18.0.1/32"}); err == nil {
t.Fatal("expected error")
}
}
// newTestSocketpair returns a connected AF_UNIX/SOCK_DGRAM pair -- a real
// fd DarwinTun.Wait's kqueue can register EVFILT_READ against, without
// needing an actual utun interface (which requires root/network
// entitlements this test environment doesn't have). Datagram sockets
// (unlike pipes) support both "write makes readable" and "close makes
// readable" the same way a tun fd's read-readiness behaves.
func newTestSocketpair(t *testing.T) (a, b int) {
t.Helper()
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
if err != nil {
t.Fatalf("socketpair: %v", err)
}
t.Cleanup(func() {
_ = unix.Close(fds[0])
_ = unix.Close(fds[1])
})
return fds[0], fds[1]
}
// Reviewer feedback, XTLS/Xray-core#6580: "blocking with no data" case --
// wait() must not return before the timeout when nothing is written.
func TestWaitKqueueBlocksWithNoData(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
start := time.Now()
ok := kq.wait(150 * time.Millisecond)
elapsed := time.Since(start)
if !ok {
t.Fatal("wait() returned false on a healthy kqueue with a plain timeout")
}
if elapsed < 100*time.Millisecond {
t.Fatalf("wait() returned after only %v, expected it to block close to the 150ms timeout", elapsed)
}
}
// Reviewer feedback: "wake up with a readable fd" case.
func TestWaitKqueueWakesOnReadable(t *testing.T) {
a, b := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
done := make(chan bool, 1)
go func() {
done <- kq.wait(5 * time.Second)
}()
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
if _, err := unix.Write(b, []byte{0x1}); err != nil {
t.Fatalf("write: %v", err)
}
select {
case ok := <-done:
if !ok {
t.Fatal("wait() returned false after the fd became readable")
}
case <-time.After(2 * time.Second):
t.Fatal("wait() did not wake up within 2s of the fd becoming readable")
}
}
// Reviewer feedback: "timeout" case, explicitly (distinct from the
// no-data test above, which also checks blocking duration -- this one
// only checks the return value).
func TestWaitKqueueTimesOut(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
if !kq.wait(50 * time.Millisecond) {
t.Fatal("wait() returned false on a plain timeout with no error condition")
}
}
// Reviewer feedback: "Close() wakes a blocked wait" case, and the
// no-double-close/no-fd-reuse concern (P1) -- close() while wait() is
// parked in its syscall must not panic, must not leave wait() hung, and a
// second close() call (from a caller that, say, calls Close() twice on
// the same DarwinTun) must be safe.
func TestWaitKqueueCloseDuringWaitIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
started := make(chan struct{})
done := make(chan bool, 1)
go func() {
close(started)
done <- kq.wait(5 * time.Second)
}()
<-started
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
kq.close()
kq.close() // double-close must be idempotent, not panic or double-free the fd
select {
case <-done:
// Either true (the close-of-the-underlying-fd unblocked kevent, a
// real kqueue behavior) or false (wait() observed the closed flag
// first) is acceptable -- what matters is that it returned at all,
// promptly, without hanging or crashing.
case <-time.After(2 * time.Second):
t.Fatal("wait() did not return within 2s of close() being called")
}
// A wait() call *after* close() must return false immediately (the
// closed-flag fast path), not attempt a syscall against the
// already-closed (and potentially since-reused, on a real system) fd
// number.
if kq.wait(time.Second) {
t.Fatal("wait() returned true after close() -- should short-circuit via the closed flag")
}
}
// Reviewer feedback: "multiple/concurrent close guard" case -- many
// goroutines calling close() concurrently must close the underlying fd
// exactly once.
func TestWaitKqueueConcurrentCloseIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
var wg sync.WaitGroup
for range 20 {
wg.Add(1)
go func() {
defer wg.Done()
kq.close()
}()
}
wg.Wait()
if !kq.closed.Load() {
t.Fatal("closed flag not set after concurrent close() calls")
}
}
// Reviewer feedback: "kevent runtime failure without spinning" case (P2).
// Simulates a kqueue that has gone bad (closed out from under it, as if a
// concurrent/erroneous close happened) and confirms wait() reports it as
// unusable (false) rather than silently returning true forever, which is
// what DarwinTun.Wait relies on to permanently fall back to the sleep
// path instead of re-entering a failing syscall on every dispatchLoop
// iteration.
func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
// Close the underlying kqueue fd directly (bypassing kq.close(), which
// would also set the closed flag) to simulate the fd going bad for a
// reason other than this type's own close() -- e.g. some other code
// path in the process closing it, or the kernel invalidating it.
_ = unix.Close(kq.fd)
for i := 0; i < 5; i++ {
if kq.wait(50 * time.Millisecond) {
t.Fatalf("wait() call %d returned true against a closed underlying fd -- should report failure, not spin", i)
}
}
}
+729 -25
View File
@@ -3,28 +3,60 @@
package tun package tun
import ( import (
"context"
"errors" "errors"
"net" "net"
_ "unsafe" "net/netip"
"os"
"slices"
"sync"
"unsafe"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/stack"
"golang.org/x/net/route"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
xerrors "github.com/xtls/xray-core/common/errors"
) )
const tunHeaderSize = 4 const (
tunHeaderSize = 4
defaultFreeBSDGateway = "169.254.10.1/30"
// escapeFib is the routing table outbound sockets are switched to so
// their traffic bypasses the TUN routes installed in the default FIB
// (FreeBSD's substitute for the per-socket interface binding other
// platforms use). Requires the boot tunable net.fibs >= 2.
escapeFib = 1
)
//go:linkname procyield runtime.procyield //go:linkname procyield runtime.procyield
func procyield(cycles uint32) func procyield(cycles uint32)
type FreeBSDTun struct { type FreeBSDTun struct {
device tun.Device device tun.Device
mtu uint32 options *Config
tunIndex int
autoInterface bool
systemRoutes []netip.Prefix
escapeMu sync.Mutex
escapeRoutes []escapeRoute
routeMonitor *os.File
routeMonitorOnce sync.Once
}
// escapeRoute remembers one route written into the escape FIB, in the exact
// shape needed to delete it again. A zero gateway means an interface route.
type escapeRoute struct {
prefix netip.Prefix
ifIndex int
gateway netip.Addr
} }
var ( var (
@@ -34,20 +66,172 @@ var (
// NewTun builds new tun interface handler // NewTun builds new tun interface handler
func NewTun(options *Config) (Tun, error) { func NewTun(options *Config) (Tun, error) {
gateway, local, err := selectFreeBSDGateway(options.Gateway)
if err != nil {
return nil, err
}
// net.fibs is a boot-time constant, so validate the escape routing table
// before the shared handler registers a dialer controller that would
// otherwise steer every outbound socket into a table that was never set up.
if options.AutoOutboundsInterface != "" {
if err := checkEscapeFib(); err != nil {
return nil, err
}
}
tunDev, err := tun.CreateTUN(options.Name, int(options.MTU)) tunDev, err := tun.CreateTUN(options.Name, int(options.MTU))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil name, err := tunDev.Name()
if err != nil {
_ = tunDev.Close()
return nil, err
}
// From here the interface exists in the kernel; the wireguard library does
// not remove it on Close, so every failure path must destroy it too or the
// next start fails with "interface already exists".
iface, err := net.InterfaceByName(name)
if err != nil {
_ = tunDev.Close()
destroyInterface(name)
return nil, err
}
if err := setIPAddress(name, gateway, local, iface.Index); err != nil {
_ = tunDev.Close()
destroyInterface(name)
return nil, err
}
return &FreeBSDTun{
device: tunDev,
options: options,
tunIndex: iface.Index,
autoInterface: options.AutoOutboundsInterface != "",
}, nil
}
// selectFreeBSDGateway picks the first IPv4 prefix from the configured gateway
// list and the local address derived from it (the darwin semantics: the
// gateway is the remote side of the point-to-point pair, the local address is
// the next one after it), falling back to the same link-local default.
func selectFreeBSDGateway(configured []string) (netip.Prefix, netip.Addr, error) {
gateway := netip.MustParsePrefix(defaultFreeBSDGateway)
if len(configured) > 0 {
found := false
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return netip.Prefix{}, netip.Addr{}, xerrors.New("invalid FreeBSD gateway ", value).Base(err)
}
if prefix.Addr().Is4() {
gateway, found = prefix, true
break
}
}
if !found {
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway requires at least one IPv4 prefix")
}
}
local, ok := nextLocalIPv4(gateway)
if !ok || !gateway.Contains(local) {
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
}
return gateway, local, nil
}
func nextLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
local4 := gateway.Addr().As4()
for i := len(local4) - 1; i >= 0; i-- {
local4[i]++
if local4[i] != 0 {
return netip.AddrFrom4(local4), true
}
}
return netip.Addr{}, false
} }
func (t *FreeBSDTun) Start() error { func (t *FreeBSDTun) Start() error {
if err := t.setSystemRoutes(); err != nil {
return err
}
// Gate on this instance's own option, not the package-global updater,
// which a previously-removed inbound may have left set. checkEscapeFib
// already ran in NewTun, before the dialer controller was registered.
if t.autoInterface {
if err := t.syncEscapeFib(); err != nil {
_ = t.unsetSystemRoutes()
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
t.unsetEscapeFib()
_ = t.unsetSystemRoutes()
return err
}
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
go t.monitorRouteChanges()
}
return nil return nil
} }
// monitorRouteChanges refreshes the outbound interface and the escape FIB
// mirror whenever the system routing table changes.
func (t *FreeBSDTun) monitorRouteChanges() {
buffer := make([]byte, 64*1024)
for {
if _, err := t.routeMonitor.Read(buffer); err != nil {
if !errors.Is(err, os.ErrClosed) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes")
}
return
}
if updater != nil {
updater.Update()
}
if err := t.syncEscapeFib(); err != nil {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to refresh escape routes")
}
}
}
func (t *FreeBSDTun) Close() error { func (t *FreeBSDTun) Close() error {
return t.device.Close() t.routeMonitorOnce.Do(func() {
if t.routeMonitor != nil {
_ = t.routeMonitor.Close()
}
})
t.unsetEscapeFib()
routeErr := t.unsetSystemRoutes()
name, nameErr := t.Name()
closeErr := t.device.Close()
// The wireguard tun device does not tear the interface down on FreeBSD,
// so an unclean shutdown would leave utun<n> behind and the next start
// would fail with "interface already exists"; destroy it explicitly.
if nameErr == nil {
destroyInterface(name)
}
return xerrors.Combine(routeErr, closeErr)
}
func destroyInterface(name string) {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return
}
defer unix.Close(fd)
// struct ifreq: 16-byte name + a 16-byte union (SIOCIFDESTROY's encoded
// length is 32 bytes on amd64, and the kernel copies in all of it).
var req struct {
Name [unix.IFNAMSIZ]byte
_ [16]byte
}
copy(req.Name[:], name)
_ = ioctlPtr(fd, unix.SIOCIFDESTROY, unsafe.Pointer(&req))
} }
func (t *FreeBSDTun) Name() (string, error) { func (t *FreeBSDTun) Name() (string, error) {
@@ -55,21 +239,13 @@ func (t *FreeBSDTun) Name() (string, error) {
} }
func (t *FreeBSDTun) Index() (int, error) { func (t *FreeBSDTun) Index() (int, error) {
name, err := t.Name() return t.tunIndex, nil
if err != nil {
return 0, err
}
iface, err := net.InterfaceByName(name)
if err != nil {
return 0, err
}
return iface.Index, nil
} }
// WritePacket implements GVisorDevice method to write one packet to the tun device // WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool // request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize) b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
defer b.Release() defer b.Release()
// prepare Unix specific packet header // prepare Unix specific packet header
@@ -104,7 +280,7 @@ func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// which will make the stack call Wait which should implement desired push-back // which will make the stack call Wait which should implement desired push-back
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) { func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool // request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize) b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
// read the bytes to the interface file // read the bytes to the interface file
n, err := b.ReadFrom(t.device.File()) n, err := b.ReadFrom(t.device.File())
@@ -141,23 +317,551 @@ func (t *FreeBSDTun) Wait() {
} }
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) { func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
} }
func setinterface(network, address string, fd uintptr, iface *net.Interface) error { const (
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
)
// ifAliasReq4 is struct in_aliasreq from netinet/in_var.h in the 64-byte
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs).
type ifAliasReq4 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet4
Dstaddr unix.RawSockaddrInet4
Mask unix.RawSockaddrInet4
}
// ifAliasReq6 is struct in6_aliasreq from netinet6/in6_var.h. The trailing
// Vhid field matters: unix.SIOCAIFADDR_IN6 is not in x/sys/unix, so
// siocaifaddrIn6 is derived from this struct's size, and the kernel only
// accepts the ioctl whose encoded length matches the real struct.
type ifAliasReq6 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet6
Dstaddr unix.RawSockaddrInet6
Prefixmask unix.RawSockaddrInet6
Flags int32
Lifetime addrLifetime6
Vhid int32
}
// addrLifetime6 is struct in6_addrlifetime (time_t is int64 on freebsd/amd64).
type addrLifetime6 struct {
Expire int64
Preferred int64
Vltime uint32
Pltime uint32
}
// SIOCAIFADDR_IN6 = _IOW('i', 27, struct in6_aliasreq); x/sys/unix does not
// carry the netinet6 ioctls, so encode it from the struct size like the
// header macro does.
const siocaifaddrIn6 = 0x80000000 | (uintptr(unsafe.Sizeof(ifAliasReq6{})) << 16) | ('i' << 8) | 27
// setIPAddress assigns the local/remote point-to-point IPv4 pair and a
// link-local IPv6 address to the interface, required for the routing to work
// (same scheme as the darwin implementation: local address is the one right
// after the gateway address).
func setIPAddress(name string, gateway netip.Prefix, local netip.Addr, ifIndex int) error {
socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socket4)
local4 := local.As4()
ifReq4 := ifAliasReq4{
Addr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: local4,
},
Dstaddr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: gateway.Addr().As4(),
},
Mask: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: prefixMask4(gateway.Bits()),
},
}
copy(ifReq4.Name[:], name)
if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil {
return os.NewSyscallError("SIOCAIFADDR", err)
}
socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socket6)
// link-local ipv6 address with suffix from ipv4, enough for v6 interface
// routes to be attachable (darwin parity); a link-local address needs its
// scope, which for FreeBSD ioctls is the interface index
local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]})
ifReq6 := ifAliasReq6{
Addr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: local6.As16(),
Scope_id: uint32(ifIndex),
},
Prefixmask: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: prefixMask6(64),
},
Flags: IN6_IFF_NODAD,
Lifetime: addrLifetime6{
Vltime: ND6_INFINITE_LIFETIME,
Pltime: ND6_INFINITE_LIFETIME,
},
}
copy(ifReq6.Name[:], name)
if err = ioctlPtr(socket6, uint(siocaifaddrIn6), unsafe.Pointer(&ifReq6)); err != nil {
// non-fatal: FreeBSD auto-configures a link-local address on UP
// interfaces, which is all the v6 interface routes need
xerrors.LogInfoInner(context.Background(), os.NewSyscallError("SIOCAIFADDR_IN6", err), "[tun] failed to assign the IPv6 link-local address")
}
return nil return nil
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
if fixedName == "" { _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
return nil, errors.New("automatic outbound interface selection is not supported on this platform") if errno != 0 {
return errno
} }
iface, err := net.InterfaceByName(fixedName) return nil
}
func prefixMask4(bits int) [4]byte {
var mask [4]byte
copy(mask[:], net.CIDRMask(bits, 32))
return mask
}
func prefixMask6(bits int) [16]byte {
var mask [16]byte
copy(mask[:], net.CIDRMask(bits, 128))
return mask
}
// setinterface is the per-socket half of autoOutboundsInterface. FreeBSD has
// no SO_BINDTODEVICE/IP_BOUND_IF equivalent, so the socket is pointed at the
// escape FIB instead, where Start() mirrors the physical default route; the
// iface argument is resolved by the shared updater but unused here (the escape
// is table-based, not a per-socket interface bind). checkEscapeFib in NewTun
// guarantees the FIB exists before this can run.
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SETFIB, escapeFib)
}
func (t *FreeBSDTun) setSystemRoutes() error {
routes, err := buildSystemRoutes(t.options.AutoSystemRoutingTable)
if err != nil {
return err
}
// Route through the interface, not a gateway: the tun(4) device is a
// broadcast interface here, so its point-to-point peer address doubles as
// the subnet broadcast and the kernel refuses to route to it (EACCES).
// Interface routes sidestep the gateway entirely (what wg-quick does on
// FreeBSD).
for _, destination := range routes {
if err := execRoute(-1, unix.RTM_ADD, t.tunIndex, destination, netip.Addr{}); err != nil {
_ = t.unsetSystemRoutes()
return xerrors.New("failed to add system route ", destination).Base(err)
}
t.systemRoutes = append(t.systemRoutes, destination)
}
return nil
}
func (t *FreeBSDTun) unsetSystemRoutes() error {
var errs []error
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
destination := t.systemRoutes[i]
if err := execRoute(-1, unix.RTM_DELETE, t.tunIndex, destination, netip.Addr{}); err != nil && !errors.Is(err, unix.ESRCH) {
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
}
}
t.systemRoutes = nil
return xerrors.Combine(errs...)
}
func buildSystemRoutes(configured []string) ([]netip.Prefix, error) {
routes := make([]netip.Prefix, 0, len(configured))
seen := make(map[netip.Prefix]struct{})
appendRoute := func(prefix netip.Prefix) {
prefix = prefix.Masked()
if _, found := seen[prefix]; found {
return
}
seen[prefix] = struct{}{}
routes = append(routes, prefix)
}
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return nil, xerrors.New("invalid system route ", value).Base(err)
}
if prefix.Bits() == 0 {
for _, protected := range protectedDefaultRoutes(prefix.Addr().Is4()) {
appendRoute(protected)
}
continue
}
appendRoute(prefix)
}
return routes, nil
}
// protectedDefaultRoutes splits a full default route into eight more-specific
// prefixes covering everything but the zero /8, so the system's real default
// route stays in place for outbound interface discovery (darwin parity).
func protectedDefaultRoutes(ipv4 bool) []netip.Prefix {
routes := make([]netip.Prefix, 0, 8)
for i := 0; i < 8; i++ {
if ipv4 {
var address [4]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i))
} else {
var address [16]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i))
}
}
return routes
}
// execRoute writes one RTM message to a routing socket. fib >= 0 targets that
// routing table via SO_SETFIB on the routing socket (what route(8) -fib
// does); fib -1 leaves the process default table. An invalid gateway produces
// an interface route pinned to interfaceIndex instead of a gateway route.
func execRoute(fib int, messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Addr) error {
message := route.RouteMessage{
Type: messageType,
Version: unix.RTM_VERSION,
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
Seq: 1,
}
if messageType == unix.RTM_ADD {
message.Flags |= unix.RTF_UP
}
var gatewayAddr route.Addr
switch {
case !gateway.IsValid():
message.Flags &^= unix.RTF_GATEWAY
message.Index = interfaceIndex
gatewayAddr = &route.LinkAddr{Index: interfaceIndex}
case gateway.Is4():
gatewayAddr = &route.Inet4Addr{IP: gateway.As4()}
default:
gatewayAddr = &route.Inet6Addr{IP: gateway.As16()}
}
if destination.Addr().Is4() {
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
unix.RTAX_GATEWAY: gatewayAddr,
}
} else {
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())},
unix.RTAX_GATEWAY: gatewayAddr,
}
}
request, err := message.Marshal()
if err != nil {
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
defer unix.Close(fd)
if fib >= 0 {
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SETFIB, fib); err != nil {
return err
}
}
_, err = unix.Write(fd, request)
return err
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName != "" {
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
physical, err := physicalDefaultRoutes(tunIndex, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if iface.Index == tunIndex { for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
return nil, errors.New("outbound interface cannot be the TUN interface") for _, route := range physical {
if route.family == family {
return route.iface, nil
}
}
}
return nil, errors.New("default route not found")
}
// physicalRoute describes one physical default route: the interface it
// leaves through, its gateway, and the connected prefix that makes the
// gateway resolvable.
type physicalRoute struct {
family int
iface *net.Interface
gateway netip.Addr
connected netip.Prefix
}
// physicalDefaultRoutes scans the default routing table for default routes
// that do not go through the TUN interface, at most one per address family
// (the first usable one wins, matching the darwin implementation's
// preference order). A non-zero onlyIndex restricts the scan to that
// interface, for the fixed-name mode of autoOutboundsInterface.
func physicalDefaultRoutes(tunIndex int, onlyIndex int) ([]physicalRoute, error) {
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return nil, err
}
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
if err != nil {
return nil, err
}
found := make([]physicalRoute, 0, 2)
seen := make(map[int]bool)
for _, message := range messages {
routeMessage, ok := message.(*route.RouteMessage)
if !ok || routeMessage.Index == tunIndex {
continue
}
if onlyIndex != 0 && routeMessage.Index != onlyIndex {
continue
}
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
}
family, ok := defaultRouteFamily(routeMessage)
if !ok || seen[family] {
continue
}
iface, err := usableInterface(routeMessage.Index)
if err != nil {
continue
}
gatewayAddr, ok := routeAddrToNetip(routeMessage.Addrs[unix.RTAX_GATEWAY])
if !ok {
continue
}
connected, err := connectedPrefix(iface, gatewayAddr)
if err != nil {
continue
}
seen[family] = true
found = append(found, physicalRoute{
family: family,
iface: iface,
gateway: gatewayAddr,
connected: connected,
})
}
if len(found) == 0 {
return nil, errors.New("default route not found")
}
return found, nil
}
// defaultRouteFamily reports the address family of a RIB message that
// represents a true default route (unspecified destination, zero mask).
func defaultRouteFamily(message *route.RouteMessage) (int, bool) {
if len(message.Addrs) <= unix.RTAX_NETMASK {
return 0, false
}
switch destination := message.Addrs[unix.RTAX_DST].(type) {
case *route.Inet4Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !ok || destination.IP != netip.IPv4Unspecified().As4() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET, ones == 0 && bits == 32
case *route.Inet6Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr)
if !ok || destination.IP != netip.IPv6Unspecified().As16() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET6, ones == 0 && bits == 128
default:
return 0, false
}
}
func usableInterface(index int) (*net.Interface, error) {
iface, err := net.InterfaceByIndex(index)
if err != nil {
return nil, err
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
return nil, errors.New("default route interface is not usable")
} }
return iface, nil return iface, nil
} }
func routeAddrToNetip(addr route.Addr) (netip.Addr, bool) {
switch typed := addr.(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(typed.IP), true
case *route.Inet6Addr:
return netip.AddrFrom16(typed.IP), true
default:
return netip.Addr{}, false
}
}
// checkEscapeFib verifies the system can host the escape routing table at
// all: FIBs are a boot-time resource on FreeBSD.
func checkEscapeFib() error {
fibs, err := unix.SysctlUint32("net.fibs")
if err != nil {
return xerrors.New("failed to read net.fibs").Base(err)
}
if fibs < 2 {
return errors.New("automatic outbound interface on FreeBSD needs a second routing table: add net.fibs=2 to /boot/loader.conf and reboot")
}
current, err := unix.SysctlUint32("net.my_fibnum")
if err != nil {
return xerrors.New("failed to read net.my_fibnum").Base(err)
}
if current == escapeFib {
return errors.New("xray runs inside routing table 1, which is reserved as the escape table; start it in another FIB")
}
return nil
}
// syncEscapeFib mirrors the physical default routes (and the connected
// prefixes their gateways resolve through) into the escape FIB, replacing
// whatever mirror a previous call installed. On discovery failure the old
// mirror is kept, since a stale escape route beats none during a transient
// route flap.
func (t *FreeBSDTun) syncEscapeFib() error {
var onlyIndex int
if t.options.AutoOutboundsInterface != "" && updater != nil {
if iface := updater.Get(); iface != nil {
onlyIndex = iface.Index
}
}
physical, err := physicalDefaultRoutes(t.tunIndex, onlyIndex)
if err != nil {
return err
}
desired := make([]escapeRoute, 0, 2*len(physical))
for _, p := range physical {
desired = append(desired,
escapeRoute{prefix: p.connected, ifIndex: p.iface.Index},
escapeRoute{prefix: defaultPrefix(p.family), ifIndex: p.iface.Index, gateway: p.gateway},
)
}
t.escapeMu.Lock()
defer t.escapeMu.Unlock()
// The route monitor hears our own escape FIB writes too; rewriting an
// unchanged mirror on every wake-up would ping-pong forever.
if slices.Equal(t.escapeRoutes, desired) {
return nil
}
t.unsetEscapeFibLocked()
for _, entry := range desired {
err := execRoute(escapeFib, unix.RTM_ADD, entry.ifIndex, entry.prefix, entry.gateway)
if err != nil && !errors.Is(err, unix.EEXIST) {
return xerrors.New("failed to add escape route ", entry.prefix).Base(err)
}
t.escapeRoutes = append(t.escapeRoutes, entry)
}
return nil
}
func (t *FreeBSDTun) unsetEscapeFib() {
t.escapeMu.Lock()
defer t.escapeMu.Unlock()
t.unsetEscapeFibLocked()
}
func (t *FreeBSDTun) unsetEscapeFibLocked() {
for i := len(t.escapeRoutes) - 1; i >= 0; i-- {
entry := t.escapeRoutes[i]
err := execRoute(escapeFib, unix.RTM_DELETE, entry.ifIndex, entry.prefix, entry.gateway)
if err != nil && !errors.Is(err, unix.ESRCH) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to delete escape route ", entry.prefix)
}
}
t.escapeRoutes = nil
}
func defaultPrefix(family int) netip.Prefix {
if family == unix.AF_INET {
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
}
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
}
// connectedPrefix finds the interface's address prefix containing the
// gateway, which the escape FIB needs as an interface route so the mirrored
// default route's gateway is resolvable there.
func connectedPrefix(iface *net.Interface, gateway netip.Addr) (netip.Prefix, error) {
addrs, err := iface.Addrs()
if err != nil {
return netip.Prefix{}, err
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip, ok := netip.AddrFromSlice(ipNet.IP)
if !ok {
continue
}
ip = ip.Unmap()
ones, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(ip, ones).Masked()
if prefix.Contains(gateway.WithZone("").Unmap()) {
return prefix, nil
}
}
return netip.Prefix{}, errors.New("no connected prefix contains the gateway")
}
+150
View File
@@ -0,0 +1,150 @@
//go:build freebsd
package tun
import (
"net/netip"
"testing"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
func TestSelectFreeBSDGatewayDefault(t *testing.T) {
gateway, local, err := selectFreeBSDGateway(nil)
if err != nil {
t.Fatal(err)
}
if gateway != netip.MustParsePrefix(defaultFreeBSDGateway) {
t.Fatal("expected default gateway, got ", gateway)
}
if local != netip.MustParseAddr("169.254.10.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestSelectFreeBSDGatewayPicksFirstIPv4(t *testing.T) {
gateway, local, err := selectFreeBSDGateway([]string{"fd00::1/64", "10.0.0.1/30", "10.9.9.9/24"})
if err != nil {
t.Fatal(err)
}
if gateway != netip.MustParsePrefix("10.0.0.1/30") {
t.Fatal("wrong gateway: ", gateway)
}
if local != netip.MustParseAddr("10.0.0.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestSelectFreeBSDGatewayRequiresIPv4(t *testing.T) {
if _, _, err := selectFreeBSDGateway([]string{"fd00::1/64"}); err == nil {
t.Fatal("expected error when no IPv4 gateway is configured")
}
}
func TestSelectFreeBSDGatewayRejectsGarbage(t *testing.T) {
if _, _, err := selectFreeBSDGateway([]string{"not-a-prefix"}); err == nil {
t.Fatal("expected error for invalid gateway")
}
}
func TestSelectFreeBSDGatewayRejectsFullPrefix(t *testing.T) {
// 10.0.0.255/30: the "next" local address falls outside the prefix
if _, _, err := selectFreeBSDGateway([]string{"10.0.0.255/30"}); err == nil {
t.Fatal("expected error when no usable local address follows the gateway")
}
}
func TestNextLocalIPv4(t *testing.T) {
local, ok := nextLocalIPv4(netip.MustParsePrefix("169.254.10.1/30"))
if !ok || local != netip.MustParseAddr("169.254.10.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestBuildSystemRoutesSplitsDefault(t *testing.T) {
routes, err := buildSystemRoutes([]string{"0.0.0.0/0"})
if err != nil {
t.Fatal(err)
}
expected := []string{
"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1",
}
if len(routes) != len(expected) {
t.Fatal("expected ", len(expected), " routes, got ", routes)
}
for i, want := range expected {
if routes[i] != netip.MustParsePrefix(want) {
t.Fatal("route ", i, ": expected ", want, ", got ", routes[i])
}
}
}
func TestBuildSystemRoutesSplitsDefaultIPv6(t *testing.T) {
routes, err := buildSystemRoutes([]string{"::/0"})
if err != nil {
t.Fatal(err)
}
if len(routes) != 8 || routes[7] != netip.MustParsePrefix("8000::/1") {
t.Fatal("unexpected v6 split: ", routes)
}
}
func TestBuildSystemRoutesDeduplicates(t *testing.T) {
routes, err := buildSystemRoutes([]string{"10.0.0.0/8", "10.1.2.3/8", "0.0.0.0/0", "0.0.0.0/0"})
if err != nil {
t.Fatal(err)
}
if len(routes) != 9 { // 10.0.0.0/8 once + 8 splits once
t.Fatal("expected 9 routes, got ", routes)
}
}
func TestBuildSystemRoutesRejectsGarbage(t *testing.T) {
if _, err := buildSystemRoutes([]string{"10.0.0.0/33"}); err == nil {
t.Fatal("expected error for invalid route")
}
}
func routeMessage(dst, mask route.Addr) *route.RouteMessage {
addrs := make([]route.Addr, unix.RTAX_NETMASK+1)
addrs[unix.RTAX_DST] = dst
addrs[unix.RTAX_NETMASK] = mask
return &route.RouteMessage{Addrs: addrs}
}
func TestDefaultRouteFamilyMatchesIPv4Default(t *testing.T) {
family, ok := defaultRouteFamily(routeMessage(&route.Inet4Addr{}, &route.Inet4Addr{}))
if !ok || family != unix.AF_INET {
t.Fatal("expected IPv4 default route match")
}
}
func TestDefaultRouteFamilyMatchesIPv6Default(t *testing.T) {
family, ok := defaultRouteFamily(routeMessage(&route.Inet6Addr{}, &route.Inet6Addr{}))
if !ok || family != unix.AF_INET6 {
t.Fatal("expected IPv6 default route match")
}
}
func TestDefaultRouteFamilyRejectsNonDefault(t *testing.T) {
if _, ok := defaultRouteFamily(routeMessage(
&route.Inet4Addr{IP: [4]byte{10, 0, 0, 0}},
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
)); ok {
t.Fatal("non-default destination must not match")
}
if _, ok := defaultRouteFamily(routeMessage(
&route.Inet4Addr{},
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
)); ok {
t.Fatal("non-zero mask must not match")
}
}
func TestDefaultRouteFamilyRejectsShortAddrs(t *testing.T) {
if _, ok := defaultRouteFamily(&route.RouteMessage{}); ok {
t.Fatal("message without addresses must not match")
}
}
+85 -31
View File
@@ -26,9 +26,10 @@ type LinuxTun struct {
options *Config options *Config
ownsTun bool ownsTun bool
systemRoutes []netlink.Route interfaceAddresses []netlink.Addr
routeMonitorStop chan struct{} systemRoutes []netlink.Route
routeMonitorOnce sync.Once routeMonitorStop chan struct{}
routeMonitorOnce sync.Once
} }
// LinuxTun implements Tun // LinuxTun implements Tun
@@ -172,7 +173,14 @@ func (t *LinuxTun) Start() error {
return err return err
} }
if err := t.setInterfaceAddresses(); err != nil {
_ = netlink.LinkSetDown(t.tunLink)
return err
}
if err := t.setSystemRoutes(); err != nil { if err := t.setSystemRoutes(); err != nil {
_ = t.unsetInterfaceAddresses()
_ = netlink.LinkSetDown(t.tunLink)
return err return err
} }
@@ -193,6 +201,7 @@ func (t *LinuxTun) Close() error {
}) })
_ = t.unsetSystemRoutes() _ = t.unsetSystemRoutes()
_ = t.unsetInterfaceAddresses()
if t.ownsTun { if t.ownsTun {
_ = netlink.LinkSetDown(t.tunLink) _ = netlink.LinkSetDown(t.tunLink)
@@ -223,6 +232,37 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
return unix.BindToDevice(int(fd), iface.Name) return unix.BindToDevice(int(fd), iface.Name)
} }
func (t *LinuxTun) setInterfaceAddresses() error {
if len(t.options.Gateway) == 0 {
return nil
}
for _, address := range t.options.Gateway {
addr, err := netlink.ParseAddr(address)
if err != nil {
_ = t.unsetInterfaceAddresses()
return errors.New("invalid interface address ", address).Base(err)
}
if err := netlink.AddrAdd(t.tunLink, addr); err != nil {
_ = t.unsetInterfaceAddresses()
return errors.New("failed to add interface address ", address).Base(err)
}
t.interfaceAddresses = append(t.interfaceAddresses, *addr)
}
return nil
}
func (t *LinuxTun) unsetInterfaceAddresses() error {
var errs []error
for i := len(t.interfaceAddresses) - 1; i >= 0; i-- {
address := t.interfaceAddresses[i]
if err := netlink.AddrDel(t.tunLink, &address); err != nil {
errs = append(errs, errors.New("failed to delete interface address ", address.String()).Base(err))
}
}
t.interfaceAddresses = nil
return errors.Combine(errs...)
}
func (t *LinuxTun) setSystemRoutes() error { func (t *LinuxTun) setSystemRoutes() error {
if len(t.options.AutoSystemRoutingTable) == 0 { if len(t.options.AutoSystemRoutingTable) == 0 {
return nil return nil
@@ -308,36 +348,37 @@ func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, erro
return iface, nil return iface, nil
} }
probeIPs := []net.IP{ for _, family := range []int{
net.ParseIP("8.8.8.8"), netlink.FAMILY_V4,
net.ParseIP("2001:4860:4860::8888"), netlink.FAMILY_V6,
} {
iface, err := findDefaultInterface(family, tunIndex)
if err == nil {
return iface, nil
}
} }
for _, ip := range probeIPs { return nil, errors.New("no usable outbound interface found")
routes, err := netlink.RouteGet(ip) }
if err != nil || len(routes) == 0 {
continue func findDefaultInterface(family int, tunIndex int) (*net.Interface, error) {
} routes, err := netlink.RouteList(nil, family)
route := routes[0] if err != nil {
if route.LinkIndex == tunIndex { return nil, err
continue }
var selected *net.Interface
selectedMetric := -1
for _, route := range routes {
if route.Dst != nil {
ones, _ := route.Dst.Mask.Size()
if ones != 0 {
continue
}
} }
link, err := netlink.LinkByIndex(route.LinkIndex) if route.LinkIndex == 0 || route.LinkIndex == tunIndex {
if err != nil {
continue
}
attrs := link.Attrs()
if attrs.Flags&net.FlagUp == 0 {
continue
}
operState := attrs.OperState
if operState != netlink.OperUp && operState != netlink.OperUnknown {
continue
}
if route.Src == nil || route.Src.IsLoopback() || route.Src.IsLinkLocalUnicast() {
continue continue
} }
@@ -345,8 +386,21 @@ func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, erro
if err != nil { if err != nil {
continue continue
} }
return iface, nil
if iface.Flags&net.FlagUp == 0 ||
iface.Flags&net.FlagLoopback != 0 {
continue
}
if selected == nil || route.Priority < selectedMetric {
selected = iface
selectedMetric = route.Priority
}
} }
return nil, errors.New("no usable outbound interface found") if selected == nil {
return nil, errors.New("physical default route not found")
}
return selected, nil
} }
+148 -140
View File
@@ -3,14 +3,14 @@
package tun package tun
import ( import (
"context"
"crypto/md5" "crypto/md5"
"encoding/binary" "encoding/binary"
go_errors "errors" go_errors "errors"
"net" "net"
"net/netip" "net/netip"
"sort"
"strings"
"sync" "sync"
"time"
"unsafe" "unsafe"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -31,13 +31,14 @@ func procyield(cycles uint32)
type WindowsTun struct { type WindowsTun struct {
sync.RWMutex sync.RWMutex
options *Config options *Config
adapter *wintun.Adapter adapter *wintun.Adapter
session wintun.Session session wintun.Session
readWait windows.Handle readWait windows.Handle
luid winipcfg.LUID luid winipcfg.LUID
changeCallback winipcfg.ChangeCallback cbr winipcfg.ChangeCallback
closed bool cbi winipcfg.ChangeCallback
closed bool
} }
// WindowsTun implements Tun // WindowsTun implements Tun
@@ -50,7 +51,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
// interface with the same name exist, it tried to be reused. // interface with the same name exist, it tried to be reused.
func NewTun(options *Config) (Tun, error) { func NewTun(options *Config) (Tun, error) {
// instantiate wintun adapter // instantiate wintun adapter
adapter, err := open(options.Name) adapter, err := open(options.Name, options.Desc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -73,35 +74,49 @@ func NewTun(options *Config) (Tun, error) {
return tun, nil return tun, nil
} }
func open(name string) (*wintun.Adapter, error) { func open(name, desc string) (*wintun.Adapter, error) {
// generate a deterministic GUID from the adapter name // generate a deterministic GUID from the adapter name
id := md5.Sum([]byte(name)) id := md5.Sum([]byte(name))
guid := (*windows.GUID)(unsafe.Pointer(&id[0])) guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
// try to create adapter anew // try to create adapter anew
adapter, err := wintun.CreateAdapter(name, "Xray", guid) adapter, err := wintun.CreateAdapter(name, desc, guid)
if err == nil { if err == nil {
return adapter, nil return adapter, nil
} }
return nil, err return nil, err
} }
func (t *WindowsTun) Start() error { func (t *WindowsTun) Start() (err error) {
var has4, has6 bool var address4, address6 bool
allowedIPs := make([]netip.Prefix, 0, len(t.options.AutoSystemRoutingTable)) addresses := make([]netip.Prefix, 0, len(t.options.Gateway))
for _, route := range t.options.AutoSystemRoutingTable { for _, cidr := range t.options.Gateway {
allowedIPs = append(allowedIPs, netip.MustParsePrefix(route)) prefix := netip.MustParsePrefix(cidr)
if prefix.Addr().Is4() {
address4 = true
} else {
address6 = true
}
addresses = append(addresses, prefix)
} }
dns := make([]netip.Addr, 0, len(t.options.DNS))
for _, ip := range t.options.DNS {
dns = append(dns, netip.MustParseAddr(ip))
}
var route4, route6 bool
routesMap := make(map[winipcfg.RouteData]struct{}) routesMap := make(map[winipcfg.RouteData]struct{})
for _, ip := range allowedIPs { for _, cidr := range t.options.AutoSystemRoutingTable {
prefix := netip.MustParsePrefix(cidr)
route := winipcfg.RouteData{ route := winipcfg.RouteData{
Destination: ip.Masked(), Destination: prefix.Masked(),
Metric: 0, Metric: 0,
} }
if ip.Addr().Is4() { if prefix.Addr().Is4() {
has4 = true route4 = true
route.NextHop = netip.IPv4Unspecified() route.NextHop = netip.IPv4Unspecified()
} else { } else {
has6 = true route6 = true
route.NextHop = netip.IPv6Unspecified() route.NextHop = netip.IPv6Unspecified()
} }
routesMap[route] = struct{}{} routesMap[route] = struct{}{}
@@ -111,24 +126,40 @@ func (t *WindowsTun) Start() error {
r := route r := route
routesData = append(routesData, &r) routesData = append(routesData, &r)
} }
err := t.luid.SetRoutes(routesData)
if err != nil {
return errors.New("unable to set routes").Base(err)
}
if len(t.options.Gateway) > 0 { var retryTimes int
addresses := make([]netip.Prefix, 0, len(t.options.Gateway)) var firstErr error
for _, address := range t.options.Gateway { startOver:
addresses = append(addresses, netip.MustParsePrefix(address)) if retryTimes > 0 {
} if retryTimes > 15 {
err := t.luid.SetIPAddresses(addresses) return windows.ERROR_NOT_FOUND
if err != nil {
return errors.New("unable to set ips").Base(err)
} }
errors.LogErrorInner(context.Background(), firstErr, "Interface configuration failed, retrying attempt ", retryTimes, "/15")
time.Sleep(time.Second)
} }
retryTimes++
if has4 { for _, family := range []winipcfg.AddressFamily{windows.AF_INET, windows.AF_INET6} {
ipif, err := t.luid.IPInterface(windows.AF_INET) if family == windows.AF_INET && route4 || family == windows.AF_INET6 && route6 {
err = t.luid.SetRoutesForFamily(family, routesData)
if err != nil {
firstErr = errors.New("unable to set routes").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
if family == windows.AF_INET && address4 || family == windows.AF_INET6 && address6 {
err = t.luid.SetIPAddressesForFamily(family, addresses)
if err != nil {
firstErr = errors.New("unable to set ips").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
ipif, err := t.luid.IPInterface(family)
if err != nil { if err != nil {
return err return err
} }
@@ -136,56 +167,45 @@ func (t *WindowsTun) Start() error {
ipif.DadTransmits = 0 ipif.DadTransmits = 0
ipif.ManagedAddressConfigurationSupported = false ipif.ManagedAddressConfigurationSupported = false
ipif.OtherStatefulConfigurationSupported = false ipif.OtherStatefulConfigurationSupported = false
ipif.NLMTU = t.options.MTU if family == windows.AF_INET && (address4 || route4) || family == windows.AF_INET6 && (address6 || route6) {
ipif.UseAutomaticMetric = false ipif.NLMTU = t.options.MTU
ipif.Metric = 0 }
if family == windows.AF_INET && route4 || family == windows.AF_INET6 && route6 {
ipif.UseAutomaticMetric = false
ipif.Metric = 0
}
err = ipif.Set() err = ipif.Set()
if err != nil { if err != nil {
return err firstErr = errors.New("unable to set metric and MTU").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
} }
} err = t.luid.SetDNS(family, dns, nil)
if has6 {
ipif, err := t.luid.IPInterface(windows.AF_INET6)
if err != nil { if err != nil {
return err firstErr = errors.New("unable to set DNS").Base(err)
} if err == windows.ERROR_NOT_FOUND {
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled goto startOver
ipif.DadTransmits = 0 }
ipif.ManagedAddressConfigurationSupported = false return firstErr
ipif.OtherStatefulConfigurationSupported = false
ipif.NLMTU = t.options.MTU
ipif.UseAutomaticMetric = false
ipif.Metric = 0
err = ipif.Set()
if err != nil {
return err
}
}
if len(t.options.DNS) > 0 {
dns := make([]netip.Addr, 0, len(t.options.DNS))
for _, ip := range t.options.DNS {
dns = append(dns, netip.MustParseAddr(ip))
}
err := t.luid.SetDNS(windows.AF_INET, dns, nil)
if err != nil {
return err
}
err = t.luid.SetDNS(windows.AF_INET6, dns, nil)
if err != nil {
return err
} }
} }
if updater != nil { if updater != nil {
t.changeCallback, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) { t.cbr, err = winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
updater.Update()
})
if err != nil {
return err
}
t.cbi, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
updater.Update() updater.Update()
}) })
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }
@@ -197,12 +217,26 @@ func (t *WindowsTun) Close() error {
} }
t.closed = true t.closed = true
if t.changeCallback != nil { if t.cbr != nil {
t.changeCallback.Unregister() t.cbr.Unregister()
}
if t.cbi != nil {
t.cbi.Unregister()
}
if t.luid != 0 {
t.luid.FlushRoutes(windows.AF_INET)
t.luid.FlushIPAddresses(windows.AF_INET)
t.luid.FlushDNS(windows.AF_INET)
t.luid.FlushRoutes(windows.AF_INET6)
t.luid.FlushIPAddresses(windows.AF_INET6)
t.luid.FlushDNS(windows.AF_INET6)
}
if t.session != (wintun.Session{}) {
t.session.End()
}
if t.adapter != nil {
t.adapter.Close()
} }
t.session.End()
_ = t.adapter.Close()
return nil return nil
} }
@@ -311,75 +345,49 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
interfaces, err := net.Interfaces() if fixedName != "" {
return net.InterfaceByName(fixedName)
}
r, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lowestMetric := ^uint32(0)
index := uint32(0)
lowestMetricWifi := ^uint32(0)
indexWifi := uint32(0)
for i := range r {
if r[i].DestinationPrefix.PrefixLength != 0 || r[i].InterfaceIndex == uint32(tunIndex) {
continue
}
ifrow, err := r[i].InterfaceLUID.Interface()
if err != nil || ifrow.OperStatus != winipcfg.IfOperStatusUp {
continue
}
if fixedName != "" { iface, err := r[i].InterfaceLUID.IPInterface(windows.AF_INET)
for _, iface := range interfaces { if err != nil {
if iface.Index != tunIndex && iface.Name == fixedName { iface, err = r[i].InterfaceLUID.IPInterface(windows.AF_INET6)
return &iface, nil if err != nil {
continue
} }
} }
return nil, nil
}
var candidates []struct { if ifrow.Type == windows.IF_TYPE_IEEE80211 {
index int if r[i].Metric+iface.Metric < lowestMetricWifi {
score int lowestMetricWifi = r[i].Metric + iface.Metric
indexWifi = r[i].InterfaceIndex
}
continue
}
if r[i].Metric+iface.Metric < lowestMetric {
lowestMetric = r[i].Metric + iface.Metric
index = r[i].InterfaceIndex
}
} }
for i, iface := range interfaces { if indexWifi != 0 {
if iface.Index == tunIndex { index = indexWifi
continue
}
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
candidates = append(candidates, struct {
index int
score int
}{i, scoreWindowsInterface(&iface, addrs)})
} }
return net.InterfaceByIndex(int(index))
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].score != candidates[j].score {
return candidates[i].score > candidates[j].score
}
return interfaces[candidates[i].index].Name < interfaces[candidates[j].index].Name
})
if len(candidates) == 0 {
return nil, nil
}
iface := interfaces[candidates[0].index]
return &iface, nil
}
func scoreWindowsInterface(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score++
break
}
}
return score
} }

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