Compare commits

...
55 Commits
Author SHA1 Message Date
Fangliding c84753dae6 fmt 2026-09-20 17:09:02 +08:00
Fangliding d17906c2f1 Optimize logger 2026-09-20 17:00:49 +08:00
RPRX 52a412d9e2 Xray-core v26.9.9
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 22:25:43 +00:00
LjhAUMEMandRPRX 18a1b5042a Finalmask: Make "udpHop" a new UDP mask (#6327)
https://github.com/XTLS/Xray-core/pull/6327#issuecomment-5592421364

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-09-08 22:15:02 +00:00
patterniha c26d2eda24 Direct/Freedom outbound: Skip domain resolution and finalRules when using sockopt.dialerProxy (#6742)
https://github.com/XTLS/Xray-core/pull/6058#issuecomment-5578800461
2026-09-08 18:45:38 +00:00
风扇滑翔翼 a1bf968be9 VLESS config: Fix validateOutboundTransportSecurity() (#6741)
Fixes https://github.com/XTLS/Xray-core/issues/6737#issuecomment-5587270250
2026-09-08 18:18:12 +00:00
风扇滑翔翼 c037ccd98d infra/vformat/main.go: Refactor (#6739)
https://github.com/XTLS/Xray-core/pull/6327#issuecomment-5581733848
2026-09-08 17:19:15 +00:00
RPRX 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
RPRX 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
Meow 3e2f040cd8 Direct/Freedom outbound: Better Compatibility (#6058)
Fixes https://github.com/XTLS/Xray-core/issues/6393
2026-09-08 03:31:25 +00:00
LjhAUMEM 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
Eninspaceand风扇滑翔翼 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 Asaadi 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
风扇滑翔翼andRaaad1on 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
XXcipherX 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] 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
pexcn 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
patterniha 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
Younes 8ee131cbbb HTTP inbound: Fix a potential panic in readResponseAndHandle100Continue() (#6545) 2026-09-01 02:59:56 +03:00
Jorik_Smith 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
meanwhile131 5e245b082e Routing: Export HealthCheckSettings (#6680) 2026-08-30 12:19:54 +00:00
Kostaand风扇滑翔翼 d9c54026c5 Sniffing: Support QUICv2 (#6695)
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-30 12:15:25 +00:00
brookwko 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
Maksim Varentsovand风扇滑翔翼 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
LjhAUMEM 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
Jidos 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 Varentsov 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
FunLay123and风扇滑翔翼 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
ivolframand风扇滑翔翼 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
风扇滑翔翼 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
风扇滑翔翼andKosta 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
风扇滑翔翼andn0ctal 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
LjhAUMEMandLagPixelLOL 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] 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] 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] 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] 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] 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] 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] 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
风扇滑翔翼 7d214f8b09 WireGuard outbound: Fix sendThrough support (#6570)
Fixes https://github.com/XTLS/Xray-core/issues/6559
2026-08-12 08:38:01 +00:00
yiguodev 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
fanymagnet 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 Varentsov 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] 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] 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] 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] 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
RPRX 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
Sparrowand风扇滑翔翼 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
风扇滑翔翼andechoowall 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
yiguodev 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
199 changed files with 7732 additions and 2490 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
+1 -3
View File
@@ -67,9 +67,7 @@ jobs:
check-latest: true
cache: false
- name: Check Format
run: |
go install -v mvdan.cc/gofumpt@latest
go run ./infra/vformat/main.go -mode check -pwd ./
run: go run ./infra/vformat/main.go -mode check -pwd ./
test:
needs: check-assets
+1 -1
View File
@@ -23,7 +23,7 @@ func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error)
}
if fakeDNSEngine == nil {
errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used")
return protocolSnifferWithMetadata{}, errNotInit
}
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
+1 -1
View File
@@ -28,7 +28,7 @@ func toNetIP(addrs []net.Address) ([]net.IP, error) {
if addr.Family().IsIP() {
ips = append(ips, addr.IP())
} else {
return nil, errors.New("Failed to convert address", addr, "to Net IP.").AtWarning()
return nil, errors.New("Failed to convert address", addr, "to Net IP.")
}
}
return ips, nil
+2 -2
View File
@@ -188,10 +188,10 @@ func parseResponse(payload []byte) (*IPRecord, error) {
var parser dnsmessage.Parser
h, err := parser.Start(payload)
if err != nil {
return nil, errors.New("failed to parse DNS response").Base(err).AtWarning()
return nil, errors.New("failed to parse DNS response").Base(err)
}
if err := parser.SkipAllQuestions(); err != nil {
return nil, errors.New("failed to skip questions in DNS response").Base(err).AtWarning()
return nil, errors.New("failed to skip questions in DNS response").Base(err)
}
now := time.Now()
+3 -3
View File
@@ -58,7 +58,7 @@ func NewFakeDNSHolder() (*Holder, error) {
var err error
if fkdns, err = NewFakeDNSHolderConfigOnly(nil); err != nil {
return nil, errors.New("Unable to create Fake Dns Engine").Base(err).AtError()
return nil, errors.New("Unable to create Fake Dns Engine").Base(err)
}
err = fkdns.initialize(dns.FakeIPv4Pool, 65535)
if err != nil {
@@ -80,13 +80,13 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
var err error
if _, ipRange, err = net.ParseCIDR(ipPoolCidr); err != nil {
return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err).AtError()
return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err)
}
ones, bits := ipRange.Mask.Size()
rooms := bits - ones
if math.Log2(float64(lruSize)) >= float64(rooms) {
return errors.New("LRU size is bigger than subnet size").AtError()
return errors.New("LRU size is bigger than subnet size")
}
fkdns.domainToIP = cache.NewLru(lruSize)
fkdns.ipRange = ipRange
+4 -4
View File
@@ -84,7 +84,7 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
if dest.Network == net.Network_UDP { // UDP classic DNS mode
return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil
}
return nil, errors.New("No available name server could be created from ", dest).AtWarning()
return nil, errors.New("No available name server could be created from ", dest)
}
// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs.
@@ -102,7 +102,7 @@ func NewClient(
// Create a new server for each client for now
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
if err != nil {
return errors.New("failed to create nameserver").Base(err).AtWarning()
return errors.New("failed to create nameserver").Base(err)
}
_, isLocalDNS := server.(*LocalNameServer)
@@ -113,7 +113,7 @@ func NewClient(
if len(ns.ExpectedIp) > 0 {
expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp)
if err != nil {
return errors.New("failed to create expected ip matcher").Base(err).AtWarning()
return errors.New("failed to create expected ip matcher").Base(err)
}
}
@@ -122,7 +122,7 @@ func NewClient(
if len(ns.UnexpectedIp) > 0 {
unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp)
if err != nil {
return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning()
return errors.New("failed to create unexpected ip matcher").Base(err)
}
}
+2 -2
View File
@@ -27,7 +27,7 @@ func (s *FakeDNSServer) IsDisableCache() bool {
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) {
if f.fakeDNSEngine == nil {
return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError()
return nil, 0, errors.New("Unable to locate a fake DNS Engine")
}
var ips []net.Address
@@ -39,7 +39,7 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOp
netIP, err := toNetIP(ips)
if err != nil {
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err).AtError()
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err)
}
errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips)
+6 -2
View File
@@ -89,10 +89,10 @@ func (g *Instance) startInternal() error {
g.active = true
if err := g.initAccessLogger(); err != nil {
return errors.New("failed to initialize access logger").Base(err).AtWarning()
return errors.New("failed to initialize access logger").Base(err)
}
if err := g.initErrorLogger(); err != nil {
return errors.New("failed to initialize error logger").Base(err).AtWarning()
return errors.New("failed to initialize error logger").Base(err)
}
return nil
@@ -141,6 +141,10 @@ func (g *Instance) Handle(msg log.Message) {
}
}
func (g *Instance) Severity() log.Severity {
return g.config.ErrorLogLevel
}
// Close implements common.Closable.Close().
func (g *Instance) Close() error {
errors.LogDebug(context.Background(), "Logger closing")
+6
View File
@@ -78,6 +78,12 @@ func (o *Observer) background() {
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 {
sort.Strings(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.
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"`
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"`
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"`
@@ -382,13 +381,6 @@ func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig {
return nil
}
func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig {
if x != nil {
return x.ProxySettings
}
return nil
}
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
if x != nil {
return x.MultiplexSettings
@@ -506,14 +498,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\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" +
"\x0eOutboundConfig\"\x9d\x03\n" +
"\x0eOutboundConfig\"\xd6\x02\n" +
"\fSenderConfig\x12-\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" +
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12T\n" +
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\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" +
"\aenabled\x18\x01 \x01(\bR\aenabled\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
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
(internet.DomainStrategy)(0), // 13: xray.transport.internet.DomainStrategy
}
var file_app_proxyman_config_proto_depIdxs = []int32{
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
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
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
13, // [13:13] is the sub-list for method output_type
13, // [13:13] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
6, // 10: 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
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
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.
xray.common.net.IPOrDomain via = 1;
xray.transport.internet.StreamConfig stream_settings = 2;
xray.transport.internet.ProxyConfig proxy_settings = 3;
reserved 3;
MultiplexingConfig multiplex_settings = 4;
string via_cidr = 5;
xray.transport.internet.DomainStrategy target_strategy = 6;
+1 -1
View File
@@ -66,7 +66,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
return nil, errors.New("failed to parse stream config").Base(err)
}
newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src})
+1 -1
View File
@@ -165,7 +165,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound
receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig)
if !ok {
return nil, errors.New("not a ReceiverConfig").AtError()
return nil, errors.New("not a ReceiverConfig")
}
streamSettings := receiverSettings.StreamSettings
+2 -2
View File
@@ -142,7 +142,7 @@ func (w *tcpWorker) Start() error {
go w.callback(conn)
})
if err != nil {
return errors.New("failed to listen TCP on ", w.port).AtWarning().Base(err)
return errors.New("failed to listen TCP on ", w.port).Base(err)
}
w.hub = hub
return nil
@@ -528,7 +528,7 @@ func (w *dsWorker) Start() error {
go w.callback(conn)
})
if err != nil {
return errors.New("failed to listen Unix Domain Socket on ", w.address).AtWarning().Base(err)
return errors.New("failed to listen Unix Domain Socket on ", w.address).Base(err)
}
w.hub = hub
return nil
+13 -60
View File
@@ -15,7 +15,6 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/mux"
"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/session"
"github.com/xtls/xray-core/core"
@@ -26,8 +25,6 @@ import (
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"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"
)
@@ -63,7 +60,6 @@ type Handler struct {
streamSettings *internet.MemoryStreamConfig
proxyConfig proto.Message
proxy proxy.Outbound
outboundManager outbound.Manager
mux *mux.ClientManager
xudp *mux.ClientManager
udp443 string
@@ -77,7 +73,6 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
h := &Handler{
tag: config.Tag,
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
}
@@ -92,7 +87,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
h.senderSettings = s
mss, err := internet.ToMemoryStreamConfig(s.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream settings").Base(err).AtWarning()
return nil, errors.New("failed to parse stream settings").Base(err)
}
h.streamSettings = mss
default:
@@ -108,9 +103,11 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
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 {
return nil, err
}
@@ -197,7 +194,6 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
common.Interrupt(link.Reader)
return
}
} else {
unchangedDomain := ob.Target.Address.Domain()
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
@@ -221,7 +217,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 {
switch h.udp443 {
case "reject":
test(errors.New("XUDP rejected UDP/443 traffic").AtInfo())
test(errors.New("XUDP rejected UDP/443 traffic"))
return
case "skip":
goto out
@@ -270,66 +266,26 @@ func (h *Handler) DestIpAddress() net.IP {
// Dial implements internet.Dialer.
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if h.senderSettings != nil {
if h.senderSettings.ProxySettings.HasTag() {
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)
}
if h.senderSettings != nil && 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 = 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
}
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
addr := h.senderSettings.Via.AsAddress()
domain = h.senderSettings.Via.GetDomain()
switch {
case h.senderSettings.ViaCidr != "":
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
case domain == "origin":
if inbound := session.InboundFromContext(ctx); inbound != nil {
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())
}
}
// case addr.Family().IsDomain():
default:
default: // case addr.Family().IsDomain():
ob.Gateway = addr
}
}
}
+2 -2
View File
@@ -68,13 +68,13 @@ func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) err
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
if ob == nil {
return errors.New("outbound metadata not found").AtError()
return errors.New("outbound metadata not found")
}
if isDomain(ob.Target, p.domain) {
muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
if err != nil {
return errors.New("failed to create mux client worker").Base(err).AtWarning()
return errors.New("failed to create mux client worker").Base(err)
}
worker, err := NewPortalWorker(muxClient)
+3 -3
View File
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
// GetPrincipleTarget implements routing.BalancerPrincipleTarget
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 {
candidates, err := b.SelectOutbounds()
if err != nil {
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
// SetOverrideTarget implements routing.BalancerOverrider
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)
return nil
}
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
// GetOverrideTarget implements routing.BalancerOverrider
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 "", errors.New("cannot find tag")
-17
View File
@@ -2,25 +2,8 @@ package router
import (
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 {
target string
}
+20
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
@@ -393,3 +394,22 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
}
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 (
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
. "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) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
+6 -2
View File
@@ -33,6 +33,10 @@ func (r *Rule) Apply(ctx routing.Context) bool {
func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds := NewConditionChan()
if len(rr.LocalOs) > 0 {
conds.Add(NewLocalOSMatcher(rr.LocalOs))
}
if len(rr.InboundTag) > 0 {
conds.Add(NewInboundTagMatcher(rr.InboundTag))
}
@@ -111,7 +115,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
}
if conds.Len() == 0 {
return nil, errors.New("this rule has no effective fields").AtWarning()
return nil, errors.New("this rule has no effective fields")
}
return conds, nil
@@ -141,7 +145,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
}
s, ok := i.(*StrategyLeastLoadConfig)
if !ok {
return nil, errors.New("not a StrategyLeastLoadConfig").AtError()
return nil, errors.New("not a StrategyLeastLoadConfig")
}
leastLoadStrategy := NewLeastLoadStrategy(s)
return &Balancer{
+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"`
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// List of operating systems for matching the one Xray itself is running on.
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() {
@@ -278,6 +280,13 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig {
return nil
}
func (x *RoutingRule) GetLocalOs() []string {
if x != nil {
return x.LocalOs
}
return nil
}
type isRoutingRule_TargetTag interface {
isRoutingRule_TargetTag()
}
@@ -637,7 +646,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor
const file_app_router_config_proto_rawDesc = "" +
"\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" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\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" +
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\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" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
+3
View File
@@ -56,6 +56,9 @@ message RoutingRule {
repeated string process = 21;
WebhookConfig webhook = 22;
// List of operating systems for matching the one Xray itself is running on.
repeated string local_os = 23;
}
message WebhookConfig {
+59 -114
View File
@@ -2,7 +2,9 @@ package router
import (
"context"
"maps"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
@@ -17,8 +19,8 @@ import (
// Router is an implementation of routing.Router.
type Router struct {
domainStrategy Config_DomainStrategy
rules []*Rule
balancers map[string]*Balancer
rules atomic.Pointer[[]*Rule]
balancers atomic.Pointer[map[string]*Balancer]
dns dns.Client
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.dispatcher = dispatcher
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
for _, rule := range config.BalancingRule {
balancer, err := rule.Build(ohm, dispatcher)
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
r.rules.Store(new([]*Rule))
r.balancers.Store(&map[string]*Balancer{})
return r.ReloadRules(config, false)
}
// PickRoute implements routing.Router.
@@ -124,18 +83,22 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
r.mu.Lock()
defer r.mu.Unlock()
if !shouldAppend {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
oldRules := *r.rules.Load()
oldBalancers := *r.balancers.Load()
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 {
_, found := r.balancers[rule.Tag]
if found {
if _, found := newBalancers[rule.Tag]; found {
return errors.New("duplicate balancer tag")
}
balancer, err := rule.Build(r.ohm, r.dispatcher)
@@ -143,27 +106,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
return err
}
balancer.InjectContext(r.ctx)
r.balancers[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]
newBalancers[rule.Tag] = balancer
}
for _, rule := range config.Rule {
if r.RuleExists(rule.GetRuleTag()) {
closeNewWebhooks()
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
}
cond, err := rule.BuildCondition()
if err != nil {
closeNewWebhooks()
return err
}
rr := &Rule{
@@ -171,69 +119,64 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
Tag: rule.GetTag(),
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 {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
closeNewWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if btag := rule.GetBalancingTag(); len(btag) > 0 {
brule, found := newBalancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
closeNewWebhooks()
return errors.New("balancer ", btag, " not found")
}
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
}
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.
func (r *Router) RemoveRule(tag string) error {
if tag == "" {
return errors.New("empty tag name!")
}
r.mu.Lock()
defer r.mu.Unlock()
newRules := []*Rule{}
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag != tag {
newRules = append(newRules, rule)
} else if rule.Webhook != nil {
rule.Webhook.Close()
}
oldRules := *r.rules.Load()
newRules := make([]*Rule, 0, len(oldRules))
var removed []*Rule
for _, rule := range oldRules {
if rule.RuleTag != tag {
newRules = append(newRules, rule)
} 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
func (r *Router) ListRule() []routing.Route {
r.mu.Lock()
defer r.mu.Unlock()
ruleList := make([]routing.Route, 0)
for _, rule := range r.rules {
rules := *r.rules.Load()
ruleList := make([]routing.Route, 0, len(rules))
for _, rule := range rules {
ruleList = append(ruleList, &Route{
outboundTag: rule.Tag,
ruleTag: rule.RuleTag,
@@ -252,7 +195,9 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
}
for _, rule := range r.rules {
rules := *r.rules.Load()
for _, rule := range rules {
if rule.Apply(ctx) {
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)
// Try applying rules again if we have IPs.
for _, rule := range r.rules {
for _, rule := range rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
@@ -279,9 +224,9 @@ func (r *Router) Start() error {
return nil
}
// closeWebhooks closes all webhook notifiers in the current rule set.
func (r *Router) closeWebhooks() {
for _, rule := range r.rules {
// closeWebhooks closes all webhook notifiers in the given rule set.
func closeWebhooks(rules []*Rule) {
for _, rule := range rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
@@ -292,7 +237,7 @@ func (r *Router) closeWebhooks() {
func (r *Router) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
r.closeWebhooks()
closeWebhooks(*r.rules.Load())
return nil
}
+17 -23
View File
@@ -8,6 +8,7 @@ import (
"net"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/errors"
@@ -40,6 +41,7 @@ type WebhookNotifier struct {
deduplication uint32
client *http.Client
seen sync.Map
lastSweep atomic.Int64
done chan struct{}
wg sync.WaitGroup
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
}
@@ -201,6 +198,7 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
}
ttl := time.Duration(h.deduplication) * time.Second
now := time.Now()
h.maybeSweep(now, ttl)
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
if now.Sub(v.(time.Time)) < ttl {
return true
@@ -210,27 +208,23 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
return false
}
func (h *WebhookNotifier) cleanupLoop() {
defer h.wg.Done()
ttl := time.Duration(h.deduplication) * time.Second
ticker := time.NewTicker(ttl)
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
})
}
func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
last := h.lastSweep.Load()
if now.UnixNano()-last < int64(ttl) {
return
}
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 {
h.closeOnce.Do(func() {
close(h.done)
+3 -1
View File
@@ -118,7 +118,9 @@ func (w *BufferedWriter) Write(b []byte) (int, error) {
nBytes, err := w.buffer.Write(b)
totalBytes += nBytes
if err != nil {
// ErrBufferFull means a partial write, so flush below and continue
if err != nil && err != ErrBufferFull {
return totalBytes, err
}
if !w.buffered || w.buffer.IsFull() {
+3 -3
View File
@@ -10,12 +10,12 @@ import (
// [,)
func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
if from > to {
from, to = to, from
}
if d := to - from; d == 0 || d == 1 {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}
+13 -65
View File
@@ -18,17 +18,12 @@ type hasInnerError interface {
Unwrap() error
}
type hasSeverity interface {
Severity() log.Severity
}
// Error is an error object with underlying error.
type Error struct {
prefix []interface{}
message []interface{}
caller string
inner error
severity log.Severity
prefix []interface{}
message []interface{}
caller string
inner error
}
// Error implements error.Error().
@@ -69,46 +64,6 @@ func (err *Error) Base(e error) *Error {
return err
}
func (err *Error) atSeverity(s log.Severity) *Error {
err.severity = s
return err
}
func (err *Error) Severity() log.Severity {
if err.inner == nil {
return err.severity
}
if s, ok := err.inner.(hasSeverity); ok {
as := s.Severity()
if as < err.severity {
return as
}
}
return err.severity
}
// AtDebug sets the severity to debug.
func (err *Error) AtDebug() *Error {
return err.atSeverity(log.Severity_Debug)
}
// AtInfo sets the severity to info.
func (err *Error) AtInfo() *Error {
return err.atSeverity(log.Severity_Info)
}
// AtWarning sets the severity to warning.
func (err *Error) AtWarning() *Error {
return err.atSeverity(log.Severity_Warning)
}
// AtError sets the severity to error.
func (err *Error) AtError() *Error {
return err.atSeverity(log.Severity_Error)
}
// String returns the string representation of this error.
func (err *Error) String() string {
return err.Error()
@@ -132,9 +87,8 @@ func New(msg ...interface{}) *Error {
details = details[:i]
}
return &Error{
message: msg,
severity: log.Severity_Info,
caller: details,
message: msg,
caller: details,
}
}
@@ -171,6 +125,9 @@ func LogErrorInner(ctx context.Context, inner error, msg ...interface{}) {
}
func doLog(ctx context.Context, inner error, severity log.Severity, msg ...interface{}) {
if log.GetSeverity() < severity {
return
}
pc, _, _, _ := runtime.Caller(2)
details := runtime.FuncForPC(pc).Name()
if len(details) >= trim {
@@ -181,10 +138,9 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
details = details[:i]
}
err := &Error{
message: msg,
severity: severity,
caller: details,
inner: inner,
message: msg,
caller: details,
inner: inner,
}
if ctx != nil && ctx != context.Background() {
id := uint32(c.IDFromContext(ctx))
@@ -193,7 +149,7 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
}
}
log.Record(&log.GeneralMessage{
Severity: GetSeverity(err),
Severity: severity,
Content: err,
})
}
@@ -217,11 +173,3 @@ L:
}
return err
}
// GetSeverity returns the actual severity of the error, including inner errors.
func GetSeverity(err error) log.Severity {
if s, ok := err.(hasSeverity); ok {
return s.Severity()
}
return log.Severity_Info
}
+6 -15
View File
@@ -7,30 +7,21 @@ import (
"github.com/google/go-cmp/cmp"
. "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
)
func TestError(t *testing.T) {
err := New("TestError")
if v := GetSeverity(err); v != log.Severity_Info {
t.Error("severity: ", v)
if v := err.Error(); !strings.Contains(v, "TestError") {
t.Error("error: ", v)
}
err = New("TestError2").Base(io.EOF)
if v := GetSeverity(err); v != log.Severity_Info {
t.Error("severity: ", v)
if v := err.Error(); !strings.Contains(v, "EOF") {
t.Error("error: ", v)
}
err = New("TestError3").Base(io.EOF).AtWarning()
if v := GetSeverity(err); v != log.Severity_Warning {
t.Error("severity: ", v)
}
err = New("TestError4").Base(io.EOF).AtWarning()
err = New("TestError5").Base(err)
if v := GetSeverity(err); v != log.Severity_Warning {
t.Error("severity: ", v)
}
err = New("TestError3").Base(io.EOF)
err = New("TestError4").Base(err)
if v := err.Error(); !strings.Contains(v, "EOF") {
t.Error("error: ", v)
}
+21 -25
View File
@@ -1,7 +1,7 @@
package log // import "github.com/xtls/xray-core/common/log"
import (
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/serial"
)
@@ -29,36 +29,32 @@ func (m *GeneralMessage) String() string {
// Record writes a message into log stream.
func Record(msg Message) {
logHandler.Handle(msg)
if h := logHandler.Load(); h != nil {
(*h).Handle(msg)
}
}
var logHandler syncHandler
type SeverityLogger interface {
Handler
Severity() Severity
}
func GetSeverity() Severity {
if h := logHandler.Load(); h != nil {
if sh, ok := (*h).(SeverityLogger); ok {
return sh.Severity()
}
}
// log everything by default
return Severity_Debug
}
var logHandler atomic.Pointer[Handler]
// RegisterHandler registers a new handler as current log handler. Previous registered handler will be discarded.
func RegisterHandler(handler Handler) {
if handler == nil {
panic("Log handler is nil")
}
logHandler.Set(handler)
}
type syncHandler struct {
sync.RWMutex
Handler
}
func (h *syncHandler) Handle(msg Message) {
h.RLock()
defer h.RUnlock()
if h.Handler != nil {
h.Handler.Handle(msg)
}
}
func (h *syncHandler) Set(handler Handler) {
h.Lock()
defer h.Unlock()
h.Handler = handler
logHandler.Store(&handler)
}
+4
View File
@@ -68,6 +68,10 @@ func (l *serverityLogger) Handle(msg Message) {
}
}
func (l *serverityLogger) Severity() Severity {
return l.logLevel
}
func (l *generalLogger) run() {
defer l.access.Signal()
+1 -1
View File
@@ -38,7 +38,7 @@ func (m *ClientManager) Dispatch(ctx context.Context, link *transport.Link) erro
}
}
return errors.New("unable to find an available mux client").AtWarning()
return errors.New("unable to find an available mux client")
}
type WorkerPicker interface {
+1 -1
View File
@@ -117,7 +117,7 @@ func (f *FrameMetadata) Unmarshal(reader io.Reader, readSourceAndLocal bool) err
return err
}
if metaLen > 512 {
return errors.New("invalid metalen ", metaLen).AtError()
return errors.New("invalid metalen ", metaLen)
}
b := buf.New()
+1 -1
View File
@@ -351,7 +351,7 @@ func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedRead
err = w.handleStatusKeep(&meta, reader)
default:
status := meta.SessionStatus
return errors.New("unknown status: ", status).AtError()
return errors.New("unknown status: ", status)
}
if err != nil {
+4 -2
View File
@@ -1,4 +1,4 @@
//go:build darwin
//go:build darwin && !ios
package net
@@ -198,7 +198,9 @@ func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr,
vflag := info[darwinInSockInfoVFlagOff]
if srcAddr.Is4() {
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
// 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 {
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin
//go:build darwin && !ios
#include "textflag.h"
+64 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin
//go:build darwin && !ios
package net
@@ -52,6 +52,57 @@ func TestFindProcessDarwinTCP(t *testing.T) {
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 {
@@ -264,6 +315,18 @@ func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
}
}
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 {
+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")
}
+25 -34
View File
@@ -3,11 +3,8 @@ package bittorrent
import (
"encoding/binary"
"errors"
"math"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type SniffHeader struct{}
@@ -39,50 +36,44 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
return nil, common.ErrNoClue
}
buffer := buf.FromBytes(b)
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 {
// type 4 (ST_SYN), version 1
if b[0] != 0x41 {
return nil, errNotBittorrent
}
var extension uint8
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
} else if extension != 0 && extension != 1 {
// timestamp_difference is always 0 in new connections
if binary.BigEndian.Uint32(b[8:12]) != 0 {
return nil, errNotBittorrent
}
// Walk the extension chain. Selective ack (1) and extension bits (2)
extension, offset := b[1], 20
for extension != 0 {
if extension != 1 {
if len(b) < offset+2 {
return nil, errNotBittorrent
}
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
length := int(b[offset+1])
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
}
var length uint8
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
if len(b) < offset+2+length {
return nil, errNotBittorrent
}
extension = b[offset]
offset += 2 + length
}
if common.Error2(buffer.ReadBytes(2)) != nil {
return nil, common.ErrNoClue
}
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) {
// extensions should consume all ST_SYN payload
if len(b) != offset {
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")
}
})
}
}
-8
View File
@@ -1,18 +1,10 @@
package quic
import (
"crypto"
"crypto/cipher"
_ "crypto/tls"
_ "unsafe"
)
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+89 -74
View File
@@ -3,7 +3,6 @@ package quic
import (
"crypto"
"crypto/aes"
"crypto/tls"
"encoding/binary"
"io"
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
return s.domain
}
const (
versionDraft29 uint32 = 0xff00001d
version1 uint32 = 0x1
var (
errNotQUIC = errors.New("not quic")
errNotQUICInitial = errors.New("not initial packet")
)
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var (
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
initialSuite = &CipherSuiteTLS13{
ID: tls.TLS_AES_128_GCM_SHA256,
KeyLen: 16,
AEAD: AEADAESGCMTLS13,
Hash: crypto.SHA256,
quicDraft29 = quicVersionSpec{
ver: 0xff00001d,
typeInitial: 0b00,
initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
labelPrefix: "quic",
}
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) {
@@ -63,60 +83,61 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte()
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQuicInitial
return nil, errNotQUICInitial
}
vb, err := buffer.ReadBytes(4)
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
versionNumber := binary.BigEndian.Uint32(vb)
if versionNumber != 0 && typeByte&0x40 == 0 {
return nil, errNotQuic
} else if versionNumber != versionDraft29 && versionNumber != version1 {
return nil, errNotQuic
var s *quicVersionSpec
if v, ok := quicVersionSpecMap[versionNumber]; ok {
s = v
} else {
return nil, errNotQUIC
}
packetType := (typeByte & 0x30) >> 4
isQuicInitial := packetType == 0x0
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
} 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
tokenLen, err := readShortQuicVarint(buffer)
packetType := (typeByte & 0x30) >> 4
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)) {
return nil, errNotQuic
return nil, errNotQUIC
}
if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
}
packetLen, err := readShortQuicVarint(buffer)
packetLen, err := readShortQUICVarint(buffer)
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
// packetLen is impossible to be shorter than this
if packetLen < 4 {
return nil, errNotQuic
return nil, errNotQUIC
}
hdrLen := len(b) - int(buffer.Len())
@@ -125,26 +146,22 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
}
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
continue
}
var salt []byte
if versionNumber == version1 {
salt = quicSalt
} else {
salt = quicSaltOld
}
salt := s.initialSalt
label := s.labelPrefix
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
block, err := aes.NewCipher(hpKey)
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic
return nil, errNotQUIC
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
@@ -155,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1]
}
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(secret, label+" iv", 12)
cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -181,44 +198,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame
case 0x01: // PING 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
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF
}
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
if err != nil {
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
}
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
}
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
}
}
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
}
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
}
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
}
}
case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQuicVarint(buffer) // Field: Offset
offset, err := readShortQUICVarint(buffer) // Field: Offset
if err != nil {
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQuicVarint(buffer) // Field: Length
length, err := readShortQUICVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF
}
@@ -234,13 +251,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF
}
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
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
if err != nil {
return nil, io.ErrUnexpectedEOF
}
@@ -250,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default:
// Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQuicInitial
return nil, errNotQUICInitial
}
}
@@ -268,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData
}
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context))
binary.BigEndian.PutUint16(b, uint16(length))
b[2] = uint8(6 + len(label))
b = append(b, []byte("tls13 ")...)
b = append(b, []byte(label)...)
b = b[:3+6+len(label)+1]
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 0, 2+1+6+len(label)+1)
b = binary.BigEndian.AppendUint16(b, uint16(length))
b = append(b, byte(6+len(label)))
b = append(b, "tls13 "...)
b = append(b, label...)
b = append(b, 0) // context
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 {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
}
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
// 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)
if err != nil {
return 0, err
}
if v > 65535 {
// not used(
return 0, errNotQuicInitial
return 0, errNotQUICInitial
}
return int32(v), nil
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@ import (
func (u *User) GetTypedAccount() (Account, error) {
if u.GetAccount() == nil {
return nil, errors.New("Account is missing").AtWarning()
return nil, errors.New("Account is missing")
}
rawAccount, err := u.Account.GetInstance()
+1
View File
@@ -207,6 +207,7 @@ func getConfig() string {
"tag": "XHTTP_IN",
"streamSettings": {
"network": "xhttp",
"security": "tls",
"xhttpSettings": {
"host": "bing.com",
"path": "/xhttp_client_upload",
-2
View File
@@ -70,8 +70,6 @@ type Outbound struct {
Tag string
// Name of the outbound proxy that handles the connection.
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
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
CanSpliceCopy int
+2 -2
View File
@@ -16,7 +16,7 @@ var typeCreatorRegistry = make(map[reflect.Type]ConfigCreator)
func RegisterConfig(config interface{}, configCreator ConfigCreator) error {
configType := reflect.TypeOf(config)
if _, found := typeCreatorRegistry[configType]; found {
return errors.New(configType.Name() + " is already registered").AtError()
return errors.New(configType.Name() + " is already registered")
}
typeCreatorRegistry[configType] = configCreator
return nil
@@ -27,7 +27,7 @@ func CreateObject(ctx context.Context, config interface{}) (interface{}, error)
configType := reflect.TypeOf(config)
creator, found := typeCreatorRegistry[configType]
if !found {
return nil, errors.New(configType.String() + " is not registered").AtError()
return nil, errors.New(configType.String() + " is not registered")
}
return creator(ctx, config)
}
+4 -4
View File
@@ -125,7 +125,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
}
if f == "" {
return nil, errors.New("Failed to get format of ", file).AtWarning()
return nil, errors.New("Failed to get format of ", file)
}
if f == "protobuf" {
@@ -142,7 +142,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
if len(v) == 1 {
return configLoaderByName["protobuf"].Loader(v)
} else {
return nil, errors.New("Only one protobuf config file is allowed").AtWarning()
return nil, errors.New("Only one protobuf config file is allowed")
}
}
@@ -152,11 +152,11 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
if f, found := configLoaderByName[formatName]; found {
return f.Loader(v)
} else {
return nil, errors.New("Unable to load config in", formatName).AtWarning()
return nil, errors.New("Unable to load config in", formatName)
}
}
return nil, errors.New("Unable to load config").AtWarning()
return nil, errors.New("Unable to load config")
}
func loadProtobufConfig(data []byte) (*Config, error) {
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 7
Version_z byte = 11
Version_y byte = 9
Version_z byte = 9
)
var (
+24 -21
View File
@@ -1,62 +1,65 @@
module github.com/xtls/xray-core
go 1.26
go 1.27
require (
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.4
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0
github.com/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/pion/stun/v3 v3.1.6
github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.15.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1
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/xtls/reality v0.0.0-20260322125925-9234c772ba8f
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.54.0
golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.57.0
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
lukechampine.com/blake3 v1.4.1
mvdan.cc/gofumpt v0.12.0
)
require (
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/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pion/transport/v4 v4.1.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/text v0.40.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+65 -51
View File
@@ -1,20 +1,19 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
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.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
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/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
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/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/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -23,93 +22,107 @@ 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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
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/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/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/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/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
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/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8=
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/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/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc=
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/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
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/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/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
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/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/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
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/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/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/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
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/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/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
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/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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
@@ -127,15 +140,16 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -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=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
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-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/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/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/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
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/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
import (
"encoding/json"
"encoding/base64"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/blackhole"
"google.golang.org/protobuf/proto"
)
type NoneResponse struct{}
func (*NoneResponse) Build() (proto.Message, error) {
return new(blackhole.NoneResponse), nil
}
type HTTPResponse struct{}
func (*HTTPResponse) Build() (proto.Message, error) {
return new(blackhole.HTTPResponse), nil
type ResponseConfig struct {
Type string `json:"type"`
CustomResponseData string `json:"customResponseData"`
}
type BlackholeConfig struct {
Response json.RawMessage `json:"response"`
Response *ResponseConfig `json:"response"`
}
func (v *BlackholeConfig) Build() (proto.Message, error) {
config := new(blackhole.Config)
if v.Response != nil {
response, _, err := configLoader.Load(v.Response)
if err != nil {
return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err)
responseName := strings.ToLower(v.Response.Type)
switch responseName {
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
}
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 (
"testing"
"github.com/xtls/xray-core/common/serial"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/blackhole"
)
@@ -22,7 +21,7 @@ func TestHTTPResponseJSON(t *testing.T) {
}`,
Parser: loadJSON(creator),
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 {
if v == nil {
return nil
}
return net.NewIPOrDomain(v.Address)
}
+2 -2
View File
@@ -97,7 +97,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
user.Email = v.Email
} else {
if err := json.Unmarshal(rawUser, user); err != nil {
return nil, errors.New("failed to parse HTTP user").Base(err).AtError()
return nil, errors.New("failed to parse HTTP user").Base(err)
}
}
account := new(HTTPAccount)
@@ -106,7 +106,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
account.Password = v.Password
} else {
if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New("failed to parse HTTP account").Base(err).AtError()
return nil, errors.New("failed to parse HTTP account").Base(err)
}
}
user.Account = serial.ToTypedMessage(account.Build())
+1 -1
View File
@@ -18,7 +18,7 @@ func RegisterConfigureFilePostProcessingStage(name string, stage ConfigureFilePo
func PostProcessConfigureFile(conf *Config) error {
for k, v := range configureFilePostProcessingStages {
if err := v.Process(conf); err != nil {
return errors.New("Rejected by Postprocessing Stage ", k).AtError().Base(err)
return errors.New("Rejected by Postprocessing Stage ", k).Base(err)
}
}
return nil
+2 -2
View File
@@ -13,7 +13,7 @@ type ConfigCreatorCache map[string]ConfigCreator
func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error {
if _, found := v[id]; found {
return errors.New(id, " already registered.").AtError()
return errors.New(id, " already registered.")
}
v[id] = creator
@@ -61,7 +61,7 @@ func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) {
}
rawID, found := obj[v.idKey]
if !found {
return nil, "", errors.New(v.idKey, " not found in JSON context").AtError()
return nil, "", errors.New(v.idKey, " not found in JSON context")
}
var id string
if err := json.Unmarshal(rawID, &id); err != nil {
+1 -1
View File
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
type BurstObservatoryConfig struct {
SubjectSelector []string `json:"subjectSelector"`
// health check settings
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"`
HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
}
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"`
LocalPort *PortList `json:"localPort"`
Process *StringList `json:"process"`
LocalOS *StringList `json:"localOS"`
Webhook *WebhookRuleConfig `json:"webhook"`
}
rawFieldRule := new(RawFieldRule)
@@ -261,6 +262,10 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
rule.Process = *rawFieldRule.Process
}
if rawFieldRule.LocalOS != nil && len(*rawFieldRule.LocalOS) > 0 {
rule.LocalOs = *rawFieldRule.LocalOS
}
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
rule.Webhook = &router.WebhookConfig{
Url: rawFieldRule.Webhook.URL,
+3 -3
View File
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
Tolerance float64 `json:"tolerance,omitempty"`
}
// healthCheckSettings holds settings for health Checker
type healthCheckSettings struct {
// HealthCheckSettings holds settings for health Checker
type HealthCheckSettings struct {
Destination string `json:"destination"`
Connectivity string `json:"connectivity"`
Interval duration.Duration `json:"interval"`
@@ -53,7 +53,7 @@ type healthCheckSettings struct {
HttpMethod string `json:"httpMethod"`
}
func (h healthCheckSettings) Build() (proto.Message, error) {
func (h HealthCheckSettings) Build() (proto.Message, error) {
var httpMethod string
if h.HttpMethod == "" {
httpMethod = "HEAD"
+1 -1
View File
@@ -30,7 +30,7 @@ func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) {
if j, ok := creflect.MarshalToJson(c, true); ok {
return j, nil
}
return "", errors.New("marshal to json failed.").AtError()
return "", errors.New("marshal to json failed.")
}
func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
+2 -3
View File
@@ -44,7 +44,6 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
case AuthMethodUserPass:
config.AuthType = socks.AuthType_PASSWORD
default:
// errors.New("unknown socks auth method: ", v.AuthMethod, ". Default to noauth.").AtWarning().WriteToLog()
config.AuthType = socks.AuthType_NO_AUTH
}
@@ -115,7 +114,7 @@ func (v *SocksClientConfig) Build() (proto.Message, error) {
user.Email = v.Email
} else {
if err := json.Unmarshal(rawUser, user); err != nil {
return nil, errors.New("failed to parse Socks user").Base(err).AtError()
return nil, errors.New("failed to parse Socks user").Base(err)
}
}
account := new(SocksAccount)
@@ -124,7 +123,7 @@ func (v *SocksClientConfig) Build() (proto.Message, error) {
account.Password = v.Password
} else {
if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New("failed to parse socks account").Base(err).AtError()
return nil, errors.New("failed to parse socks account").Base(err)
}
}
user.Account = serial.ToTypedMessage(account.Build())
+131 -23
View File
@@ -11,8 +11,10 @@ import (
"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"
"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"
@@ -22,6 +24,7 @@ import (
"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/udphop"
"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"
@@ -82,6 +85,7 @@ var (
"xdns": func() interface{} { return new(Xdns) },
"xicmp": func() interface{} { return new(Xicmp) },
"realm": func() interface{} { return new(Realm) },
"udphop": func() interface{} { return new(UDPHop) },
}, "type", "settings")
)
@@ -720,14 +724,46 @@ func (c *Xdns) Build() (proto.Message, error) {
}
type XMC struct {
Hostname string `json:"hostname"`
Usernames []string `json:"usernames"`
Password string `json:"password"`
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.Usernames) == 0 {
c.Usernames = []string{"Dream"}
if len(c.Profiles) == 0 {
return nil, fmt.Errorf("minecraft profiles are required")
}
if c.Password == "" {
@@ -744,12 +780,21 @@ func (c *XMC) Build() (proto.Message, error) {
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,
Usernames: c.Usernames,
Hostname: c.Hostname,
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
RsaPublicKey: rsaPublicKey,
Profiles: profiles,
}, nil
}
@@ -774,9 +819,11 @@ func (c *Xicmp) Build() (proto.Message, error) {
}
type Realm struct {
Url string `json:"url"`
StunServers []string `json:"stunServers"`
TlsConfig *TLSConfig `json:"tlsConfig"`
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) {
@@ -856,6 +903,64 @@ func (c *Realm) Build() (proto.Message, error) {
ID: id,
StunServers: stunServers,
TlsConfig: tlsConfig,
IPMode: strings.ToLower(c.IPMode),
PortMapping: c.PortMapping,
}, nil
}
type UDPHop struct {
Sockopt *SocketConfig `json:"sockopt"`
Mode string `json:"mode"`
Interval Int32Range `json:"interval"`
RemotePorts PortList `json:"remotePorts"`
RemoteIPs []string `json:"remoteIPs"`
}
func (c *UDPHop) Build() (proto.Message, error) {
var sockopt *internet.SocketConfig
if c.Sockopt != nil {
var err error
sockopt, err = c.Sockopt.Build()
if err != nil {
return nil, err
}
}
var local, remote, remoteOnce bool
for _, mode := range strings.Split(c.Mode, ",") {
switch strings.ToLower(mode) {
case "intervallocal":
local = true
case "intervalremote":
remote = true
case "perconnremote":
remoteOnce = true
default:
return nil, errors.New("invalid mode ", mode)
}
}
var remoteIPs []string
for _, ip := range c.RemoteIPs {
prefix, err := netip.ParsePrefix(ip)
if err == nil {
remoteIPs = append(remoteIPs, prefix.String())
continue
}
addr, err := netip.ParseAddr(ip)
if err == nil {
remoteIPs = append(remoteIPs, netip.PrefixFrom(addr, addr.BitLen()).String())
continue
}
return nil, errors.New("invalid ip ", ip)
}
return &udphop.Config{
Sockopt: sockopt,
Local: local,
Remote: remote,
RemoteOnce: remoteOnce,
IntervalMin: int64(c.Interval.From),
IntervalMax: int64(c.Interval.To),
RemotePorts: c.RemotePorts.Build().Ports(),
RemoteIPs: remoteIPs,
}, nil
}
@@ -886,20 +991,23 @@ func (c *Mask) Build(tcp bool) (proto.Message, error) {
}
type QuicParamsConfig struct {
Congestion string `json:"congestion"`
Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"`
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"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
Congestion string `json:"congestion"`
Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"`
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
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 {
@@ -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)
}
}
+16 -39
View File
@@ -253,10 +253,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if (c.FinalMask.QuicParams.UdpHop.Interval.From != 0 && c.FinalMask.QuicParams.UdpHop.Interval.From < 5) || (c.FinalMask.QuicParams.UdpHop.Interval.To != 0 && c.FinalMask.QuicParams.UdpHop.Interval.To < 5) {
return nil, errors.New("Interval must be at least 5")
}
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
@@ -285,44 +281,25 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
UdpHop: &internet.UdpHop{
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
IntervalMax: int64(c.FinalMask.QuicParams.UdpHop.Interval.To),
},
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
}
}
}
return config, nil
}
type ProxyConfig struct {
Tag string `json:"tag"`
// TransportLayerProxy: For compatibility.
TransportLayerProxy bool `json:"transportLayer"`
}
// Build implements Buildable.
func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) {
if v.Tag == "" {
return nil, errors.New("Proxy tag is not set.")
}
return &internet.ProxyConfig{
Tag: v.Tag,
TransportLayerProxy: v.TransportLayerProxy,
}, nil
}
+14 -32
View File
@@ -1,7 +1,6 @@
package conf
import (
"context"
"encoding/json"
"math/big"
"net/url"
@@ -122,7 +121,7 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
return nil, errors.New("empty HTTP header value: " + key)
}
config.Header = append(config.Header, &http.Header{
Name: key,
@@ -190,7 +189,7 @@ func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
return nil, errors.New("empty HTTP header value: " + key)
}
config.Header = append(config.Header, &http.Header{
Name: key,
@@ -240,11 +239,11 @@ func (c *TCPConfig) Build() (proto.Message, error) {
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()
return nil, errors.New("invalid TCP header config").Base(err)
}
ts, err := headerConfig.(Buildable).Build()
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
return nil, errors.New("invalid TCP header config").Base(err)
}
config.HeaderSettings = serial.ToTypedMessage(ts)
}
@@ -450,8 +449,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 6
c.Xmux.MaxConnections.To = 6
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
@@ -534,10 +533,6 @@ type KCPConfig struct {
// 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 {
@@ -560,16 +555,16 @@ func (c *KCPConfig) Build() (proto.Message, error) {
}
if config.Mtu < 21 {
return nil, errors.New("Mtu must be at least 21").AtError()
return nil, errors.New("MTU must be at least 21")
}
if config.Tti < 10 || config.Tti > 1000 {
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
return nil, errors.New("TTI must be between 10 and 1000")
}
if config.CwndMultiplier < 1 {
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
return nil, errors.New("CwndMultiplier must be at least 1")
}
if config.GetSendingBufferSize() == 0 {
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
return nil, errors.New("MaxSendingWindow must be at least ", config.Mtu)
}
return config, nil
@@ -739,11 +734,6 @@ func (b Bandwidth) Bps() (uint64, error) {
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"`
@@ -751,6 +741,7 @@ type Masquerade struct {
Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"`
XForwarded bool `json:"xForwarded"`
Insecure bool `json:"insecure"`
Content string `json:"content"`
@@ -759,14 +750,8 @@ type Masquerade struct {
}
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"`
Version int32 `json:"version"`
Auth string `json:"auth"`
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
Masquerade Masquerade `json:"masquerade"`
}
@@ -776,10 +761,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
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")
}
@@ -791,6 +772,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
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
+3 -3
View File
@@ -113,10 +113,10 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
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`)
// 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`)
// 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)
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
type CustomSockoptConfig struct {
Syetem string `json:"system"`
System string `json:"system"`
Network string `json:"network"`
Level string `json:"level"`
Opt string `json:"opt"`
@@ -124,7 +124,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{
System: copt.Syetem,
System: copt.System,
Network: copt.Network,
Level: copt.Level,
Opt: copt.Opt,
+3
View File
@@ -312,6 +312,9 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New(`VLESS users: invalid user`).Base(err)
}
// validateOutboundTransportSecurity needs to see these
c.Encryption = account.Encryption
c.Address = rec.Address
if account.Reverse != nil { // may not be reached: error json unmarshal
return nil, errors.New(`VLESS users: please use simplified outbound's config style to use "reverse"`)
}
+2
View File
@@ -66,6 +66,7 @@ type WireGuardConfig struct {
MTU int32 `json:"mtu"`
Reserved []byte `json:"reserved"`
DomainStrategy string `json:"domainStrategy"`
DNS []string `json:"remoteDNS"`
}
func (c *WireGuardConfig) Build() (proto.Message, error) {
@@ -141,6 +142,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
config.IsClient = c.IsClient
config.NoKernelTun = c.NoKernelTun
config.DNS = c.DNS
return config, nil
}
+36 -39
View File
@@ -15,6 +15,7 @@ import (
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/transport/internet"
)
@@ -140,7 +141,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
// TUN inbound doesn't need port configuration as it uses network interface instead
if strings.ToLower(c.Protocol) == "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
if c.PortList == nil {
return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.")
@@ -216,21 +217,11 @@ type OutboundDetourConfig struct {
Tag string `json:"tag"`
Settings *json.RawMessage `json:"settings"`
StreamSetting *StreamConfig `json:"streamSettings"`
ProxySettings *ProxyConfig `json:"proxySettings"`
ProxySettings *json.RawMessage `json:"proxySettings"`
MuxSettings *MuxConfig `json:"mux"`
TargetStrategy string `json:"targetStrategy"`
}
func (c *OutboundDetourConfig) checkChainProxyConfig() error {
if c.StreamSetting == nil || c.ProxySettings == nil || c.StreamSetting.SocketSettings == 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()
}
return nil
}
func requiresTransportSecurity(address *Address) bool {
if address == nil || address.Address == nil {
return false
@@ -257,7 +248,7 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
}
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
if requiresTransportSecurity(tjCfg.Address) {
if requiresTransportSecurity(tjCfg.Servers[0].Address) {
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
}
}
@@ -267,6 +258,10 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
// Build implements Buildable.
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.ProxySettings != nil {
return nil, errors.PrintRemovedFeatureError(`outbound "proxySettings"`, `"streamSettings.sockopt.dialerProxy"`)
}
senderSettings := &proxyman.SenderConfig{}
switch strings.ToLower(c.TargetStrategy) {
case "asis", "":
@@ -294,9 +289,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
default:
return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
}
if err := c.checkChainProxyConfig(); err != nil {
return nil, err
}
if c.SendThrough != nil {
address := ParseSendThough(c.SendThrough)
@@ -322,26 +314,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
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 {
ms, err := c.MuxSettings.Build()
if err != nil {
@@ -358,13 +330,38 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if err != nil {
return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err)
}
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
ts, err := rawConfig.(Buildable).Build()
if err != nil {
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{
SenderSettings: serial.ToTypedMessage(senderSettings),
+273 -139
View File
@@ -1,15 +1,18 @@
package main
import (
"errors"
"bytes"
"flag"
"fmt"
"go/build"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"mvdan.cc/gofumpt/format"
)
var (
@@ -23,101 +26,27 @@ var (
isFormat bool
)
// envFile returns the name of the Go environment configuration file.
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
func envFile() (string, error) {
if file := os.Getenv("GOENV"); file != "" {
if file == "off" {
return "", errors.New("GOENV=off")
}
return file, nil
}
dir, err := os.UserConfigDir()
func getModuleInfo(pwd string) (modPath, langVersion string, err error) {
data, err := os.ReadFile(filepath.Join(pwd, "go.mod"))
if err != nil {
return "", err
return "", "", err
}
if dir == "" {
return "", errors.New("missing user-config dir")
}
return filepath.Join(dir, "go", "env"), nil
}
// GetRuntimeEnv returns the value of runtime environment variable,
// that is set by running following command: `go env -w key=value`.
func GetRuntimeEnv(key string) (string, error) {
file, err := envFile()
if err != nil {
return "", err
}
if file == "" {
return "", errors.New("missing runtime env file")
}
var data []byte
var runtimeEnv string
data, readErr := os.ReadFile(file)
if readErr != nil {
return "", readErr
}
envStrings := strings.Split(string(data), "\n")
for _, envItem := range envStrings {
envItem = strings.TrimSuffix(envItem, "\r")
envKeyValue := strings.Split(envItem, "=")
if 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
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 {
switch fields[0] {
case "module":
modPath = fields[1]
case "go":
langVersion = "go" + strings.TrimPrefix(fields[1], "go")
}
<-maxTasks
}(file)
}
}
return formatRequired
return modPath, langVersion, nil
}
func formatGoSource(src []byte, opts format.Options) ([]byte, error) {
return format.Source(src, opts)
}
func main() {
@@ -150,26 +79,76 @@ func main() {
}
pwd := *directory
GOBIN := GetGOBIN()
binPath := os.Getenv("PATH")
pathSlice := []string{pwd, GOBIN, binPath}
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
os.Setenv("PATH", binPath)
suffix := ""
if runtime.GOOS == "windows" {
suffix = ".exe"
}
gofmt := "gofumpt" + suffix
if gofmtPath, err := exec.LookPath(gofmt); err != nil {
fmt.Println("Can not find", gofmt, "in system path or current working directory.")
modPath, langVersion, modErr := getModuleInfo(pwd)
if modErr != nil {
fmt.Println("Error reading go.mod:", modErr)
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 {
if err != nil {
fmt.Println(err)
@@ -186,51 +165,206 @@ func main() {
!strings.HasSuffix(filename, ".pb.go") &&
!strings.Contains(dir, filepath.Join("testing", "mocks")) &&
!strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) {
rawFilesSlice = append(rawFilesSlice, path)
jobs <- path
}
return nil
})
close(jobs)
wg.Wait()
if walkErr != nil {
fmt.Println(walkErr)
os.Exit(1)
}
if isFormat {
gofmtArgs := []string{
"-l", "-e", "-w",
}
if hasErrors.Load() {
os.Exit(1)
}
fmt.Println("Formatting Go source files...")
RunMany(gofmt, gofmtArgs, rawFilesSlice)
fmt.Println("Do NOT forget to commit file changes.")
if isFormat {
if formatRequired.Load() {
fmt.Println("Do NOT forget to commit file changes.")
}
}
if isCheck {
gofmtListArgs := []string{
"-l", "-e",
}
fmt.Println("Checking files thar are not properly formatted...")
formatRequired := RunMany(gofmt, gofmtListArgs, rawFilesSlice)
if formatRequired {
if formatRequired.Load() {
fmt.Println("Format problem(s) found.")
}
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.")
fmt.Println("Please run 'go run ./infra/vformat/main.go' to format the Go source files.")
os.Exit(1)
} else {
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
}
+31 -6
View File
@@ -2,12 +2,15 @@
package blackhole
import (
"bytes"
"context"
"net/http"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"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/session"
"github.com/xtls/xray-core/common/signal"
@@ -17,14 +20,34 @@ import (
// Handler is an outbound connection that silently swallow the entire payload.
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.
func New(ctx context.Context, config *Config) (*Handler, error) {
response, err := config.GetInternalResponse()
if err != nil {
return nil, err
response := []byte{}
if config.Response != nil {
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{
response: response,
@@ -37,8 +60,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob := outbounds[len(outbounds)-1]
ob.Name = "blackhole"
nBytes := h.response.WriteTo(link.Writer)
if nBytes > 0 {
if len(h.response) > 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.
time.Sleep(time.Second)
}
+44 -10
View File
@@ -1,12 +1,15 @@
package blackhole_test
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"net/http"
"testing"
"github.com/xtls/xray-core/common"
"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/proxy/blackhole"
"github.com/xtls/xray-core/transport"
@@ -16,27 +19,58 @@ import (
func TestBlackholeHTTPResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
handler, err := blackhole.New(ctx, &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
Response: &blackhole.Response{Type: "http"},
})
common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit())
var mb buf.MultiBuffer
var rerr error
dataCh := make(chan buf.MultiBuffer, 1)
go func() {
b, e := reader.ReadMultiBuffer()
mb = b
rerr = e
mb := common.Must2(reader.ReadMultiBuffer())
dataCh <- mb
}()
link := transport.Link{
Reader: reader,
Writer: writer,
}
common.Must(handler.Process(ctx, &link, nil))
mb := <-dataCh
data := make([]byte, mb.Len())
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 mb.IsEmpty() {
t.Error("expect http response, but nothing")
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
import (
serial "github.com/xtls/xray-core/common/serial"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -22,26 +21,28 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type NoneResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
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() {
*x = NoneResponse{}
func (x *Response) Reset() {
*x = Response{}
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NoneResponse) String() string {
func (x *Response) String() string {
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]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -53,57 +54,35 @@ func (x *NoneResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use NoneResponse.ProtoReflect.Descriptor instead.
func (*NoneResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0}
}
type HTTPResponse struct {
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]
func (x *Response) GetType() string {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
return x.Type
}
return mi.MessageOf(x)
return ""
}
// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead.
func (*HTTPResponse) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
func (x *Response) GetCustomResponseData() []byte {
if x != nil {
return x.CustomResponseData
}
return nil
}
type Config struct {
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
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*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.StoreMessageInfo(mi)
}
@@ -115,7 +94,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -128,10 +107,10 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
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 {
return x.Response
}
@@ -142,11 +121,12 @@ var File_proxy_blackhole_config_proto protoreflect.FileDescriptor
const file_proxy_blackhole_config_proto_rawDesc = "" +
"\n" +
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n" +
"\fNoneResponse\"\x0e\n" +
"\fHTTPResponse\"F\n" +
"\x06Config\x12<\n" +
"\bresponse\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\bresponseB^\n" +
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\"P\n" +
"\bResponse\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x120\n" +
"\x14custom_response_data\x18\x02 \x01(\fR\x12customResponseData\"D\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"
var (
@@ -161,15 +141,13 @@ func file_proxy_blackhole_config_proto_rawDescGZIP() []byte {
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{
(*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse
(*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse
(*Config)(nil), // 2: xray.proxy.blackhole.Config
(*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage
(*Response)(nil), // 0: xray.proxy.blackhole.Response
(*Config)(nil), // 1: xray.proxy.blackhole.Config
}
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 input_type
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(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumMessages: 2,
NumExtensions: 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_multiple_files = true;
import "common/serial/typed_message.proto";
message NoneResponse {}
message HTTPResponse {}
message Response {
string type = 1;
bytes custom_response_data = 2;
}
message Config {
xray.common.serial.TypedMessage response = 1;
Response response = 1;
}
+7 -14
View File
@@ -1,26 +1,19 @@
package blackhole_test
import (
"bufio"
"net/http"
"context"
"testing"
"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) {
buffer := buf.New()
httpResponse := new(HTTPResponse)
httpResponse.WriteTo(buf.NewWriter(buffer))
reader := bufio.NewReader(buffer)
response, err := http.ReadResponse(reader, nil)
handler, err := blackhole.New(context.Background(), &blackhole.Config{
Response: &blackhole.Response{Type: "http"},
})
common.Must(err)
if response.StatusCode != 403 {
t.Error("expected status code 403, but got ", response.StatusCode)
if handler == nil {
t.Error("expected HTTP response handler")
}
}
+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
// +build !linux
//go:build !linux && !openbsd
// +build !linux,!openbsd
package dokodemo
+96 -102
View File
@@ -53,6 +53,10 @@ func reloadEnvSettings() error {
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
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 {
return h.Init(config.(*Config), pm)
}); err != nil {
@@ -89,9 +93,11 @@ type FinalRule struct {
// Handler handles Freedom connections.
type Handler struct {
policyManager policy.Manager
config *Config
finalRules []*FinalRule
policyManager policy.Manager
config *Config
finalRules []*FinalRule
resolveStrategy internet.DomainStrategy
usesDialerProxy bool
}
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
@@ -168,22 +174,6 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
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 {
for _, rule := range h.finalRules {
if rule.Apply(network, address, port) {
@@ -196,17 +186,16 @@ func (h *Handler) matchFinalRule(network net.Network, address net.Address, port
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.
func (h *Handler) Init(config *Config, pm policy.Manager) error {
h.config = config
h.policyManager = pm
if h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply
if len(config.FinalRules) > 0 {
errors.LogWarning(context.Background(), `The "finalRules" setting is ignored when "sockopt.dialerProxy" is set, since freedom is not the final outbound.`)
}
return nil
}
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
for _, rc := range config.FinalRules {
rule, err := buildFinalRule(rc)
@@ -237,6 +226,20 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
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 {
if addr == nil {
return false
@@ -256,7 +259,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.Name = "freedom"
ob.CanSpliceCopy = 1
inbound := session.InboundFromContext(ctx)
defaultRule := getDefaultFinalRule(inbound)
var defaultRule *FinalRule
if !h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply (and the domain is not resolved)
defaultRule = getDefaultFinalRule(inbound)
}
destination := ob.Target
origTargetAddr := ob.OriginalTarget.Address
@@ -284,61 +290,53 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
var conn stat.Connection
var blockedDest *net.Destination
var blockedRule *FinalRule
firstResolve := true
err := retry.ExponentialBackoff(5, 100).On(func() error {
dialDest := destination
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
strategy := h.config.DomainStrategy
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
}
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
if err != nil {
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
if destination.Address.Family().IsDomain() {
if defaultRule != nil || len(h.finalRules) > 0 {
if strategy := h.resolveStrategy; strategy.HasStrategy() {
ips, err := internet.LookupForIP(destination.Address.Domain(), strategy, outGateway)
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())
if strategy.ForceIP() {
return err // retry
}
}
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
domain := dialDest.Address.Domain()
var ips []net.IP
if firstResolve {
firstResolve = false
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)
} else {
if rule := h.matchFinalRule(destination.Network, destination.Address, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &destination
blockedRule = rule
return nil
}
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 {
return err
}
@@ -350,20 +348,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return errors.New("failed to open connection to ", destination).Base(err)
}
if blockedDest != nil {
delay := h.blockDelay(blockedRule)
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
return h.blackhole(ctx, input, output, blockedRule, blockedDest)
}
if destination.Address.Family().IsDomain() && (defaultRule != nil || len(h.finalRules) > 0) {
// 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 {
version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr()
@@ -408,7 +403,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
writer = buf.NewWriter(conn)
}
} else {
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination)
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
if h.config.Noises != nil {
errors.LogDebug(ctx, "NOISE", h.config.Noises)
writer = &NoisePacketWriter{
@@ -512,7 +507,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
}
udpAddr := d.(*net.UDPAddr)
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
}
b.Resize(0, int32(n))
@@ -537,7 +532,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
}
// 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
statConn, ok := iConn.(*stat.CounterConnection)
if ok {
@@ -561,9 +556,8 @@ func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
DefaultRule: defaultRule,
UDPOverride: UDPOverride,
ResolvedUDPAddr: resolvedUDPAddr,
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
OutGateway: outGateway,
}
}
return &buf.SequentialWriter{Writer: conn}
}
@@ -580,7 +574,7 @@ type PacketWriter struct {
// Resulting in these packets being sent to many different IPs randomly
// So, cache and keep the resolve result
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
LocalAddr net.Address
OutGateway net.Address
}
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
@@ -603,21 +597,21 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
b.UDP.Address = ip
} else {
ShouldUseSystemResolver := true
if w.Handler.config.DomainStrategy.HasStrategy() {
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr)
shouldUseSystemResolver := true
if strategy := w.Handler.resolveStrategy; strategy.HasStrategy() {
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), strategy, w.OutGateway)
if err != nil {
// drop packet if resolve failed when forceIP
if w.Handler.config.DomainStrategy.ForceIP() {
if strategy.ForceIP() {
b.Release()
continue
}
} else {
ip = net.IPAddress(ips[dice.Roll(len(ips))])
ShouldUseSystemResolver = false
shouldUseSystemResolver = false
}
}
if ShouldUseSystemResolver {
if shouldUseSystemResolver {
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
if err != nil {
b.Release()
@@ -631,7 +625,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()
continue
}
+6 -3
View File
@@ -173,15 +173,18 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
if inbound == nil || ob == nil {
return nil, errors.New("missing inbound or outbound metadata from context")
var src net.Destination
if inbound != nil {
src = inbound.Source
} else {
src = net.TCPDestination(net.AnyIP, 0)
}
data := struct {
Source net.Destination
Target net.Destination
}{
Source: inbound.Source,
Source: src,
Target: ob.Target,
}
+5 -9
View File
@@ -115,11 +115,7 @@ Start:
request, err := http.ReadRequest(reader)
if err != nil {
trace := errors.New("failed to read http request").Base(err)
if errors.Cause(err) != io.EOF && !isTimeout(errors.Cause(err)) {
trace.AtWarning()
}
return trace
return errors.New("failed to read http request").Base(err)
}
if len(s.config.Accounts) > 0 {
@@ -147,7 +143,7 @@ Start:
}
dest, err := http_proto.ParseHost(host, defaultPort)
if err != nil {
return errors.New("malformed proxy host: ", host).AtWarning().Base(err)
return errors.New("malformed proxy host: ", host).Base(err)
}
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
From: conn.RemoteAddr(),
@@ -262,7 +258,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
requestWriter := buf.NewBufferedWriter(link.Writer)
common.Must(requestWriter.SetBuffered(false))
if err := request.Write(requestWriter); err != nil {
return errors.New("failed to write whole request").Base(err).AtWarning()
return errors.New("failed to write whole request").Base(err)
}
return nil
}
@@ -299,7 +295,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
response.Header.Set("Proxy-Connection", "close")
}
if err := response.Write(writer); err != nil {
return errors.New("failed to write response").Base(err).AtWarning()
return errors.New("failed to write response").Base(err)
}
return nil
}
@@ -332,7 +328,7 @@ func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer
return nil, errors.New("failed to read http 1xx response").Base(err)
}
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
}
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())
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
conn, err := dialer.Dial(hysteria.ContextWithDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination)
if err != nil {
return errors.New("failed to find an available destination").AtWarning().Base(err)
return errors.New("failed to find an available destination").Base(err)
}
defer conn.Close()
errors.LogInfo(ctx, "tunneling request to ", target, " via ", target.Network, ":", c.server.Destination.NetAddr())
+2 -2
View File
@@ -40,11 +40,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
for _, user := range config.Users {
u, err := user.ToMemoryUser()
if err != nil {
return nil, errors.New("failed to get hysteria user").Base(err).AtError()
return nil, errors.New("failed to get hysteria user").Base(err)
}
if err := validator.Add(u); err != nil {
return nil, errors.New("failed to add user").Base(err).AtError()
return nil, errors.New("failed to add user").Base(err)
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ func (l *Loopback) init(config *Config, dispatcherInstance routing.Dispatcher) e
if config.Sniffing.GetEnabled() {
request, err := proxyman.BuildSniffingRequest(config.Sniffing)
if err != nil {
return errors.New("failed to build loopback sniffing request").Base(err).AtError()
return errors.New("failed to build loopback sniffing request").Base(err)
}
l.sniffingRequest = request
}
+2 -2
View File
@@ -71,7 +71,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
return nil
})
if err != nil {
return errors.New("failed to find an available destination").AtWarning().Base(err)
return errors.New("failed to find an available destination").Base(err)
}
errors.LogInfo(ctx, "tunneling request to ", destination, " via ", network, ":", server.Destination.NetAddr())
@@ -124,7 +124,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
}
if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
return errors.New("failed to write A request payload").Base(err).AtWarning()
return errors.New("failed to write A request payload").Base(err)
}
if err := bufferedWriter.SetBuffered(false); err != nil {
+2 -2
View File
@@ -98,7 +98,7 @@ func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHe
iv := append([]byte(nil), buffer.BytesTo(ivLen)...)
r, err = account.Cipher.NewDecryptionReader(account.Key, iv, reader)
if err != nil {
return nil, nil, drain.WithError(drainer, reader, errors.New("failed to initialize decoding stream").Base(err).AtError())
return nil, nil, drain.WithError(drainer, reader, errors.New("failed to initialize decoding stream").Base(err))
}
}
}
@@ -146,7 +146,7 @@ func WriteTCPRequest(request *protocol.RequestHeader, writer io.Writer) (buf.Wri
w, err := account.Cipher.NewEncryptionWriter(account.Key, iv, writer)
if err != nil {
return nil, errors.New("failed to create encoding stream").Base(err).AtError()
return nil, errors.New("failed to create encoding stream").Base(err)
}
header := buf.New()
+3 -3
View File
@@ -34,11 +34,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
for _, user := range config.Users {
u, err := user.ToMemoryUser()
if err != nil {
return nil, errors.New("failed to get shadowsocks user").Base(err).AtError()
return nil, errors.New("failed to get shadowsocks user").Base(err)
}
if err := validator.Add(u); err != nil {
return nil, errors.New("failed to add user").Base(err).AtError()
return nil, errors.New("failed to add user").Base(err)
}
}
@@ -200,7 +200,7 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
func (s *Server) handleConnection(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
sessionPolicy := s.policyManager.ForLevel(0)
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
return errors.New("unable to set read deadline").Base(err).AtWarning()
return errors.New("unable to set read deadline").Base(err)
}
bufferedReader := buf.BufferedReader{Reader: buf.NewReader(conn)}
+1 -1
View File
@@ -59,7 +59,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
}
u, err := user.ToMemoryUser()
if err != nil {
return nil, errors.New("failed to get shadowsocks user").Base(err).AtError()
return nil, errors.New("failed to get shadowsocks user").Base(err)
}
memUsers = append(memUsers, u)
}
+1
View File
@@ -82,6 +82,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
if err != nil {
return errors.New("failed to connect to server").Base(err)
}
defer connection.Close()
if session.TimeoutOnlyFromContext(ctx) {
ctx, _ = context.WithCancel(context.Background())
+1 -1
View File
@@ -105,7 +105,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
}
udpRequest, err := ClientHandshake(request, conn, conn)
if err != nil {
return errors.New("failed to establish connection to server").AtWarning().Base(err)
return errors.New("failed to establish connection to server").Base(err)
}
if udpRequest != nil {
if udpRequest.Address == net.AnyIP || udpRequest.Address == net.AnyIPv6 {
+2 -2
View File
@@ -458,10 +458,10 @@ func ClientHandshake(request *protocol.RequestHeader, reader io.Reader, writer i
}
if b.Byte(0) != socks5Version {
return nil, errors.New("unexpected server version: ", b.Byte(0)).AtWarning()
return nil, errors.New("unexpected server version: ", b.Byte(0))
}
if b.Byte(1) != authByte {
return nil, errors.New("auth method not supported.").AtWarning()
return nil, errors.New("auth method not supported.")
}
if authByte == authPassword {
+5 -5
View File
@@ -69,7 +69,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
return nil
})
if err != nil {
return errors.New("failed to find an available destination").AtWarning().Base(err)
return errors.New("failed to find an available destination").Base(err)
}
errors.LogInfo(ctx, "tunneling request to ", destination, " via ", server.Destination.NetAddr())
@@ -116,21 +116,21 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
// write some request payload to buffer
if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
return errors.New("failed to write A request payload").Base(err).AtWarning()
return errors.New("failed to write A request payload").Base(err)
}
// Flush; bufferWriter.WriteMultiBuffer now is bufferWriter.writer.WriteMultiBuffer
if err = bufferWriter.SetBuffered(false); err != nil {
return errors.New("failed to flush payload").Base(err).AtWarning()
return errors.New("failed to flush payload").Base(err)
}
// Send header if not sent yet
if _, err = connWriter.Write([]byte{}); err != nil {
return err.(*errors.Error).AtWarning()
return err
}
if err = buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to transfer request payload").Base(err).AtInfo()
return errors.New("failed to transfer request payload").Base(err)
}
return nil
+12 -12
View File
@@ -47,11 +47,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
for _, user := range config.Users {
u, err := user.ToMemoryUser()
if err != nil {
return nil, errors.New("failed to get trojan user").Base(err).AtError()
return nil, errors.New("failed to get trojan user").Base(err)
}
if err := validator.Add(u); err != nil {
return nil, errors.New("failed to add user").Base(err).AtError()
return nil, errors.New("failed to add user").Base(err)
}
}
@@ -151,7 +151,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
sessionPolicy := s.policyManager.ForLevel(0)
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
return errors.New("unable to set read deadline").Base(err).AtWarning()
return errors.New("unable to set read deadline").Base(err)
}
first := buf.FromBytes(make([]byte, buf.Size))
@@ -219,7 +219,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
destination := clientReader.Target
if err := conn.SetReadDeadline(time.Time{}); err != nil {
return errors.New("unable to set read deadline").Base(err).AtWarning()
return errors.New("unable to set read deadline").Base(err)
}
inbound := session.InboundFromContext(ctx)
@@ -402,7 +402,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
}
apfb := napfb[name]
if apfb == nil {
return errors.New(`failed to find the default "name" config`).AtWarning()
return errors.New(`failed to find the default "name" config`)
}
if apfb[alpn] == nil {
@@ -410,7 +410,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
}
pfb := apfb[alpn]
if pfb == nil {
return errors.New(`failed to find the default "alpn" config`).AtWarning()
return errors.New(`failed to find the default "alpn" config`)
}
path := ""
@@ -444,7 +444,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
}
fb := pfb[path]
if fb == nil {
return errors.New(`failed to find the default "path" config`).AtWarning()
return errors.New(`failed to find the default "path" config`)
}
ctx, cancel := context.WithCancel(ctx)
@@ -460,7 +460,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
}
return nil
}); err != nil {
return errors.New("failed to dial to " + fb.Dest).Base(err).AtWarning()
return errors.New("failed to dial to " + fb.Dest).Base(err)
}
defer conn.Close()
@@ -520,11 +520,11 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
common.Must2(pro.Write([]byte{byte(p1 >> 8), byte(p1), byte(p2 >> 8), byte(p2)}))
}
if err := serverWriter.WriteMultiBuffer(buf.MultiBuffer{pro}); err != nil {
return errors.New("failed to set PROXY protocol v", fb.Xver).Base(err).AtWarning()
return errors.New("failed to set PROXY protocol v", fb.Xver).Base(err)
}
}
if err := buf.Copy(reader, serverWriter, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to fallback request payload").Base(err).AtInfo()
return errors.New("failed to fallback request payload").Base(err)
}
return nil
}
@@ -534,7 +534,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
getResponse := func() error {
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
if err := buf.Copy(serverReader, writer, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to deliver response payload").Base(err).AtInfo()
return errors.New("failed to deliver response payload").Base(err)
}
return nil
}
@@ -542,7 +542,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), task.OnSuccess(getResponse, task.Close(writer))); err != nil {
common.Must(common.Interrupt(serverReader))
common.Must(common.Interrupt(serverWriter))
return errors.New("fallback ends").Base(err).AtInfo()
return errors.New("fallback ends").Base(err)
}
return nil
+136 -5
View File
@@ -11,6 +11,8 @@ import (
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/xtls/xray-core/common/buf"
@@ -38,21 +40,111 @@ const (
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
)
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
type DarwinTun struct {
tunFile *os.File
options *Config
tunFd int
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
routeMonitorOnce sync.Once
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 (
_ Tun = (*DarwinTun)(nil)
_ GVisorDevice = (*DarwinTun)(nil)
@@ -77,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
options: options,
tunFd: fd,
ownsFd: false,
waitKq: newWaitKqueue(fd),
}, nil
}
@@ -103,6 +196,7 @@ func NewTun(options *Config) (Tun, error) {
options: options,
tunFd: int(tunFile.Fd()),
ownsFd: true,
waitKq: newWaitKqueue(int(tunFile.Fd())),
gateway: gateway,
}, nil
}
@@ -134,6 +228,9 @@ func (t *DarwinTun) Close() error {
_ = t.routeMonitor.Close()
}
})
if t.waitKq != nil {
t.waitKq.close()
}
routeErr := t.unsetSystemRoutes()
if t.ownsFd {
return xerrors.Combine(routeErr, t.tunFile.Close())
@@ -242,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
}), 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() {
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) {
+184
View File
@@ -3,7 +3,11 @@
package tun
import (
"sync"
"testing"
"time"
"golang.org/x/sys/unix"
)
func TestSelectDarwinGatewayDefault(t *testing.T) {
@@ -47,3 +51,183 @@ func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
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
import (
"context"
"errors"
"net"
_ "unsafe"
"net/netip"
"os"
"slices"
"sync"
"unsafe"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"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
func procyield(cycles uint32)
type FreeBSDTun struct {
device tun.Device
mtu uint32
device tun.Device
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 (
@@ -34,20 +66,172 @@ var (
// NewTun builds new tun interface handler
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))
if err != nil {
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 {
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
}
// 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 {
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) {
@@ -55,21 +239,13 @@ func (t *FreeBSDTun) Name() (string, error) {
}
func (t *FreeBSDTun) Index() (int, error) {
name, err := t.Name()
if err != nil {
return 0, err
}
iface, err := net.InterfaceByName(name)
if err != nil {
return 0, err
}
return iface.Index, nil
return t.tunIndex, nil
}
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// 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()
// 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
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// 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
n, err := b.ReadFrom(t.device.File())
@@ -141,23 +317,551 @@ func (t *FreeBSDTun) Wait() {
}
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
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName == "" {
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
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 {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
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
}
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")
}

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