Compare commits

...
55 Commits
Author SHA1 Message Date
Fangliding e1d4a04fb4 Fix potential panic 2026-07-11 03:39:24 +08:00
ivolframandGitHub 1aabe7ea78 XHTTP transport: Do not force trailing path / when both sessionID and seq are placed elsewhere (#6307)
https://github.com/XTLS/Xray-core/discussions/6385#discussioncomment-17454120

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

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

---------

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

---------

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

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-07-07 01:26:15 +00:00
yiguodevandGitHub 0495b17650 TUN inbound: Refine gateway and autoSystemRoutingTable on macOS (#6434)
https://github.com/XTLS/Xray-core/pull/6434#issuecomment-4885521586
2026-07-07 00:28:50 +00:00
yiguodevandGitHub 65f6f0a43b TUN inbound: Refine gateway and autoSystemRoutingTable on Linux (#6398)
https://github.com/XTLS/Xray-core/pull/6398#issuecomment-4880261591
2026-07-04 02:25:15 +00:00
Jasper344612andGitHub 3263ae9255 TUN inbound: Fix autoOutboundsInterface on Linux (#6413)
Fixes https://github.com/XTLS/Xray-core/issues/6412
2026-07-04 01:38:25 +00:00
LjhAUMEMandGitHub 3dc8bf3d8b Hysteria inbound: Fix dynamic UUID not accepted (#6395)
Fixes https://github.com/XTLS/Xray-core/pull/6375#issuecomment-4825923934
2026-07-04 01:28:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
695e68ef9e Bump github.com/pires/go-proxyproto from 0.12.0 to 0.14.0 (#6420)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.12.0 to 0.14.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.12.0...v0.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 17:53:15 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dfdbcf86cc Bump github.com/klauspost/cpuid/v2 from 2.3.0 to 2.4.0 (#6417)
Bumps [github.com/klauspost/cpuid/v2](https://github.com/klauspost/cpuid) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/klauspost/cpuid/releases)
- [Commits](https://github.com/klauspost/cpuid/compare/v2.3.0...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 17:45:51 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2b828b7bc2 Bump google.golang.org/grpc from 1.81.1 to 1.82.0 (#6415)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 17:45:47 +00:00
RPRXandGitHub 45cf2898ab Xray-core v26.6.27
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-06-27 13:18:03 +00:00
RPRXandGitHub 18b85adb4e XHTTP client: Change default maxConnections to 6 for anti-RKN
"xmux": {
    "maxConcurrency": 0,
    "maxConnections": "6",
    "cMaxReuseTimes": 0,
    "hMaxRequestTimes": "600-900",
    "hMaxReusableSecs": "1800-3000",
    "hKeepAlivePeriod": 0
}

Replaces https://github.com/XTLS/Xray-core/commit/9cc7907234a8297a87a0ff77fc40db373b74a0f2 and https://github.com/XTLS/Xray-core/commit/4ce65fc74c4c50919b10b3faff9725f75bba5d73

Closes https://github.com/XTLS/Xray-core/issues/6376#issuecomment-4817033592
2026-06-27 12:41:39 +00:00
452b719504 Hysteria inbound: Support routing's vlessRoute as well (#6375)
https://github.com/XTLS/Xray-core/pull/6375#issuecomment-4795522284

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-06-27 12:04:58 +00:00
345c76f9a8 WireGuard inbound: Support dynamic peer management (#6360)
https://github.com/XTLS/Xray-core/pull/6360#issuecomment-4780311547

Closes https://github.com/XTLS/Xray-core/issues/6314

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-06-27 11:41:22 +00:00
f496437b84 XHTTP server: Refactor upload_queue.go (#6372)
https://github.com/XTLS/Xray-core/pull/6372#issuecomment-4801395378

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-27 10:40:58 +00:00
b12bc504c8 README.md: Add Magic_V2Ray to Magisk in Installation (#6355)
https://github.com/XTLS/Xray-core/pull/6355#issuecomment-4793603321

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-24 21:06:24 +00:00
yiguodevandGitHub dda2b10c9d TUN inbound: Add traffic counters; Metrics: Rely on instance (#6349)
https://github.com/XTLS/Xray-core/pull/6349#issuecomment-4775121300
2026-06-24 12:00:22 +00:00
Jasper344612andGitHub 241aa38ac0 TUN inbound: Support autoSystemRoutingTable and autoOutboundsInterface on macOS and Linux as well (#6366)
https://github.com/XTLS/Xray-core/pull/6366#issuecomment-4788510365
2026-06-24 11:06:00 +00:00
风扇滑翔翼andRPRX 7e7e820763 Geodata: Apply uTLS Chrome fingerprint when downloading (#6371)
Closes https://github.com/XTLS/Xray-core/issues/6369
2026-06-24 10:52:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f9eb1597ad Bump actions/cache from 5 to 6 (#6368)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 10:48:21 +00:00
风扇滑翔翼andGitHub ac04c445bd DNS: Fix unexpected TTL clamp (#6363)
Fixes https://github.com/XTLS/Xray-core/issues/6359
2026-06-23 11:48:17 +00:00
e7e9254630 TUN inbound: Avoid panic on nil RemoteAddr due to quickly closed connection (#6365)
Fixes https://github.com/XTLS/Xray-core/issues/6364#issuecomment-4778262075

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-23 11:37:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fab4bcc1ed Bump github.com/cloudflare/circl from 1.6.3 to 1.6.4 (#6362)
Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.3 to 1.6.4.
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.6.3...v1.6.4)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/circl
  dependency-version: 1.6.4
  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-06-23 10:34:07 +00:00
RPRXandGitHub b99c3e5657 Xray-core v26.6.22
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-06-22 18:55:10 +00:00
OmoebaandGitHub 583bb4a63f XHTTP server: Fix scStreamUpServerSecs when xPaddingObfsMode is true (#6343)
https://github.com/XTLS/Xray-core/pull/6343#issuecomment-4771666966
2026-06-22 18:51:47 +00:00
Жора ЗмейкинandRPRX 9cd9382e3d TUN inbound: Support env XRAY_TUN_FD on Linux as well (#6338)
https://github.com/XTLS/Xray-core/pull/6338#issuecomment-4770945163
2026-06-22 17:38:36 +00:00
patternihaRPRXcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
567500c4af Fragment finalmask: Add lengths and delays (#6334)
Usage: https://github.com/XTLS/Xray-core/pull/6334#issue-4685556394

Behavior: https://github.com/XTLS/Xray-core/pull/6334#issuecomment-4751547750

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-22 17:38:12 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5aefcb41fb Bump github.com/pion/stun/v3 from 3.1.5 to 3.1.6 (#6357)
Bumps [github.com/pion/stun/v3](https://github.com/pion/stun) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pion/stun/releases)
- [Commits](https://github.com/pion/stun/compare/v3.1.5...v3.1.6)

---
updated-dependencies:
- dependency-name: github.com/pion/stun/v3
  dependency-version: 3.1.6
  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-06-22 16:05:47 +00:00
MeowandGitHub be8009c625 Geodata: Cleanup unneeded matchers & domain: ignore case (#6342)
Completes https://github.com/XTLS/Xray-core/pull/6139
2026-06-19 12:02:27 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8734774e4a Bump actions/checkout from 6 to 7 (#6344)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 11:23:41 +00:00
风扇滑翔翼andGitHub 1e036ce1c5 XHTTP/3 client: Actively close underlying QUIC & UDP (#6332)
Fixes https://github.com/XTLS/Xray-core/issues/6328#issuecomment-4730379021
2026-06-18 23:55:11 +00:00
j2rong4cnandGitHub c815c2f2df Loopback outbound: Add sniffing (#6326)
Example: https://github.com/XTLS/Xray-core/pull/6326#issue-4659701786
2026-06-18 23:17:01 +00:00
bytecategoryandGitHub 986c512e0f XHTTP client: Avoid panic when host is invalid (#6316)
Fixes https://github.com/XTLS/Xray-core/issues/6315
2026-06-18 22:55:18 +00:00
MeowandGitHub 711aea4e34 XHTTP & WS & HU & gRPC servers: Require sockopt.trustedXForwardedFor (#6309)
https://github.com/XTLS/Xray-core/pull/6258#issuecomment-4663652131

Behavior: https://github.com/XTLS/Xray-core/pull/6258#issuecomment-4746598275

Replaces https://github.com/XTLS/Xray-core/pull/6159
2026-06-18 22:31:21 +00:00
风扇滑翔翼andRPRX 6412738486 Socks5 inbound: Fix issues in new UDP ASSOCIATE (#6325)
https://github.com/XTLS/Xray-core/pull/6325#issuecomment-4724008713

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

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-17 14:44:40 +00:00
LjhAUMEMandRPRX ad2e4cb0e1 Finalmask: Fix unexpected order and UDP's buf issue (#6331)
https://github.com/XTLS/Xray-docs-next/pull/866#issuecomment-4729859528

And https://github.com/XTLS/Xray-core/pull/6331#issuecomment-4730527410

Fixes https://github.com/XTLS/Xray-core/issues/6184#issuecomment-4725831023
2026-06-17 14:44:12 +00:00
829d54d7be Hysteria & XHTTP/3 clients: udpHop supports dialerProxy (#6320)
https://github.com/XTLS/Xray-core/pull/6320#issuecomment-4725679616

Fixes https://github.com/XTLS/Xray-core/pull/6320#issuecomment-4699599655

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-06-17 12:20:28 +00:00
LjhAUMEMandRPRX 862631172d WireGuard proxy: Refactor (#6287)
And https://github.com/XTLS/Xray-core/pull/6303#issuecomment-4669158076

Fixes https://github.com/XTLS/Xray-core/issues/6257
2026-06-17 12:02:10 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d27b3e46e2 Bump golang.org/x/net from 0.55.0 to 0.56.0 (#6310)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0.
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.56.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-06-16 11:45:27 +00:00
LjhAUMEMandGitHub da21a8f77f TUN & WireGuard inbounds: Ignore b.UDP's domain when receiving it from outbound (#6285)
Fixes https://github.com/XTLS/Xray-core/issues/6279
2026-06-09 20:53:55 +00:00
e10347bf01 XHTTP transport: Add sessionIDTable and sessionIDLength; Rename session* to sessionID* (#6258)
https://github.com/XTLS/Xray-core/pull/6258#issuecomment-4658534046
https://github.com/XTLS/Xray-core/pull/6253#issuecomment-4657704004
https://github.com/XTLS/Xray-core/pull/6251#issuecomment-4612756220

Usage: https://github.com/XTLS/Xray-core/pull/6258#issue-4580617110

Closes https://github.com/XTLS/Xray-core/issues/6264

---------

Co-authored-by: XXcipherX <knazevvv6514@gmail.com>
2026-06-09 15:58:02 +00:00
𐲓𐳛𐳪𐳂𐳐 𐲀𐳢𐳦𐳫𐳢 𐲥𐳔𐳛𐳪𐳌𐳑𐳖𐳇andGitHub 26a022c905 GitHub Action CI, README.md: Refinements and add compliance contents (#6283)
And https://github.com/XTLS/Xray-core/pull/6283#issuecomment-4640162879
2026-06-09 12:08:38 +00:00
𐲓𐳛𐳪𐳂𐳐 𐲀𐳢𐳦𐳫𐳢 𐲥𐳔𐳛𐳪𐳌𐳑𐳖𐳇andGitHub 95e9816223 Chore: Limit sing* dependencies to shadowsocks_2022 only (#6286)
https://github.com/XTLS/Xray-core/pull/6286#issuecomment-4658968225
2026-06-09 10:55:42 +00:00
风扇滑翔翼andGitHub 3239d21168 TUN inbound: autoOutboundsInterface bypasses loopback addresses (#6276)
Fixes https://github.com/XTLS/Xray-core/issues/6269
2026-06-09 09:40:55 +00:00
风扇滑翔翼andGitHub 06b4931743 TUN inbound: Start TUN by AlwaysOnInboundHandler (#6275)
Fixes https://github.com/XTLS/Xray-core/issues/6274
2026-06-09 09:22:33 +00:00
LjhAUMEMandGitHub 6189d2bfd5 XICMP finalmask: Refine Linux sever (#6272)
Fixes https://github.com/XTLS/Xray-core/pull/6168
2026-06-09 09:14:54 +00:00
a0e9347f1b TLS ECH: Handle "h2c://" query correctly (#6261)
Fixes https://github.com/XTLS/Xray-core/issues/6259#issuecomment-4614984919

---------

Co-authored-by: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com>
2026-06-09 09:03:24 +00:00
IconHHwandGitHub 83cf229909 Salamander finalmask: Replace math/rand with crypto/rand in salt generation (#6228)
And https://github.com/XTLS/Xray-core/pull/6228#issuecomment-4612712100

Fixes https://github.com/XTLS/Xray-core/pull/6228#issuecomment-4599037015
2026-06-08 19:55:06 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2249f8b5c6 Bump github.com/pion/stun/v3 from 3.1.2 to 3.1.5 (#6291)
Bumps [github.com/pion/stun/v3](https://github.com/pion/stun) from 3.1.2 to 3.1.5.
- [Release notes](https://github.com/pion/stun/releases)
- [Commits](https://github.com/pion/stun/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: github.com/pion/stun/v3
  dependency-version: 3.1.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-06-08 19:29:14 +00:00
fdb9b616fc README.md: Add Hey to HarmonyOS in GUI Clients (#6244)
https://github.com/popsiclelmlm/Hey/blob/main/entry/src/main/cpp/README.md

---------

Co-authored-by: liumin01 <liumin01@roborock.com>
2026-06-02 23:58:56 +00:00
𐲓𐳛𐳪𐳂𐳐 𐲀𐳢𐳦𐳫𐳢 𐲥𐳔𐳛𐳪𐳌𐳑𐳖𐳇andGitHub d792fba59c GitHub Actions CI: Builders run only once in pull requests in the same repository (#6222)
https://github.com/XTLS/Xray-core/pull/6221#issuecomment-4588919827

Completes https://github.com/XTLS/Xray-core/pull/6090#issuecomment-4572094191
2026-06-02 23:40:17 +00:00
55956f8d70 TLS config: Remove some deprecated fields (#6226)
https://t.me/projectXtls/1490

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-02 23:36:42 +00:00
143 changed files with 7721 additions and 5161 deletions
+1
View File
@@ -0,0 +1 @@
powershell.exe -ExecutionPolicy Bypass -File ".\xray_no_window.ps1"
+1
View File
@@ -0,0 +1 @@
Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden
+1
View File
@@ -0,0 +1 @@
CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
echo "LATEST=$LATEST" >>${GITHUB_ENV}
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
+12 -8
View File
@@ -11,15 +11,16 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -75,13 +76,14 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env:
GOOS: ${{ matrix.goos}}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Show workflow information
run: |
@@ -117,13 +119,13 @@ jobs:
# go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -132,15 +134,17 @@ jobs:
run: |
mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then
echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
cp .github/build/windows/* build_assets/
fi
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi
if [[ ${GOARCH} == '386' ]]; then
mv resources/wintun/bin/x86/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun
fi
- name: Copy README.md & LICENSE
+12 -8
View File
@@ -11,15 +11,16 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -161,6 +162,7 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
@@ -168,7 +170,7 @@ jobs:
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up NDK
if: matrix.goos == 'android'
@@ -223,14 +225,14 @@ jobs:
fi
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
if: matrix.goos == 'windows'
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -239,8 +241,10 @@ jobs:
run: |
mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then
echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
cp .github/build/windows/* build_assets/
fi
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi
@@ -250,7 +254,7 @@ jobs:
if [[ ${GOARCH} == 'arm64' ]]; then
mv resources/wintun/bin/arm64/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun
fi
- name: Copy README.md & LICENSE
+16 -9
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
@@ -59,7 +59,7 @@ jobs:
done
- name: Save Geodat Cache
uses: actions/cache/save@v5
uses: actions/cache/save@v6
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
@@ -68,9 +68,12 @@ jobs:
wintun:
if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
ASSETVER: 0.14.1
ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
steps:
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -96,7 +99,6 @@ jobs:
echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..."
if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then
echo -e "wintun.dll for ${ARCHITECTURE} exists"
continue
else
echo -e "wintun.dll for ${ARCHITECTURE} is missing"
missing=true
@@ -113,16 +115,21 @@ jobs:
fi
if [[ "$missing" == true ]]; then
FILENAME=wintun.zip
DOWNLOAD_FILE=wintun-0.14.1.zip
DOWNLOAD_FILE=wintun-${ASSETVER}.zip
echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..."
curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}"
echo -e "Unpacking wintun..."
unzip -u ${FILENAME} -d resources/
echo "unhit=true" >> $GITHUB_OUTPUT
if [[ "$(sha256sum "./${FILENAME}" | awk -F ' ' '{print $1}')" == "${ASSETHASH}" ]]; then
echo -e "Unpacking wintun..."
unzip -u ${FILENAME} -d resources/
echo "unhit=true" >> $GITHUB_OUTPUT
else
echo -e "Digest of ${FILENAME} mismatch."
exit 1
fi
fi
- name: Save Wintun Cache
uses: actions/cache/save@v5
uses: actions/cache/save@v6
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
+5 -5
View File
@@ -11,7 +11,7 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
@@ -40,7 +40,7 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
@@ -59,7 +59,7 @@ jobs:
contents: read
steps:
- name: Checkout codebase
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
with:
@@ -83,14 +83,14 @@ jobs:
os: [windows-latest, ubuntu-latest, macos-latest]
steps:
- name: Checkout codebase
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
+24
View File
@@ -73,6 +73,7 @@
- [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless)
- [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- Magisk
- [Magic_V2Ray](https://github.com/vincentng295/Magic_V2Ray)
- [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
- Homebrew
- `brew install xray`
@@ -145,6 +146,8 @@
- [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- HarmonyOS
- [Hey](https://github.com/popsiclelmlm/Hey)
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
@@ -184,6 +187,27 @@
- [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases).
- For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod).
### Bundled Third-Party Components Redistribution
**Certain optional features dynamically load third-party components. These optional components are separate works distributed under their own licenses, and are bundled into the ZIP package for ease of use. Users may replace these components under the licenses from these components.**
These components include:
#### Wintun
This distribution contains unmodified official precompiled and pre-signed Wintun binaries.
- Project: Wintun
- Copyright: Copyright (C) 2018-2021 WireGuard LLC. All Rights Reserved.
- Redistribution License: Prebuilt Binaries License (PBL) bundled with official precompiled and pre-signed binaries from wintun.net
- Component(s): wintun.dll
- Source: https://www.wintun.net/
- Included in:
- Windows x86 (windows-32, win7-32)
- Windows x86-64 (windows-64, win7-64)
- Windows AArch64 (windows-arm64)
- Notes: Wintun is an optional runtime-loaded component only used for TUN inbound functionality on supported Windows platforms.
## One-line Compilation
### Windows (PowerShell)
+7 -2
View File
@@ -198,9 +198,14 @@ func parseResponse(payload []byte) (*IPRecord, error) {
ipRecord := &IPRecord{
ReqID: h.ID,
RCode: h.RCode,
Expire: now.Add(time.Second * dns_feature.DefaultTTL),
RawHeader: &h,
}
defer func() {
// set to default TTL if no valid TTL is found
if ipRecord.Expire.IsZero() {
ipRecord.Expire = now.Add(time.Second * dns_feature.DefaultTTL)
}
}()
L:
for {
@@ -217,7 +222,7 @@ L:
ttl = 1
}
expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.After(expire) {
if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) {
ipRecord.Expire = expire
}
+85 -43
View File
@@ -2,6 +2,7 @@ package geodata
import (
"context"
"crypto/tls"
go_errors "errors"
"io"
"net/http"
@@ -9,6 +10,7 @@ import (
"path/filepath"
"time"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
@@ -16,6 +18,7 @@ import (
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged"
"golang.org/x/net/http2"
)
const idleTimeout = 30 * time.Second
@@ -26,8 +29,9 @@ type stage struct {
}
type downloader struct {
ctx context.Context
client *http.Client
ctx context.Context
httpClient *http.Client
httpsClient *http.Client
}
type idleConn struct {
@@ -53,52 +57,84 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
client: newClient(ctx, dispatcher, outbound),
ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
}
}
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn
err := task.Run(ctx, func() error {
if tagged.Dialer == nil {
return errors.New("tagged dialer is not initialized")
}
dest, err := net.ParseDestination(network + ":" + address)
if err != nil {
return errors.New("cannot understand address").Base(err)
}
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
if err != nil {
return errors.New("cannot dial remote address ", dest).Base(err)
}
conn = c
return nil
})
if err != nil {
return nil, errors.New("cannot finish connection").Base(err)
}
return &idleConn{
Conn: conn,
}, nil
},
TLSHandshakeTimeout: idleTimeout,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client {
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn
err := task.Run(ctx, func() error {
if tagged.Dialer == nil {
return errors.New("tagged dialer is not initialized")
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
dest, err := net.ParseDestination(network + ":" + address)
if err != nil {
return errors.New("cannot understand address").Base(err)
}
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
if err != nil {
return errors.New("cannot dial remote address ", dest).Base(err)
}
conn = c
return nil
},
})
if err != nil {
return nil, errors.New("cannot finish connection").Base(err)
}
return &idleConn{
Conn: conn,
}, nil
}
if isHTTPS {
return &http.Client{
Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
conn, err := dial(ctx, network, address)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(address)
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
} else {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: dial,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
}
@@ -160,7 +196,13 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
}
utils.TryDefaultHeadersWith(req.Header, "nav")
resp, err := d.client.Do(req)
var client *http.Client
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
if err != nil {
return err
}
+151 -59
View File
@@ -2,15 +2,18 @@ package metrics
import (
"context"
"encoding/json"
stderrors "errors"
"expvar"
stdnet "net"
"net/http"
_ "net/http/pprof"
"net/http/pprof"
"strings"
"github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/extension"
@@ -21,15 +24,17 @@ import (
type MetricsHandler struct {
ohm outbound.Manager
statsManager feature_stats.Manager
observatory extension.Observatory
ctx context.Context
tag string
listen string
tcpListener net.Listener
tcpListener xnet.Listener
listener *OutboundListener
}
// NewMetricsHandler creates a new MetricsHandler based on the given config.
func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) {
c := &MetricsHandler{
ctx: ctx,
tag: config.Tag,
listen: config.Listen,
}
@@ -37,46 +42,6 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er
c.statsManager = sm
c.ohm = om
}))
expvar.Publish("stats", expvar.Func(func() interface{} {
resp := map[string]map[string]map[string]int64{
"inbound": {},
"outbound": {},
"user": {},
}
c.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>")
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
if item, found := resp[typeName][tagOrUser]; found {
item[direction] = counter.Value()
} else {
resp[typeName][tagOrUser] = map[string]int64{
direction: counter.Value(),
}
}
return true
})
return resp
}))
expvar.Publish("observatory", expvar.Func(func() interface{} {
if c.observatory == nil {
common.Must(core.RequireFeatures(ctx, func(observatory extension.Observatory) error {
c.observatory = observatory
return nil
}))
if c.observatory == nil {
return nil
}
}
resp := map[string]*observatory.OutboundStatus{}
if o, err := c.observatory.GetObservation(context.Background()); err != nil {
return err
} else {
for _, x := range o.(*observatory.ObservationResult).GetStatus() {
resp[x.OutboundTag] = x
}
}
return resp
}))
return c, nil
}
@@ -85,45 +50,172 @@ func (p *MetricsHandler) Type() interface{} {
}
func (p *MetricsHandler) Start() error {
handler := p.httpHandler()
// direct listen a port if listen is set
if p.listen != "" {
TCPlistener, err := net.Listen("tcp", p.listen)
TCPlistener, err := xnet.Listen("tcp", p.listen)
if err != nil {
return err
}
p.tcpListener = TCPlistener
errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen)
go func() {
if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
go p.serve(TCPlistener, handler)
}
if p.tag == "" {
if p.tcpListener == nil {
return errors.New("metrics must have a tag or listen address")
}
return nil
}
listener := &OutboundListener{
buffer: make(chan net.Conn, 4),
buffer: make(chan xnet.Conn, 4),
done: done.New(),
}
p.listener = listener
go func() {
if err := http.Serve(listener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
go p.serve(listener, handler)
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove existing handler")
}
return p.ohm.AddHandler(context.Background(), &Outbound{
if err := p.ohm.AddHandler(context.Background(), &Outbound{
tag: p.tag,
listener: listener,
})
}); err != nil {
if closeErr := p.Close(); closeErr != nil {
errors.LogErrorInner(context.Background(), closeErr, "failed to close metrics server after start failure")
}
return err
}
return nil
}
func (p *MetricsHandler) Close() error {
return nil
var errs []error
if p.tcpListener != nil {
errs = append(errs, p.tcpListener.Close())
p.tcpListener = nil
}
if p.listener != nil {
errs = append(errs, p.listener.Close())
p.listener = nil
}
if p.ohm != nil && p.tag != "" {
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove metrics handler")
}
}
return errors.Combine(errs...)
}
func (p *MetricsHandler) serve(listener xnet.Listener, handler http.Handler) {
if err := http.Serve(listener, handler); err != nil && !isClosedListenerError(err) {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}
func isClosedListenerError(err error) bool {
if err == nil {
return true
}
if stderrors.Is(err, stdnet.ErrClosed) || stderrors.Is(err, http.ErrServerClosed) {
return true
}
errText := err.Error()
return strings.Contains(errText, "listen closed") ||
strings.Contains(errText, "use of closed network connection")
}
func (p *MetricsHandler) httpHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/debug/vars", p.handleDebugVars)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return mux
}
func (p *MetricsHandler) handleDebugVars(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
vars := map[string]json.RawMessage{}
expvar.Do(func(kv expvar.KeyValue) {
value := json.RawMessage(kv.Value.String())
if !json.Valid(value) {
value = json.RawMessage("null")
}
vars[kv.Key] = value
})
vars["stats"] = marshalJSON(p.stats())
vars["observatory"] = marshalJSON(p.observatoryStatus())
payload, err := json.Marshal(vars)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(payload)
}
func marshalJSON(value interface{}) json.RawMessage {
data, err := json.Marshal(value)
if err != nil {
return json.RawMessage("null")
}
return data
}
func (p *MetricsHandler) stats() map[string]map[string]map[string]int64 {
resp := map[string]map[string]map[string]int64{
"inbound": {},
"outbound": {},
"user": {},
}
p.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>")
if len(nameSplit) < 4 {
return true
}
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
items, found := resp[typeName]
if !found {
items = map[string]map[string]int64{}
resp[typeName] = items
}
if item, found := items[tagOrUser]; found {
item[direction] = counter.Value()
} else {
items[tagOrUser] = map[string]int64{
direction: counter.Value(),
}
}
return true
})
return resp
}
func (p *MetricsHandler) observatoryStatus() interface{} {
feature := core.MustFromContext(p.ctx).GetFeature(extension.ObservatoryType())
if feature == nil {
return nil
}
observatoryFeature := feature.(extension.Observatory)
resp := map[string]*observatory.OutboundStatus{}
if o, err := observatoryFeature.GetObservation(context.Background()); err != nil {
return err
} else {
for _, x := range o.(*observatory.ObservationResult).GetStatus() {
resp[x.OutboundTag] = x
}
}
return resp
}
func init() {
+161
View File
@@ -0,0 +1,161 @@
package metrics
import (
"context"
"encoding/json"
stdnet "net"
"net/http"
"net/http/httptest"
"testing"
"github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/app/proxyman"
_ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound"
appstats "github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
feature_outbound "github.com/xtls/xray-core/features/outbound"
)
func TestMetricsCanRestartInSameProcess(t *testing.T) {
for i := 0; i < 2; i++ {
server := startMetricsTestServer(t)
readMetricsVars(t, server)
readMetricsPprof(t, server)
if err := server.Close(); err != nil {
t.Fatalf("failed to close metrics server: %v", err)
}
}
}
func TestMetricsCanRunMultipleInstancesInSameProcess(t *testing.T) {
server1 := startMetricsTestServer(t)
t.Cleanup(func() {
_ = server1.Close()
})
server2 := startMetricsTestServer(t)
t.Cleanup(func() {
_ = server2.Close()
})
readMetricsVars(t, server1)
readMetricsVars(t, server2)
}
func TestMetricsListenOnlyWithoutTagDoesNotRegisterOutbound(t *testing.T) {
listen := pickMetricsListenAddress(t)
server := startMetricsTestServerWithMetricsConfig(t, &Config{
Listen: listen,
})
t.Cleanup(func() {
_ = server.Close()
})
response, err := http.Get("http://" + listen + "/debug/vars")
if err != nil {
t.Fatalf("failed to read listen-only metrics: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("unexpected listen-only metrics status: %d", response.StatusCode)
}
outboundManager := server.GetFeature(feature_outbound.ManagerType()).(feature_outbound.Manager)
if handlers := outboundManager.ListHandlers(context.Background()); len(handlers) != 0 {
t.Fatalf("listen-only metrics registered outbound handlers: got %d, want 0", len(handlers))
}
}
func startMetricsTestServer(t *testing.T) *core.Instance {
return startMetricsTestServerWithMetricsConfig(t, &Config{
Tag: "metrics_out",
})
}
func startMetricsTestServerWithMetricsConfig(t *testing.T, metricsConfig *Config) *core.Instance {
t.Helper()
server, err := core.New(metricsTestConfig(metricsConfig))
if err != nil {
t.Fatalf("failed to create metrics server: %v", err)
}
if err := server.Start(); err != nil {
_ = server.Close()
t.Fatalf("failed to start metrics server: %v", err)
}
return server
}
func metricsTestConfig(metricsConfig *Config) *core.Config {
return &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&appstats.Config{}),
serial.ToTypedMessage(metricsConfig),
},
}
}
func pickMetricsListenAddress(t *testing.T) string {
t.Helper()
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to pick metrics listen address: %v", err)
}
defer listener.Close()
return listener.Addr().String()
}
func readMetricsVars(t *testing.T, server *core.Instance) {
t.Helper()
recorder := httptest.NewRecorder()
metricsHandler(t, server).httpHandler().ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/debug/vars", nil),
)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected metrics vars status: %d", recorder.Code)
}
var payload map[string]interface{}
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
t.Fatalf("failed to decode metrics vars: %v", err)
}
if _, found := payload["stats"]; !found {
t.Fatal("metrics vars missing stats")
}
if _, found := payload["observatory"]; !found {
t.Fatal("metrics vars missing observatory")
}
}
func readMetricsPprof(t *testing.T, server *core.Instance) {
t.Helper()
recorder := httptest.NewRecorder()
metricsHandler(t, server).httpHandler().ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/debug/pprof/goroutine?debug=1", nil),
)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected metrics pprof status: %d", recorder.Code)
}
}
func metricsHandler(t *testing.T, server *core.Instance) *MetricsHandler {
t.Helper()
feature := server.GetFeature((*MetricsHandler)(nil))
handler, ok := feature.(*MetricsHandler)
if !ok || handler == nil {
t.Fatal("metrics handler not registered")
}
return handler
}
+22 -14
View File
@@ -57,16 +57,23 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
if err != nil {
return nil, err
}
// Set tag and sniffing config in context before creating proxy
// This allows proxies like TUN to access these settings
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag})
if receiverConfig.SniffingSettings != nil {
ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: sniffingRequest,
})
src := net.TCPDestination(net.AnyIP, 0)
if receiverConfig.Listen != nil {
src.Address = receiverConfig.Listen.AsAddress()
}
rawProxy, err := common.CreateObject(ctx, proxyConfig)
if receiverConfig.PortList != nil && len(receiverConfig.PortList.Range) > 0 {
src.Port = net.Port(receiverConfig.PortList.Range[0].From)
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
}
newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src})
newCtx = session.ContextWithContent(newCtx, &session.Content{SniffingRequest: sniffingRequest})
newCtx = session.ContextWithStreamSettings(newCtx, mss)
rawProxy, err := common.CreateObject(newCtx, proxyConfig)
if err != nil {
return nil, err
}
@@ -92,11 +99,6 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
address = net.AnyIP
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
}
if receiverConfig.ReceiveOriginalDestination {
if mss.SocketSettings == nil {
mss.SocketSettings = &internet.SocketConfig{}
@@ -170,6 +172,12 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
// Start implements common.Runnable.
func (h *AlwaysOnInboundHandler) Start() error {
// for inbound without worker (TUN)
if run, ok := h.proxy.(common.Runnable); ok {
if err := run.Start(); err != nil {
return errors.New("failed to start proxy").Base(err)
}
}
for _, worker := range h.workers {
if err := worker.Start(); err != nil {
return err
+3 -7
View File
@@ -6,7 +6,6 @@ import (
goerrors "errors"
"io"
"math/big"
"os"
"github.com/xtls/xray-core/common/dice"
@@ -109,7 +108,9 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
ctx = session.ContextWithFullHandler(ctx, h)
rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings)
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig)
if err != nil {
return nil, err
}
@@ -306,11 +307,6 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
}
}
if conn, err := h.getUoTConnection(ctx, dest); err != os.ErrInvalid {
return conn, err
}
conn, err := internet.Dial(ctx, dest, h.streamSettings)
-35
View File
@@ -1,35 +0,0 @@
package outbound
import (
"context"
"os"
"github.com/sagernet/sing/common/uot"
"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/stat"
)
func (h *Handler) getUoTConnection(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if dest.Address == nil {
return nil, errors.New("nil destination address")
}
if !dest.Address.Family().IsDomain() {
return nil, os.ErrInvalid
}
var uotVersion int
if dest.Address.Domain() == uot.MagicAddress {
uotVersion = uot.Version
} else if dest.Address.Domain() == uot.LegacyMagicAddress {
uotVersion = uot.LegacyVersion
} else {
return nil, os.ErrInvalid
}
packetConn, err := internet.ListenSystemPacket(ctx, &net.UDPAddr{IP: net.AnyIP.IP(), Port: 0}, h.streamSettings.SocketSettings)
if err != nil {
return nil, errors.New("unable to listen socket").Base(err)
}
conn := uot.NewServerConn(packetConn, uotVersion)
return h.getStatCouterConnection(conn), nil
}
+49
View File
@@ -0,0 +1,49 @@
package geodata
import (
"sync"
"github.com/xtls/xray-core/common"
)
var privateIPMatcher = sync.OnceValue(func() IPMatcher {
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
"ff00::/8",
}))))
})
func GetPrivateIPMatcher() IPMatcher { return privateIPMatcher() }
var privateDomainMatcher = sync.OnceValue(func() DomainMatcher {
return common.Must2(DomainReg.BuildDomainMatcher(common.Must2(ParseDomainRules([]string{
"lan",
"localdomain",
"example",
"invalid",
"localhost",
"test",
"local",
"home.arpa",
"internal",
"regexp:^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", // Dotless domains
}, Domain_Domain))))
})
func GetPrivateDomainMatcher() DomainMatcher { return privateDomainMatcher() }
+1 -1
View File
@@ -220,7 +220,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
case Domain_Regex:
return strmatcher.Regex.New(d.Value)
case Domain_Domain:
return strmatcher.Domain.New(d.Value)
return strmatcher.Domain.New(strings.ToLower(d.Value))
case Domain_Full:
return strmatcher.Full.New(strings.ToLower(d.Value))
default:
+15 -7
View File
@@ -6,12 +6,14 @@ import (
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
type DomainRegistry struct {
mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
matchers *utils.WeakCacheMap[uuid.UUID, DynamicDomainMatcher]
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
@@ -24,7 +26,7 @@ func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher,
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers = append(r.matchers, d)
r.matchers.Store(uuid.New(), d)
return d, nil
}
@@ -32,15 +34,20 @@ func (r *DomainRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
var matchers []*DynamicDomainMatcher
r.matchers.Range(func(_ uuid.UUID, matcher *DynamicDomainMatcher) bool {
matchers = append(matchers, matcher)
return true
})
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(matchers), " domain matcher(s)")
factory := newDomainMatcherFactory()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
reloaded := make([]reloadEntry, len(matchers))
for i, d := range matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
@@ -52,13 +59,14 @@ func (r *DomainRegistry) Reload() error {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(matchers), " domain matcher(s)")
return nil
}
func newDomainRegistry() *DomainRegistry {
return &DomainRegistry{
factory: newDomainMatcherFactory(),
factory: newDomainMatcherFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicDomainMatcher](),
}
}
+19 -11
View File
@@ -7,25 +7,27 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
mu sync.Mutex
factory *IPSetFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicIPMatcher]
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
m, err := buildOptimizedIPMatcher(r.factory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers = append(r.matchers, d)
r.matchers.Store(uuid.New(), d)
return d, nil
}
@@ -33,15 +35,20 @@ func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
var matchers []*DynamicIPMatcher
r.matchers.Range(func(_ uuid.UUID, matcher *DynamicIPMatcher) bool {
matchers = append(matchers, matcher)
return true
})
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(matchers), " IP matcher(s)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
reloaded := make([]reloadEntry, len(matchers))
for i, d := range matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
@@ -52,14 +59,15 @@ func (r *IPRegistry) Reload() error {
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(matchers), " IP matcher(s)")
return nil
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
ipsetFactory: newIPSetFactory(),
factory: newIPSetFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicIPMatcher](),
}
}
+2 -2
View File
@@ -138,7 +138,7 @@ func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:"} {
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
@@ -167,7 +167,7 @@ func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, e
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:"} {
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
+348
View File
@@ -0,0 +1,348 @@
//go:build darwin
package net
import (
"bytes"
"net"
"net/netip"
"path/filepath"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/errors"
)
const (
darwinProcPIDListFDs = 1
darwinProcPIDFDSocketInfo = 3
darwinProcFDTypeSocket = 2
darwinProcFDInfoSize = 8
darwinSocketFDInfoSize = 792
darwinSocketFDInfoPSIOff = 24
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
darwinInSockInfoSize = 80
darwinInSockInfoIPv4 = 0x1
darwinInSockInfoIPv6 = 0x2
darwinSockInfoIN = 1
darwinSockInfoTCP = 2
)
type darwinSocketMatchLevel int
const (
darwinSocketNoMatch darwinSocketMatchLevel = iota
darwinSocketPortMatch
darwinSocketRemoteMatch
darwinSocketLocalMatch
darwinSocketExactMatch
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
isLocal, err := IsLocal(net.ParseIP(srcIP))
if err != nil {
return 0, "", "", errors.New("failed to determine if address is local: ", err)
}
if !isLocal {
return 0, "", "", ErrNotLocal
}
if network != "tcp" && network != "udp" {
panic("Unsupported network type for process lookup.")
}
srcAddr, err := netip.ParseAddr(srcIP)
if err != nil {
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
}
srcAddr = srcAddr.Unmap()
var dstAddr netip.Addr
hasDstAddr := false
if destIP != "" && destPort != 0 {
dstAddr, err = netip.ParseAddr(destIP)
if err != nil {
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
}
dstAddr = dstAddr.Unmap()
hasDstAddr = true
}
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
if err != nil {
return 0, "", "", errors.New("failed to list processes").Base(err)
}
var bestPID int32
bestLevel := darwinSocketNoMatch
ambiguousBest := false
for _, process := range processes {
pid := process.Proc.P_pid
if pid <= 0 {
continue
}
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
if err != nil || matchLevel == darwinSocketNoMatch {
continue
}
if matchLevel == darwinSocketExactMatch {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
break
}
if matchLevel > bestLevel {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
continue
}
if matchLevel == bestLevel {
ambiguousBest = true
}
}
if bestLevel == darwinSocketNoMatch {
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
if ambiguousBest {
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
absPath, err := darwinProcessPath(bestPID)
if err != nil {
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
}
absPath = filepath.ToSlash(absPath)
return int(bestPID), filepath.Base(absPath), absPath, nil
}
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
fds, err := darwinProcessFDs(pid)
if err != nil {
return darwinSocketNoMatch, err
}
bestLevel := darwinSocketNoMatch
info := make([]byte, darwinSocketFDInfoSize)
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
if fdType != darwinProcFDTypeSocket {
continue
}
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
continue
}
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
if level == darwinSocketExactMatch {
return level, nil
}
if level > bestLevel {
bestLevel = level
}
}
return bestLevel, nil
}
func darwinProcessFDs(pid int32) ([]byte, error) {
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
if err != nil {
return nil, err
}
if n <= 0 {
return nil, nil
}
buf := make([]byte, n)
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
switch network {
case "tcp":
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
return darwinSocketNoMatch
}
case "udp":
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
return darwinSocketNoMatch
}
default:
return darwinSocketNoMatch
}
vflag := info[darwinInSockInfoVFlagOff]
if srcAddr.Is4() {
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
return darwinSocketNoMatch
}
} else {
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
return darwinSocketNoMatch
}
}
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
if !darwinPortMatches(localPort, srcPort) {
return darwinSocketNoMatch
}
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
if !hasDstAddr {
if localAddrMatches {
return darwinSocketExactMatch
}
return darwinSocketNoMatch
}
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
return darwinSocketExactMatch
}
switch {
case localAddrMatches && remoteMatches:
return darwinSocketExactMatch
case localAddrMatches:
return darwinSocketLocalMatch
case remoteMatches:
return darwinSocketRemoteMatch
default:
return darwinSocketPortMatch
}
}
func darwinPortMatches(value int32, port uint16) bool {
raw := uint16(value)
return raw == port || darwinNtohs(raw) == port
}
func darwinNtohs(value uint16) uint16 {
return value<<8 | value>>8
}
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
if addr.Is4() {
ip := addr.As4()
return bytes.Equal(raw[12:16], ip[:])
}
ip := addr.As16()
return bytes.Equal(raw, ip[:])
}
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
if darwinAddrMatches(raw, addr) {
return true
}
if addr.Is4() {
return darwinBytesAreZero(raw[12:16])
}
return darwinBytesAreZero(raw)
}
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
}
func darwinBytesAreZero(raw []byte) bool {
for _, value := range raw {
if value != 0 {
return false
}
}
return true
}
func darwinReadNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func darwinProcessPath(pid int32) (string, error) {
buf := make([]byte, unix.PathMax)
n, err := darwinProcPIDPath(pid, buf)
if err != nil {
return "", err
}
if n <= 0 {
return "", errors.New("empty process path")
}
return strings.TrimRight(string(buf[:n]), "\x00"), nil
}
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
var libc_proc_pidinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
var libc_proc_pidfdinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
var libc_proc_pidpath_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
// Implemented in the runtime package (runtime/sys_darwin.go).
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
//go:linkname syscall_syscall6 syscall.syscall6
+18
View File
@@ -0,0 +1,18 @@
//go:build darwin
#include "textflag.h"
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidinfo(SB)
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidfdinfo(SB)
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidpath(SB)
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)
+293
View File
@@ -0,0 +1,293 @@
//go:build darwin
package net
import (
stdnet "net"
"net/netip"
"os"
"testing"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
func TestFindProcessDarwinTCP(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
remote := conn.RemoteAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinUDP(t *testing.T) {
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
local := conn.LocalAddr().(*stdnet.UDPAddr)
pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinNonLocal(t *testing.T) {
_, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0)
if err != ErrNotLocal {
t.Fatalf("expected ErrNotLocal, got %v", err)
}
}
func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic")
}
}()
_, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0)
}
func assertCurrentProcess(t *testing.T, pid int, name string, path string) {
t.Helper()
if pid != os.Getpid() {
t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path)
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if path == "" || name == "" {
t.Fatalf("expected process path and name, got name=%q path=%q", name, path)
}
if sameFile(executable, path) {
return
}
t.Fatalf("expected executable %q, got %q", executable, path)
}
func sameFile(left string, right string) bool {
leftInfo, leftErr := os.Stat(left)
rightInfo, rightErr := os.Stat(right)
if leftErr != nil || rightErr != nil {
return false
}
return os.SameFile(leftInfo, rightInfo)
}
func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
src := netip.MustParseAddr("198.18.0.2")
dst := netip.MustParseAddr("203.0.113.10")
otherLocal := netip.MustParseAddr("192.168.1.10")
otherRemote := netip.MustParseAddr("198.51.100.10")
unspecifiedLocal := netip.MustParseAddr("0.0.0.0")
tests := []struct {
name string
local netip.Addr
remote netip.Addr
hasDst bool
wantLevel darwinSocketMatchLevel
}{
{
name: "exact",
local: src,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local with matching remote",
local: unspecifiedLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local without destination",
local: unspecifiedLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketExactMatch,
},
{
name: "local match with different remote",
local: src,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketLocalMatch,
},
{
name: "remote match with different local",
local: otherLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketRemoteMatch,
},
{
name: "port only with destination",
local: otherLocal,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketPortMatch,
},
{
name: "different local without destination",
local: otherLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketNoMatch,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443)
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst)
if level != test.wantLevel {
t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel)
}
})
}
}
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
info := make([]byte, darwinSocketFDInfoSize)
switch network {
case "tcp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP))
case "udp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN))
}
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET))
info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4
writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort))
writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort))
copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local)
copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote)
return info
}
func writeDarwinNativeUint32(b []byte, offset int, value uint32) {
*(*uint32)(unsafe.Pointer(&b[offset])) = value
}
func copyDarwinIPv4(dst []byte, addr netip.Addr) {
ip := addr.As4()
copy(dst[12:16], ip[:])
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows && !linux && !android
//go:build !windows && !linux && !android && !darwin
package net
+2 -10
View File
@@ -28,8 +28,6 @@ const (
SecurityType_AUTO SecurityType = 2
SecurityType_AES128_GCM SecurityType = 3
SecurityType_CHACHA20_POLY1305 SecurityType = 4
SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06]
SecurityType_ZERO SecurityType = 6
)
// Enum value maps for SecurityType.
@@ -39,16 +37,12 @@ var (
2: "AUTO",
3: "AES128_GCM",
4: "CHACHA20_POLY1305",
5: "NONE",
6: "ZERO",
}
SecurityType_value = map[string]int32{
"UNKNOWN": 0,
"AUTO": 2,
"AES128_GCM": 3,
"CHACHA20_POLY1305": 4,
"NONE": 5,
"ZERO": 6,
}
)
@@ -129,15 +123,13 @@ const file_common_protocol_headers_proto_rawDesc = "" +
"\n" +
"\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" +
"\x0eSecurityConfig\x126\n" +
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" +
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*L\n" +
"\fSecurityType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\b\n" +
"\x04AUTO\x10\x02\x12\x0e\n" +
"\n" +
"AES128_GCM\x10\x03\x12\x15\n" +
"\x11CHACHA20_POLY1305\x10\x04\x12\b\n" +
"\x04NONE\x10\x05\x12\b\n" +
"\x04ZERO\x10\x06B^\n" +
"\x11CHACHA20_POLY1305\x10\x04B^\n" +
"\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3"
var (
-2
View File
@@ -11,8 +11,6 @@ enum SecurityType {
AUTO = 2;
AES128_GCM = 3;
CHACHA20_POLY1305 = 4;
NONE = 5; // [DEPRECATED 2023-06]
ZERO = 6;
}
message SecurityConfig {
+26 -10
View File
@@ -1,25 +1,41 @@
package http
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it.
func ParseXForwardedFor(header http.Header) []net.Address {
xff := header.Get("X-Forwarded-For")
if xff == "" {
return nil
// ApplyTrustedXForwardedFor returns remoteAddr overridden by X-Forwarded-For only when a configured trusted header is present.
func ApplyTrustedXForwardedFor(header http.Header, trusted []string, remoteAddr net.Addr) net.Addr {
value := header.Get("X-Forwarded-For")
if value == "" {
return remoteAddr
}
list := strings.Split(xff, ",")
addrs := make([]net.Address, 0, len(list))
for _, proxy := range list {
addrs = append(addrs, net.ParseAddress(proxy))
for _, t := range trusted {
if len(header.Values(t)) > 0 {
if idx := strings.IndexByte(value, ','); idx >= 0 {
value = value[:idx]
}
if addr := net.ParseAddress(value); addr.Family().IsIP() {
return &net.TCPAddr{
IP: addr.IP(),
Port: 0,
}
}
return remoteAddr
}
}
return addrs
if len(trusted) == 0 {
errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`)
} else {
errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value)
}
return remoteAddr
}
// RemoveHopByHopHeaders removes hop by hop headers in http header list.
+33 -8
View File
@@ -2,23 +2,48 @@ package http_test
import (
"bufio"
gonet "net"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/common/protocol/http"
)
func TestParseXForwardedFor(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
addrs := ParseXForwardedFor(header)
if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" {
t.Error(r)
}
func TestApplyTrustedXForwardedFor(t *testing.T) {
remoteAddr := &gonet.TCPAddr{IP: gonet.ParseIP("127.0.0.1"), Port: 12345}
t.Run("ignore X-Forwarded-For without trusted header", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
if addr := ApplyTrustedXForwardedFor(header, nil, remoteAddr); addr != remoteAddr {
t.Fatalf("unexpected remote address: %v", addr)
}
})
t.Run("trust X-Forwarded-For", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
header.Add("X-Trusted-CDN", "")
addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr)
if addr.String() != "129.78.138.66:0" {
t.Fatalf("unexpected remote address: %v", addr)
}
})
t.Run("ignore non-IP X-Forwarded-For", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "example.com")
header.Add("X-Trusted-CDN", "")
if addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr); addr != remoteAddr {
t.Fatalf("unexpected remote address: %v", addr)
}
})
}
func TestHopByHopHeadersRemoving(t *testing.T) {
+3 -1
View File
@@ -143,7 +143,9 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
+10
View File
@@ -26,6 +26,8 @@ const (
fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler
mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer
mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer
streamSettingsKey ctx.SessionKey = 13
)
func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context {
@@ -192,3 +194,11 @@ func MitmServerNameFromContext(ctx context.Context) string {
}
return ""
}
func ContextWithStreamSettings(ctx context.Context, streamSettings any) context.Context {
return context.WithValue(ctx, streamSettingsKey, streamSettings)
}
func StreamSettingsFromContext(ctx context.Context) any {
return ctx.Value(streamSettingsKey)
}
+14
View File
@@ -1,6 +1,7 @@
package utils
import (
"maps"
"runtime"
"sync"
"weak"
@@ -43,3 +44,16 @@ func (c *WeakCacheMap[K, V]) Store(key K, value *V) {
}
}, struct{}{})
}
func (c *WeakCacheMap[K, V]) Range(f func(K, *V) bool) {
c.mu.Lock()
snapshot := maps.Clone(c.m)
c.mu.Unlock()
for k, v := range snapshot {
if value := v.Value(); value != nil {
if !f(k, value) {
break
}
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 6
Version_z byte = 1
Version_z byte = 27
)
var (
+17 -16
View File
@@ -4,16 +4,17 @@ go 1.26
require (
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.3
github.com/cloudflare/circl v1.6.4
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.3.0
github.com/klauspost/cpuid/v2 v2.4.0
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.2
github.com/pires/go-proxyproto v0.12.0
github.com/pion/stun/v3 v3.1.6
github.com/pires/go-proxyproto v0.14.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
@@ -22,15 +23,15 @@ require (
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.51.0
golang.org/x/crypto v0.53.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.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.81.1
google.golang.org/grpc v1.82.0
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
@@ -44,18 +45,18 @@ require (
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.2 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.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.35.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+32 -32
View File
@@ -4,8 +4,8 @@ github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkd
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
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=
@@ -33,8 +33,8 @@ 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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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/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=
@@ -45,16 +45,16 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
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.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
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/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.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
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/pires/go-proxyproto v0.14.0 h1:2vIGIfVG8eVRsKF0xukEoeT5RWhDXxBU0uv6smLOKdI=
github.com/pires/go-proxyproto v0.14.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/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -98,22 +98,22 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
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/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,21 +121,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
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,10 +147,10 @@ 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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
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.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-41
View File
@@ -1,41 +0,0 @@
package conf
import (
"github.com/xtls/xray-core/transport/internet/grpc"
"google.golang.org/protobuf/proto"
)
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
func (g *GRPCConfig) Build() (proto.Message, error) {
if g.IdleTimeout <= 0 {
g.IdleTimeout = 0
}
if g.HealthCheckTimeout <= 0 {
g.HealthCheckTimeout = 0
}
if g.InitialWindowsSize < 0 {
// default window size of gRPC-go
g.InitialWindowsSize = 0
}
return &grpc.Config{
Authority: g.Authority,
ServiceName: g.ServiceName,
MultiMode: g.MultiMode,
IdleTimeout: g.IdleTimeout,
HealthCheckTimeout: g.HealthCheckTimeout,
PermitWithoutStream: g.PermitWithoutStream,
InitialWindowsSize: g.InitialWindowsSize,
UserAgent: g.UserAgent,
}, nil
}
+4 -1
View File
@@ -22,7 +22,6 @@ func (c *HysteriaClientConfig) Build() (proto.Message, error) {
}
config := &hysteria.ClientConfig{}
config.Version = c.Version
config.Server = &protocol.ServerEndpoint{
Address: c.Address.Build(),
Port: uint32(c.Port),
@@ -44,6 +43,10 @@ type HysteriaServerConfig struct {
}
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
if c.Version != 2 {
return nil, errors.New("version != 2")
}
config := new(hysteria.ServerConfig)
if c.Clients != nil {
+12 -2
View File
@@ -1,14 +1,24 @@
package conf
import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/proxy/loopback"
"google.golang.org/protobuf/proto"
)
type LoopbackConfig struct {
InboundTag string `json:"inboundTag"`
InboundTag string `json:"inboundTag"`
Sniffing *SniffingConfig `json:"sniffing"`
}
func (l LoopbackConfig) Build() (proto.Message, error) {
return &loopback.Config{InboundTag: l.InboundTag}, nil
c := &loopback.Config{InboundTag: l.InboundTag}
if l.Sniffing != nil {
sc, err := l.Sniffing.Build()
if err != nil {
return nil, errors.New("failed to build sniffing config").Base(err)
}
c.Sniffing = sc
}
return c, nil
}
+19 -29
View File
@@ -24,8 +24,6 @@ func cipherFromString(c string) shadowsocks.CipherType {
return shadowsocks.CipherType_CHACHA20_POLY1305
case "xchacha20-poly1305", "aead_xchacha20_poly1305", "xchacha20-ietf-poly1305":
return shadowsocks.CipherType_XCHACHA20_POLY1305
case "none", "plain":
return shadowsocks.CipherType_NONE
default:
return shadowsocks.CipherType_UNKNOWN
}
@@ -179,26 +177,22 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
}
type ShadowsocksServerTarget struct {
Address *Address `json:"address"`
Port uint16 `json:"port"`
Level byte `json:"level"`
Email string `json:"email"`
Cipher string `json:"method"`
Password string `json:"password"`
UoT bool `json:"uot"`
UoTVersion int `json:"uotVersion"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
Level byte `json:"level"`
Email string `json:"email"`
Cipher string `json:"method"`
Password string `json:"password"`
}
type ShadowsocksClientConfig struct {
Address *Address `json:"address"`
Port uint16 `json:"port"`
Level byte `json:"level"`
Email string `json:"email"`
Cipher string `json:"method"`
Password string `json:"password"`
UoT bool `json:"uot"`
UoTVersion int `json:"uotVersion"`
Servers []*ShadowsocksServerTarget `json:"servers"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
Level byte `json:"level"`
Email string `json:"email"`
Cipher string `json:"method"`
Password string `json:"password"`
Servers []*ShadowsocksServerTarget `json:"servers"`
}
func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
@@ -207,14 +201,12 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
if v.Address != nil {
v.Servers = []*ShadowsocksServerTarget{
{
Address: v.Address,
Port: v.Port,
Level: v.Level,
Email: v.Email,
Cipher: v.Cipher,
Password: v.Password,
UoT: v.UoT,
UoTVersion: v.UoTVersion,
Address: v.Address,
Port: v.Port,
Level: v.Level,
Email: v.Email,
Cipher: v.Cipher,
Password: v.Password,
},
}
}
@@ -240,8 +232,6 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
config.Port = uint32(server.Port)
config.Method = server.Cipher
config.Key = server.Password
config.UdpOverTcp = server.UoT
config.UdpOverTcpVersion = uint32(server.UoTVersion)
return config, nil
}
}
-208
View File
@@ -1,208 +0,0 @@
package conf
import (
"sort"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet/headers/http"
"github.com/xtls/xray-core/transport/internet/headers/noop"
"google.golang.org/protobuf/proto"
)
type NoOpConnectionAuthenticator struct{}
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
return new(noop.ConnectionConfig), nil
}
type AuthenticatorRequest struct {
Version string `json:"version"`
Method string `json:"method"`
Path StringList `json:"path"`
Headers map[string]*StringList `json:"headers"`
}
func sortMapKeys(m map[string]*StringList) []string {
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
config := &http.RequestConfig{
Uri: []string{"/"},
Header: []*http.Header{
{
Name: "Host",
Value: []string{"www.baidu.com", "www.bing.com"},
},
{
Name: "User-Agent",
Value: []string{utils.ChromeUA},
},
{
Name: "Sec-CH-UA",
Value: []string{utils.ChromeUACH},
},
{
Name: "Sec-CH-UA-Mobile",
Value: []string{"?0"},
},
{
Name: "Sec-CH-UA-Platform",
Value: []string{"Windows"},
},
{
Name: "Sec-Fetch-Mode",
Value: []string{"no-cors", "cors", "same-origin"},
},
{
Name: "Sec-Fetch-Dest",
Value: []string{"empty"},
},
{
Name: "Sec-Fetch-Site",
Value: []string{"none"},
},
{
Name: "Sec-Fetch-User",
Value: []string{"?1"},
},
{
Name: "Accept-Encoding",
Value: []string{"gzip, deflate"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Method) > 0 {
config.Method = &http.Method{Value: v.Method}
}
if len(v.Path) > 0 {
config.Uri = append([]string(nil), v.Path...)
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type AuthenticatorResponse struct {
Version string `json:"version"`
Status string `json:"status"`
Reason string `json:"reason"`
Headers map[string]*StringList `json:"headers"`
}
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
config := &http.ResponseConfig{
Header: []*http.Header{
{
Name: "Content-Type",
Value: []string{"application/octet-stream", "video/mpeg"},
},
{
Name: "Transfer-Encoding",
Value: []string{"chunked"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
{
Name: "Cache-Control",
Value: []string{"private", "no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Status) > 0 || len(v.Reason) > 0 {
config.Status = &http.Status{
Code: "200",
Reason: "OK",
}
if len(v.Status) > 0 {
config.Status.Code = v.Status
}
if len(v.Reason) > 0 {
config.Status.Reason = v.Reason
}
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type Authenticator struct {
Request AuthenticatorRequest `json:"request"`
Response AuthenticatorResponse `json:"response"`
}
func (v *Authenticator) Build() (proto.Message, error) {
config := new(http.Config)
requestConfig, err := v.Request.Build()
if err != nil {
return nil, err
}
config.Request = requestConfig
responseConfig, err := v.Response.Build()
if err != nil {
return nil, err
}
config.Response = responseConfig
return config, nil
}
+871
View File
@@ -0,0 +1,871 @@
package conf
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/netip"
"net/url"
"regexp"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
"github.com/xtls/xray-core/transport/internet/finalmask/realm"
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
switch strings.ToLower(typ) {
case "", "array":
if len(data) == 0 {
return data, nil
}
var packet []byte
if err := json.Unmarshal(data, &packet); err != nil {
return nil, err
}
return packet, nil
case "str":
var str string
if err := json.Unmarshal(data, &str); err != nil {
return nil, err
}
return []byte(str), nil
case "hex":
var str string
if err := json.Unmarshal(data, &str); err != nil {
return nil, err
}
return hex.DecodeString(str)
case "base64":
var str string
if err := json.Unmarshal(data, &str); err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(str)
default:
return nil, errors.New("unknown type")
}
}
var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) },
}, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"header-custom": func() interface{} { return new(HeaderCustomUDP) },
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
"noise": func() interface{} { return new(NoiseMask) },
"salamander": func() interface{} { return new(Salamander) },
"sudoku": func() interface{} { return new(Sudoku) },
"xdns": func() interface{} { return new(Xdns) },
"xicmp": func() interface{} { return new(Xicmp) },
"realm": func() interface{} { return new(Realm) },
}, "type", "settings")
)
type TCPItem struct {
Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Capture string `json:"capture"`
Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
}
type HeaderCustomTCP struct {
Clients [][]TCPItem `json:"clients"`
Servers [][]TCPItem `json:"servers"`
Errors [][]TCPItem `json:"errors"`
}
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
for _, value := range c.Clients {
for _, item := range value {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
for _, value := range c.Servers {
for _, item := range value {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
for _, value := range c.Errors {
for _, item := range value {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
errInvalidRange := errors.New("invalid randRange")
clients := make([]*custom.TCPSequence, len(c.Clients))
for i, value := range c.Clients {
clients[i] = &custom.TCPSequence{}
for _, item := range value {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errInvalidRange
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
servers := make([]*custom.TCPSequence, len(c.Servers))
for i, value := range c.Servers {
servers[i] = &custom.TCPSequence{}
for _, item := range value {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errInvalidRange
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
errors := make([]*custom.TCPSequence, len(c.Errors))
for i, value := range c.Errors {
errors[i] = &custom.TCPSequence{}
for _, item := range value {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errInvalidRange
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
return &custom.TCPConfig{
Clients: clients,
Servers: servers,
Errors: errors,
}, nil
}
type FragmentMask struct {
Packets string `json:"packets"`
Length Int32Range `json:"length"`
Delay Int32Range `json:"delay"`
Lengths []Int32Range `json:"lengths"`
Delays []Int32Range `json:"delays"`
MaxSplit Int32Range `json:"maxSplit"`
}
func (c *FragmentMask) Build() (proto.Message, error) {
config := &fragment.Config{}
switch strings.ToLower(c.Packets) {
case "tlshello":
config.PacketsFrom = 0
config.PacketsTo = 1
case "":
config.PacketsFrom = 0
config.PacketsTo = 0
default:
from, to, err := ParseRangeString(c.Packets)
if err != nil {
return nil, errors.New("Invalid PacketsFrom").Base(err)
}
config.PacketsFrom = int64(from)
config.PacketsTo = int64(to)
if config.PacketsFrom == 0 {
return nil, errors.New("PacketsFrom can't be 0")
}
}
if len(c.Lengths) > 0 {
for _, r := range c.Lengths {
config.LengthsMin = append(config.LengthsMin, int64(r.From))
config.LengthsMax = append(config.LengthsMax, int64(r.To))
}
} else {
config.LengthsMin = append(config.LengthsMin, int64(c.Length.From))
config.LengthsMax = append(config.LengthsMax, int64(c.Length.To))
}
if config.LengthsMin[len(config.LengthsMin)-1] == 0 {
return nil, errors.New("last lengths entry min can't be 0")
}
if len(c.Delays) > 0 {
for _, r := range c.Delays {
config.DelaysMin = append(config.DelaysMin, int64(r.From))
config.DelaysMax = append(config.DelaysMax, int64(r.To))
}
} else {
config.DelaysMin = append(config.DelaysMin, int64(c.Delay.From))
config.DelaysMax = append(config.DelaysMax, int64(c.Delay.To))
}
config.MaxSplitMin = int64(c.MaxSplit.From)
config.MaxSplitMax = int64(c.MaxSplit.To)
return config, nil
}
type NoiseItem struct {
Rand Int32Range `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Type string `json:"type"`
Packet json.RawMessage `json:"packet"`
Delay Int32Range `json:"delay"`
}
type NoiseMask struct {
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
}
func (c *NoiseMask) Build() (proto.Message, error) {
for _, item := range c.Noise {
if len(item.Packet) > 0 && item.Rand.To > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0")
}
}
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
for _, item := range c.Noise {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errors.New("invalid randRange")
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
noiseSlice = append(noiseSlice, &noise.Item{
RandMin: int64(item.Rand.From),
RandMax: int64(item.Rand.To),
RandRangeMin: item.RandRange.From,
RandRangeMax: item.RandRange.To,
Packet: item.Packet,
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
})
}
return &noise.Config{
ResetMin: int64(c.Reset.From),
ResetMax: int64(c.Reset.To),
Items: noiseSlice,
}, nil
}
type UDPItem struct {
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Capture string `json:"capture"`
Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
}
type CustomTransform struct {
Op string `json:"op"`
Args []CustomTransformArg `json:"args"`
}
type CustomTransformArg struct {
Type string `json:"type"`
Bytes json.RawMessage `json:"bytes"`
U64 *uint64 `json:"u64"`
Reuse string `json:"reuse"`
Metadata string `json:"metadata"`
Transform *CustomTransform `json:"transform"`
}
func validateCustomVarName(name string) error {
if name == "" {
return nil
}
if !customVarNamePattern.MatchString(name) {
return errors.New("invalid variable name")
}
return nil
}
func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error {
if err := validateCustomVarName(capture); err != nil {
return err
}
if err := validateCustomVarName(reuse); err != nil {
return err
}
kindCount := 0
if len(packet) > 0 {
kindCount++
}
if rand > 0 {
kindCount++
}
if reuse != "" {
kindCount++
}
if transform != nil {
kindCount++
}
if kindCount > 1 {
return errors.New("exactly one item kind must be set")
}
if kindCount == 0 && capture != "" {
return errors.New("exactly one item kind must be set")
}
return nil
}
func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) {
if transform == nil {
return nil, nil
}
if transform.Op == "" {
return nil, errors.New("transform op is required")
}
if len(transform.Args) == 0 {
return nil, errors.New("transform args are required")
}
args := make([]*custom.ExprArg, 0, len(transform.Args))
for _, arg := range transform.Args {
parsedArg, err := buildCustomTransformArg(arg)
if err != nil {
return nil, err
}
args = append(args, parsedArg)
}
return &custom.Expr{
Op: transform.Op,
Args: args,
}, nil
}
func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) {
kindCount := 0
if len(arg.Bytes) > 0 {
kindCount++
}
if arg.U64 != nil {
kindCount++
}
if arg.Reuse != "" {
kindCount++
}
if arg.Metadata != "" {
kindCount++
}
if arg.Transform != nil {
kindCount++
}
if kindCount != 1 {
return nil, errors.New("transform arg must set exactly one value")
}
if len(arg.Bytes) > 0 {
value, err := PraseByteSlice(arg.Bytes, arg.Type)
if err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Bytes{
Bytes: value,
},
}, nil
}
if arg.U64 != nil {
return &custom.ExprArg{
Value: &custom.ExprArg_U64{
U64: *arg.U64,
},
}, nil
}
if arg.Reuse != "" {
if err := validateCustomVarName(arg.Reuse); err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Var{
Var: arg.Reuse,
},
}, nil
}
if arg.Metadata != "" {
return &custom.ExprArg{
Value: &custom.ExprArg_Metadata{
Metadata: arg.Metadata,
},
}, nil
}
parsedExpr, err := buildCustomTransform(arg.Transform)
if err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Expr{
Expr: parsedExpr,
},
}, nil
}
type HeaderCustomUDP struct {
Mode string `json:"mode"`
Client []UDPItem `json:"client"`
Server []UDPItem `json:"server"`
}
func (c *HeaderCustomUDP) Build() (proto.Message, error) {
switch c.Mode {
case "", "prefix", "standalone":
default:
return nil, errors.New("unknown udp mode")
}
for _, item := range c.Client {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
for _, item := range c.Server {
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
client := make([]*custom.UDPItem, 0, len(c.Client))
for _, item := range c.Client {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errors.New("invalid randRange")
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
client = append(client, &custom.UDPItem{
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
server := make([]*custom.UDPItem, 0, len(c.Server))
for _, item := range c.Server {
if item.RandRange == nil {
item.RandRange = &Int32Range{From: 0, To: 255}
}
if item.RandRange.From < 0 || item.RandRange.To > 255 {
return nil, errors.New("invalid randRange")
}
var err error
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
server = append(server, &custom.UDPItem{
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
if c.Mode == "standalone" {
return &custom.UDPStandaloneConfig{
Client: client,
Server: server,
}, nil
} else {
return &custom.UDPConfig{
Client: client,
Server: server,
}, nil
}
}
type MkcpLegacy struct {
Header string `json:"header"`
Value string `json:"value"`
}
func (c *MkcpLegacy) Build() (proto.Message, error) {
if len(c.Header) == 0 {
if len(c.Value) == 0 {
return &original.Config{}, nil
} else {
return &aes128gcm.Config{Password: c.Value}, nil
}
}
switch strings.ToLower(c.Header) {
case "dns":
domain := c.Value
if len(domain) == 0 {
domain = "www.baidu.com"
}
return &header.Config{ID: 0, Domain: domain}, nil
case "dtls":
return &header.Config{ID: 1}, nil
case "srtp":
return &header.Config{ID: 2}, nil
case "utp":
return &header.Config{ID: 3}, nil
case "wechat":
return &header.Config{ID: 4}, nil
case "wireguard":
return &header.Config{ID: 5}, nil
default:
return nil, errors.New("invalid header ", c.Header)
}
}
type Salamander struct {
Password string `json:"password"`
PacketSize Int32Range `json:"packetSize"`
}
func (c *Salamander) Build() (proto.Message, error) {
if c.PacketSize.To > 0 {
if c.PacketSize.From <= 0 || c.PacketSize.To > 2048 {
return nil, errors.New("gecko: invalid min/max packet size")
}
return &salamander.GeckoConfig{
Password: c.Password,
MinPacketSize: c.PacketSize.From,
MaxPacketSize: c.PacketSize.To,
}, nil
}
return &salamander.Config{
Password: c.Password,
}, nil
}
type Sudoku struct {
Password string `json:"password"`
ASCII string `json:"ascii"`
CustomTable string `json:"customTable"`
LegacyCustomTable string `json:"custom_table"`
CustomTables []string `json:"customTables"`
LegacyCustomSets []string `json:"custom_tables"`
PaddingMin uint32 `json:"paddingMin"`
LegacyPaddingMin uint32 `json:"padding_min"`
PaddingMax uint32 `json:"paddingMax"`
LegacyPaddingMax uint32 `json:"padding_max"`
}
func (c *Sudoku) Build() (proto.Message, error) {
customTable := c.CustomTable
if customTable == "" {
customTable = c.LegacyCustomTable
}
customTables := c.CustomTables
if len(customTables) == 0 {
customTables = c.LegacyCustomSets
}
paddingMin := c.PaddingMin
if paddingMin == 0 {
paddingMin = c.LegacyPaddingMin
}
paddingMax := c.PaddingMax
if paddingMax == 0 {
paddingMax = c.LegacyPaddingMax
}
return &sudoku.Config{
Password: c.Password,
Ascii: c.ASCII,
CustomTable: customTable,
CustomTables: customTables,
PaddingMin: paddingMin,
PaddingMax: paddingMax,
}, nil
}
type Xdns struct {
Domain json.RawMessage `json:"domain"`
Domains []string `json:"domains"`
Resolvers []string `json:"resolvers"`
}
func (c *Xdns) Build() (proto.Message, error) {
if c.Domain != nil {
return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)")
}
if len(c.Domains) == 0 && len(c.Resolvers) == 0 {
return nil, errors.New("empty domains & empty resolvers")
}
for _, r := range c.Resolvers {
if !strings.Contains(r, "+udp://") {
return nil, errors.New("invalid resolver ", r)
}
}
return &xdns.Config{
Domains: c.Domains,
Resolvers: c.Resolvers,
}, nil
}
type Xicmp struct {
DGRAM bool `json:"dgram"`
IPs []string `json:"ips"`
}
func (c *Xicmp) Build() (proto.Message, error) {
for _, ip := range c.IPs {
if _, err := netip.ParseAddr(ip); err != nil {
return nil, err
}
}
config := &xicmp.Config{
DGRAM: c.DGRAM,
IPs: c.IPs,
}
return config, nil
}
type Realm struct {
Url string `json:"url"`
StunServers []string `json:"stunServers"`
TlsConfig *TLSConfig `json:"tlsConfig"`
}
func (c *Realm) Build() (proto.Message, error) {
var scheme, host, port, token, id string
var stunServers []string
var tlsConfig *tls.Config
u, err := url.Parse(c.Url)
if err != nil {
return nil, err
}
switch u.Scheme {
case "realm":
scheme = "https"
case "realm+http":
scheme = "http"
default:
return nil, errors.New("invalid scheme", u.Scheme)
}
host = u.Hostname()
if host == "" {
return nil, errors.New("invalid host", host)
}
port = u.Port()
if port == "" {
port = "443"
if scheme == "http" {
port = "80"
}
}
token, err = url.PathUnescape(u.User.String())
if err != nil {
return nil, err
}
if token == "" {
return nil, errors.New("invalid token", token)
}
id, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/"))
if err != nil {
return nil, err
}
if id == "" {
return nil, errors.New("invalid id", id)
}
if len(c.StunServers) == 0 {
return nil, errors.New("empty stunServers")
}
for _, s := range c.StunServers {
_, _, err = net.SplitHostPort(s)
if err != nil {
return nil, err
}
}
stunServers = c.StunServers
if c.TlsConfig != nil {
tc, err := c.TlsConfig.Build()
if err != nil {
return nil, err
}
tlsConfig = tc.(*tls.Config)
}
return &realm.Config{
Scheme: scheme,
Host: host,
Port: port,
Token: token,
ID: id,
StunServers: stunServers,
TlsConfig: tlsConfig,
}, nil
}
type Mask struct {
Type string `json:"type"`
Settings *json.RawMessage `json:"settings"`
}
func (c *Mask) Build(tcp bool) (proto.Message, error) {
loader := udpmaskLoader
if tcp {
loader = tcpmaskLoader
}
settings := []byte("{}")
if c.Settings != nil {
settings = ([]byte)(*c.Settings)
}
rawConfig, err := loader.LoadWithID(settings, c.Type)
if err != nil {
return nil, err
}
ts, err := rawConfig.(Buildable).Build()
if err != nil {
return nil, err
}
return ts, nil
}
type QuicParamsConfig struct {
Congestion string `json:"congestion"`
Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"`
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"`
}
type FinalMask struct {
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
QuicParams *QuicParamsConfig `json:"quicParams"`
}
File diff suppressed because it is too large Load Diff
+814
View File
@@ -0,0 +1,814 @@
package conf
import (
"context"
"encoding/json"
"math/big"
"net/url"
"sort"
"strconv"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/grpc"
"github.com/xtls/xray-core/transport/internet/headers/http"
"github.com/xtls/xray-core/transport/internet/headers/noop"
"github.com/xtls/xray-core/transport/internet/httpupgrade"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/kcp"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/websocket"
"google.golang.org/protobuf/proto"
)
type NoOpConnectionAuthenticator struct{}
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
return new(noop.ConnectionConfig), nil
}
type AuthenticatorRequest struct {
Version string `json:"version"`
Method string `json:"method"`
Path StringList `json:"path"`
Headers map[string]*StringList `json:"headers"`
}
func sortMapKeys(m map[string]*StringList) []string {
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
config := &http.RequestConfig{
Uri: []string{"/"},
Header: []*http.Header{
{
Name: "Host",
Value: []string{"www.baidu.com", "www.bing.com"},
},
{
Name: "User-Agent",
Value: []string{utils.ChromeUA},
},
{
Name: "Sec-CH-UA",
Value: []string{utils.ChromeUACH},
},
{
Name: "Sec-CH-UA-Mobile",
Value: []string{"?0"},
},
{
Name: "Sec-CH-UA-Platform",
Value: []string{"Windows"},
},
{
Name: "Sec-Fetch-Mode",
Value: []string{"no-cors", "cors", "same-origin"},
},
{
Name: "Sec-Fetch-Dest",
Value: []string{"empty"},
},
{
Name: "Sec-Fetch-Site",
Value: []string{"none"},
},
{
Name: "Sec-Fetch-User",
Value: []string{"?1"},
},
{
Name: "Accept-Encoding",
Value: []string{"gzip, deflate"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Method) > 0 {
config.Method = &http.Method{Value: v.Method}
}
if len(v.Path) > 0 {
config.Uri = append([]string(nil), v.Path...)
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type AuthenticatorResponse struct {
Version string `json:"version"`
Status string `json:"status"`
Reason string `json:"reason"`
Headers map[string]*StringList `json:"headers"`
}
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
config := &http.ResponseConfig{
Header: []*http.Header{
{
Name: "Content-Type",
Value: []string{"application/octet-stream", "video/mpeg"},
},
{
Name: "Transfer-Encoding",
Value: []string{"chunked"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
{
Name: "Cache-Control",
Value: []string{"private", "no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Status) > 0 || len(v.Reason) > 0 {
config.Status = &http.Status{
Code: "200",
Reason: "OK",
}
if len(v.Status) > 0 {
config.Status.Code = v.Status
}
if len(v.Reason) > 0 {
config.Status.Reason = v.Reason
}
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type Authenticator struct {
Request AuthenticatorRequest `json:"request"`
Response AuthenticatorResponse `json:"response"`
}
func (v *Authenticator) Build() (proto.Message, error) {
config := new(http.Config)
requestConfig, err := v.Request.Build()
if err != nil {
return nil, err
}
config.Request = requestConfig
responseConfig, err := v.Response.Build()
if err != nil {
return nil, err
}
config.Response = responseConfig
return config, nil
}
var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{
"none": func() interface{} { return new(NoOpConnectionAuthenticator) },
"http": func() interface{} { return new(Authenticator) },
}, "type", "")
type TCPConfig struct {
HeaderConfig json.RawMessage `json:"header"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
}
// Build implements Buildable.
func (c *TCPConfig) Build() (proto.Message, error) {
config := new(tcp.Config)
if len(c.HeaderConfig) > 0 {
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
}
ts, err := headerConfig.(Buildable).Build()
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
}
config.HeaderSettings = serial.ToTypedMessage(ts)
}
if c.AcceptProxyProtocol {
config.AcceptProxyProtocol = c.AcceptProxyProtocol
}
return config, nil
}
type SplitHTTPConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Mode string `json:"mode"`
Headers map[string]string `json:"headers"`
XPaddingBytes Int32Range `json:"xPaddingBytes"`
XPaddingObfsMode bool `json:"xPaddingObfsMode"`
XPaddingKey string `json:"xPaddingKey"`
XPaddingHeader string `json:"xPaddingHeader"`
XPaddingPlacement string `json:"xPaddingPlacement"`
XPaddingMethod string `json:"xPaddingMethod"`
UplinkHTTPMethod string `json:"uplinkHTTPMethod"`
SessionIDPlacement string `json:"sessionIDPlacement"`
SessionIDKey string `json:"sessionIDKey"`
SessionIDTable string `json:"sessionIDTable"`
SessionIDLength Int32Range `json:"sessionIDLength"`
SeqPlacement string `json:"seqPlacement"`
SeqKey string `json:"seqKey"`
UplinkDataPlacement string `json:"uplinkDataPlacement"`
UplinkDataKey string `json:"uplinkDataKey"`
UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
NoGRPCHeader bool `json:"noGRPCHeader"`
NoSSEHeader bool `json:"noSSEHeader"`
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"`
Extra json.RawMessage `json:"extra"`
}
type XmuxConfig struct {
MaxConcurrency Int32Range `json:"maxConcurrency"`
MaxConnections Int32Range `json:"maxConnections"`
CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"`
HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"`
HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"`
HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"`
}
func newRangeConfig(input Int32Range) *splithttp.RangeConfig {
return &splithttp.RangeConfig{
From: input.From,
To: input.To,
}
}
// Build implements Buildable.
func (c *SplitHTTPConfig) Build() (proto.Message, error) {
if c.Extra != nil {
var extra SplitHTTPConfig
if err := json.Unmarshal(c.Extra, &extra); err != nil {
return nil, errors.New(`Failed to unmarshal "extra".`).Base(err)
}
extra.Host = c.Host
extra.Path = c.Path
extra.Mode = c.Mode
c = &extra
}
switch c.Mode {
case "":
c.Mode = "auto"
case "auto", "packet-up", "stream-up", "stream-one":
default:
return nil, errors.New("unsupported mode: " + c.Mode)
}
// Priority (client): host > serverName > address
for k := range c.Headers {
if strings.ToLower(k) == "host" {
return nil, errors.New(`"headers" can't contain "host"`)
}
}
if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
return nil, errors.New("xPaddingBytes cannot be disabled")
}
if c.XPaddingKey == "" {
c.XPaddingKey = "x_padding"
}
if c.XPaddingHeader == "" {
c.XPaddingHeader = "X-Padding"
}
switch c.XPaddingPlacement {
case "":
c.XPaddingPlacement = "queryInHeader"
case "cookie", "header", "query", "queryInHeader":
default:
return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement)
}
switch c.XPaddingMethod {
case "":
c.XPaddingMethod = "repeat-x"
case "repeat-x", "tokenish":
default:
return nil, errors.New("unsupported padding method: " + c.XPaddingMethod)
}
switch c.UplinkDataPlacement {
case "":
c.UplinkDataPlacement = splithttp.PlacementAuto
case splithttp.PlacementAuto, splithttp.PlacementBody:
case splithttp.PlacementCookie, splithttp.PlacementHeader:
if c.Mode != "packet-up" {
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
}
default:
return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement)
}
if c.UplinkHTTPMethod == "" {
c.UplinkHTTPMethod = "POST"
}
c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod)
if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" {
return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode")
}
switch c.SessionIDPlacement {
case "":
c.SessionIDPlacement = "path"
case "path", "cookie", "header", "query":
default:
return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement)
}
switch c.SeqPlacement {
case "":
c.SeqPlacement = "path"
case "path", "cookie", "header", "query":
default:
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
}
if c.SessionIDPlacement != "path" && c.SessionIDKey == "" {
switch c.SessionIDPlacement {
case "cookie", "query":
c.SessionIDKey = "x_session"
case "header":
c.SessionIDKey = "X-Session"
}
}
if c.SessionIDTable != "" {
if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok {
c.SessionIDTable = predefined
}
room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To)
// 2.1B possiblities should be enough
if room.Cmp(big.NewInt(2<<30)) < 0 {
return nil, errors.New("sessionIDTable or sessionIDLength is too small")
}
if c.SessionIDLength.From <= 0 {
return nil, errors.New("sessionIDLength.from must be greater than 0")
}
for i := 0; i < len(c.SessionIDTable); i++ {
if c.SessionIDTable[i] >= 0x80 {
return nil, errors.New("sessionIDTable must contain only ASCII characters")
}
}
}
if c.SeqPlacement != "path" && c.SeqKey == "" {
switch c.SeqPlacement {
case "cookie", "query":
c.SeqKey = "x_seq"
case "header":
c.SeqKey = "X-Seq"
}
}
if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" {
switch c.UplinkDataPlacement {
case splithttp.PlacementCookie:
c.UplinkDataKey = "x_data"
case splithttp.PlacementAuto, splithttp.PlacementHeader:
c.UplinkDataKey = "X-Data"
}
}
if c.ServerMaxHeaderBytes < 0 {
return nil, errors.New("invalid negative value of maxHeaderBytes")
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 6
c.Xmux.MaxConnections.To = 6
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
}
config := &splithttp.Config{
Host: c.Host,
Path: c.Path,
Mode: c.Mode,
Headers: c.Headers,
XPaddingBytes: newRangeConfig(c.XPaddingBytes),
XPaddingObfsMode: c.XPaddingObfsMode,
XPaddingKey: c.XPaddingKey,
XPaddingHeader: c.XPaddingHeader,
XPaddingPlacement: c.XPaddingPlacement,
XPaddingMethod: c.XPaddingMethod,
UplinkHTTPMethod: c.UplinkHTTPMethod,
SessionIDPlacement: c.SessionIDPlacement,
SeqPlacement: c.SeqPlacement,
SessionIDKey: c.SessionIDKey,
SeqKey: c.SeqKey,
UplinkDataPlacement: c.UplinkDataPlacement,
UplinkDataKey: c.UplinkDataKey,
UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
NoGRPCHeader: c.NoGRPCHeader,
NoSSEHeader: c.NoSSEHeader,
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
SessionIDTable: c.SessionIDTable,
SessionIDLength: newRangeConfig(c.SessionIDLength),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
},
}
if c.DownloadSettings != nil {
if c.Mode == "stream-one" {
return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`)
}
var err error
if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil {
return nil, errors.New(`Failed to build "downloadSettings".`).Base(err)
}
}
return config, nil
}
func roomSize(tableSize int, min, max int32) *big.Int {
base := big.NewInt(int64(tableSize))
sum := new(big.Int)
term := new(big.Int)
for k := min; k <= max; k++ {
term.Exp(base, big.NewInt(int64(k)), nil)
sum.Add(sum, term)
}
return sum
}
type KCPConfig struct {
Mtu *uint32 `json:"mtu"`
Tti *uint32 `json:"tti"`
UpCap *uint32 `json:"uplinkCapacity"`
DownCap *uint32 `json:"downlinkCapacity"`
CwndMultiplier *uint32 `json:"cwndMultiplier"`
MaxSendingWindow *uint32 `json:"maxSendingWindow"`
HeaderConfig json.RawMessage `json:"header"`
Seed *string `json:"seed"`
}
// Build implements Buildable.
func (c *KCPConfig) Build() (proto.Message, error) {
if c.HeaderConfig != nil || c.Seed != nil {
return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm")
}
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
if c.Mtu != nil {
config.Mtu = *c.Mtu
}
if c.Tti != nil {
config.Tti = *c.Tti
}
if c.UpCap != nil {
config.UplinkCapacity = *c.UpCap
}
if c.DownCap != nil {
config.DownlinkCapacity = *c.DownCap
}
if c.CwndMultiplier != nil {
config.CwndMultiplier = *c.CwndMultiplier
}
if c.MaxSendingWindow != nil {
config.MaxSendingWindow = *c.MaxSendingWindow
}
if config.Mtu < 21 {
return nil, errors.New("Mtu must be at least 21").AtError()
}
if config.Tti < 10 || config.Tti > 1000 {
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
}
if config.CwndMultiplier < 1 {
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
}
if config.GetSendingBufferSize() == 0 {
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
}
return config, nil
}
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
func (g *GRPCConfig) Build() (proto.Message, error) {
if g.IdleTimeout <= 0 {
g.IdleTimeout = 0
}
if g.HealthCheckTimeout <= 0 {
g.HealthCheckTimeout = 0
}
if g.InitialWindowsSize < 0 {
// default window size of gRPC-go
g.InitialWindowsSize = 0
}
return &grpc.Config{
Authority: g.Authority,
ServiceName: g.ServiceName,
MultiMode: g.MultiMode,
IdleTimeout: g.IdleTimeout,
HealthCheckTimeout: g.HealthCheckTimeout,
PermitWithoutStream: g.PermitWithoutStream,
InitialWindowsSize: g.InitialWindowsSize,
UserAgent: g.UserAgent,
}, nil
}
type WebSocketConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
HeartbeatPeriod uint32 `json:"heartbeatPeriod"`
}
// Build implements Buildable.
func (c *WebSocketConfig) Build() (proto.Message, error) {
path := c.Path
var ed uint32
if u, err := url.Parse(path); err == nil {
if q := u.Query(); q.Get("ed") != "" {
Ed, _ := strconv.Atoi(q.Get("ed"))
ed = uint32(Ed)
q.Del("ed")
u.RawQuery = q.Encode()
path = u.String()
}
}
// Priority (client): host > serverName > address
for k, v := range c.Headers {
if strings.ToLower(k) == "host" {
errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`)
if c.Host == "" {
c.Host = v
}
delete(c.Headers, k)
}
}
config := &websocket.Config{
Path: path,
Host: c.Host,
Header: c.Headers,
AcceptProxyProtocol: c.AcceptProxyProtocol,
Ed: ed,
HeartbeatPeriod: c.HeartbeatPeriod,
}
return config, nil
}
type HttpUpgradeConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
}
// Build implements Buildable.
func (c *HttpUpgradeConfig) Build() (proto.Message, error) {
path := c.Path
var ed uint32
if u, err := url.Parse(path); err == nil {
if q := u.Query(); q.Get("ed") != "" {
Ed, _ := strconv.Atoi(q.Get("ed"))
ed = uint32(Ed)
q.Del("ed")
u.RawQuery = q.Encode()
path = u.String()
}
}
// Priority (client): host > serverName > address
for k := range c.Headers {
if strings.ToLower(k) == "host" {
return nil, errors.New(`"headers" can't contain "host"`)
}
}
config := &httpupgrade.Config{
Path: path,
Host: c.Host,
Header: c.Headers,
AcceptProxyProtocol: c.AcceptProxyProtocol,
Ed: ed,
}
return config, nil
}
const (
Byte = 1
Kilobyte = 1024 * Byte
Megabyte = 1024 * Kilobyte
Gigabyte = 1024 * Megabyte
Terabyte = 1024 * Gigabyte
)
type Bandwidth string
func (b Bandwidth) Bps() (uint64, error) {
s := strings.TrimSpace(strings.ToLower(string(b)))
if s == "" {
return 0, nil
}
idx := len(s)
for i, c := range s {
if (c < '0' || c > '9') && c != '.' {
idx = i
break
}
}
numStr := s[:idx]
unit := strings.TrimSpace(s[idx:])
val, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, err
}
mul := uint64(1)
switch unit {
case "", "b", "bps":
mul = Byte
case "k", "kb", "kbps":
mul = Kilobyte
case "m", "mb", "mbps":
mul = Megabyte
case "g", "gb", "gbps":
mul = Gigabyte
case "t", "tb", "tbps":
mul = Terabyte
default:
return 0, errors.New("unsupported unit: " + unit)
}
return uint64(val*float64(mul)) / 8, nil
}
type UdpHop struct {
PortList PortList `json:"ports"`
Interval Int32Range `json:"interval"`
}
type Masquerade struct {
Type string `json:"type"`
Dir string `json:"dir"`
Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"`
Insecure bool `json:"insecure"`
Content string `json:"content"`
Headers map[string]string `json:"headers"`
StatusCode int32 `json:"statusCode"`
}
type HysteriaConfig struct {
Version int32 `json:"version"`
Auth string `json:"auth"`
Congestion *string `json:"congestion"`
Up *Bandwidth `json:"up"`
Down *Bandwidth `json:"down"`
UdpHop *UdpHop `json:"udphop"`
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
Masquerade Masquerade `json:"masquerade"`
}
func (c *HysteriaConfig) Build() (proto.Message, error) {
if c.Version != 2 {
return nil, errors.New("version != 2")
}
if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil {
errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams")
}
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
}
config := &hysteria.Config{}
config.Auth = c.Auth
config.UdpIdleTimeout = c.UdpIdleTimeout
config.MasqType = c.Masquerade.Type
config.MasqFile = c.Masquerade.Dir
config.MasqUrl = c.Masquerade.Url
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
config.MasqUrlInsecure = c.Masquerade.Insecure
config.MasqString = c.Masquerade.Content
config.MasqStringHeaders = c.Masquerade.Headers
config.MasqStringStatusCode = c.Masquerade.StatusCode
if config.UdpIdleTimeout == 0 {
config.UdpIdleTimeout = 60
}
return config, nil
}
func readFileOrString(f string, s []string) ([]byte, error) {
if len(f) > 0 {
return filesystem.ReadCert(f)
}
if len(s) > 0 {
return []byte(strings.Join(s, "\n")), nil
}
return nil, errors.New("both file and bytes are empty.")
}
+402
View File
@@ -0,0 +1,402 @@
package conf
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/url"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
type LimitFallback struct {
AfterBytes uint64
BytesPerSec uint64
BurstBytesPerSec uint64
}
type REALITYConfig struct {
MasterKeyLog string `json:"masterKeyLog"`
Show bool `json:"show"`
Target json.RawMessage `json:"target"`
Dest json.RawMessage `json:"dest"`
Type string `json:"type"`
Xver uint64 `json:"xver"`
ServerNames []string `json:"serverNames"`
PrivateKey string `json:"privateKey"`
MinClientVer string `json:"minClientVer"`
MaxClientVer string `json:"maxClientVer"`
MaxTimeDiff uint64 `json:"maxTimeDiff"`
ShortIds []string `json:"shortIds"`
Mldsa65Seed string `json:"mldsa65Seed"`
LimitFallbackUpload LimitFallback `json:"limitFallbackUpload"`
LimitFallbackDownload LimitFallback `json:"limitFallbackDownload"`
Fingerprint string `json:"fingerprint"`
ServerName string `json:"serverName"`
Password string `json:"password"`
PublicKey string `json:"publicKey"`
ShortId string `json:"shortId"`
Mldsa65Verify string `json:"mldsa65Verify"`
SpiderX string `json:"spiderX"`
}
func (c *REALITYConfig) Build() (proto.Message, error) {
config := new(reality.Config)
config.MasterKeyLog = c.MasterKeyLog
config.Show = c.Show
var err error
if c.Target != nil {
c.Dest = c.Target
}
if c.Dest != nil {
var i uint16
var s string
if err = json.Unmarshal(c.Dest, &i); err == nil {
s = strconv.Itoa(int(i))
} else {
_ = json.Unmarshal(c.Dest, &s)
}
if c.Type == "" && s != "" {
switch s[0] {
case '@', '/':
c.Type = "unix"
if s[0] == '@' && len(s) > 1 && s[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") {
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
copy(fullAddr, s[1:])
s = string(fullAddr)
}
default:
if _, err = strconv.Atoi(s); err == nil {
s = "localhost:" + s
}
if _, _, err = net.SplitHostPort(s); err == nil {
c.Type = "tcp"
}
}
}
if c.Type == "" {
return nil, errors.New(`please fill in a valid value for "target"`)
}
if c.Xver > 2 {
return nil, errors.New(`invalid PROXY protocol version, "xver" only accepts 0, 1, 2`)
}
if len(c.ServerNames) == 0 {
return nil, errors.New(`empty "serverNames"`)
}
if c.PrivateKey == "" {
return nil, errors.New(`empty "privateKey"`)
}
if config.PrivateKey, err = base64.RawURLEncoding.DecodeString(c.PrivateKey); err != nil || len(config.PrivateKey) != 32 {
return nil, errors.New(`invalid "privateKey": `, c.PrivateKey)
}
if c.MinClientVer != "" {
config.MinClientVer = make([]byte, 3)
var u uint64
for i, s := range strings.Split(c.MinClientVer, ".") {
if i == 3 {
return nil, errors.New(`invalid "minClientVer": `, c.MinClientVer)
}
if u, err = strconv.ParseUint(s, 10, 8); err != nil {
return nil, errors.New(`"minClientVer[`, i, `]" should be less than 256`)
} else {
config.MinClientVer[i] = byte(u)
}
}
}
if c.MaxClientVer != "" {
config.MaxClientVer = make([]byte, 3)
var u uint64
for i, s := range strings.Split(c.MaxClientVer, ".") {
if i == 3 {
return nil, errors.New(`invalid "maxClientVer": `, c.MaxClientVer)
}
if u, err = strconv.ParseUint(s, 10, 8); err != nil {
return nil, errors.New(`"maxClientVer[`, i, `]" should be less than 256`)
} else {
config.MaxClientVer[i] = byte(u)
}
}
}
if len(c.ShortIds) == 0 {
return nil, errors.New(`empty "shortIds"`)
}
config.ShortIds = make([][]byte, len(c.ShortIds))
for i, s := range c.ShortIds {
if len(s) > 16 {
return nil, errors.New(`too long "shortIds[`, i, `]": `, s)
}
config.ShortIds[i] = make([]byte, 8)
if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil {
return nil, errors.New(`invalid "shortIds[`, i, `]": `, s)
}
}
config.Dest = s
config.Type = c.Type
config.Xver = c.Xver
config.ServerNames = c.ServerNames
config.MaxTimeDiff = c.MaxTimeDiff
if c.Mldsa65Seed != "" {
if c.Mldsa65Seed == c.PrivateKey {
return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed)
}
if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 {
return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed)
}
}
for _, sn := range config.ServerNames {
if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") {
errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`)
}
}
config.LimitFallbackUpload = new(reality.LimitFallback)
config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes
config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec
config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec
config.LimitFallbackDownload = new(reality.LimitFallback)
config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes
config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec
config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec
} else {
config.Fingerprint = strings.ToLower(c.Fingerprint)
if config.Fingerprint == "unsafe" || config.Fingerprint == "hellogolang" {
return nil, errors.New(`invalid "fingerprint": `, config.Fingerprint)
}
if tls.GetFingerprint(config.Fingerprint) == nil {
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint)
}
if len(c.ServerNames) != 0 {
return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`)
}
if c.Password != "" {
c.PublicKey = c.Password
}
if c.PublicKey == "" {
return nil, errors.New(`empty "password"`)
}
if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 {
return nil, errors.New(`invalid "password": `, c.PublicKey)
}
if len(c.ShortIds) != 0 {
return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`)
}
if len(c.ShortId) > 16 {
return nil, errors.New(`too long "shortId": `, c.ShortId)
}
config.ShortId = make([]byte, 8)
if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil {
return nil, errors.New(`invalid "shortId": `, c.ShortId)
}
if c.Mldsa65Verify != "" {
if config.Mldsa65Verify, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Verify); err != nil || len(config.Mldsa65Verify) != 1952 {
return nil, errors.New(`invalid "mldsa65Verify": `, c.Mldsa65Verify)
}
}
if c.SpiderX == "" {
c.SpiderX = "/"
}
if c.SpiderX[0] != '/' {
return nil, errors.New(`invalid "spiderX": `, c.SpiderX)
}
config.SpiderY = make([]int64, 10)
u, _ := url.Parse(c.SpiderX)
q := u.Query()
parse := func(param string, index int) {
if q.Get(param) != "" {
s := strings.Split(q.Get(param), "-")
if len(s) == 1 {
config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64)
config.SpiderY[index+1], _ = strconv.ParseInt(s[0], 10, 64)
} else {
config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64)
config.SpiderY[index+1], _ = strconv.ParseInt(s[1], 10, 64)
}
}
q.Del(param)
}
parse("p", 0) // padding
parse("c", 2) // concurrency
parse("t", 4) // times
parse("i", 6) // interval
parse("r", 8) // return
u.RawQuery = q.Encode()
config.SpiderX = u.String()
config.ServerName = c.ServerName
}
return config, nil
}
type TLSCertConfig struct {
CertFile string `json:"certificateFile"`
CertStr []string `json:"certificate"`
KeyFile string `json:"keyFile"`
KeyStr []string `json:"key"`
Usage string `json:"usage"`
OcspStapling uint64 `json:"ocspStapling"`
OneTimeLoading bool `json:"oneTimeLoading"`
BuildChain bool `json:"buildChain"`
}
// Build implements Buildable.
func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
certificate := new(tls.Certificate)
cert, err := readFileOrString(c.CertFile, c.CertStr)
if err != nil {
return nil, errors.New("failed to parse certificate").Base(err)
}
certificate.Certificate = cert
certificate.CertificatePath = c.CertFile
if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 {
key, err := readFileOrString(c.KeyFile, c.KeyStr)
if err != nil {
return nil, errors.New("failed to parse key").Base(err)
}
certificate.Key = key
certificate.KeyPath = c.KeyFile
}
switch strings.ToLower(c.Usage) {
case "encipherment":
certificate.Usage = tls.Certificate_ENCIPHERMENT
case "verify":
certificate.Usage = tls.Certificate_AUTHORITY_VERIFY
case "issue":
certificate.Usage = tls.Certificate_AUTHORITY_ISSUE
default:
certificate.Usage = tls.Certificate_ENCIPHERMENT
}
if certificate.KeyPath == "" && certificate.CertificatePath == "" {
certificate.OneTimeLoading = true
} else {
certificate.OneTimeLoading = c.OneTimeLoading
}
certificate.OcspStapling = c.OcspStapling
certificate.BuildChain = c.BuildChain
return certificate, nil
}
type TLSConfig struct {
AllowInsecure bool `json:"allowInsecure"`
Certs []*TLSCertConfig `json:"certificates"`
ServerName string `json:"serverName"`
ALPN *StringList `json:"alpn"`
EnableSessionResumption bool `json:"enableSessionResumption"`
DisableSystemRoot bool `json:"disableSystemRoot"`
MinVersion string `json:"minVersion"`
MaxVersion string `json:"maxVersion"`
CipherSuites string `json:"cipherSuites"`
Fingerprint string `json:"fingerprint"`
RejectUnknownSNI bool `json:"rejectUnknownSni"`
CurvePreferences *StringList `json:"curvePreferences"`
MasterKeyLog string `json:"masterKeyLog"`
PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"`
VerifyPeerCertByName string `json:"verifyPeerCertByName"`
ECHServerKeys string `json:"echServerKeys"`
ECHConfigList string `json:"echConfigList"`
ECHSocketSettings *SocketConfig `json:"echSockopt"`
}
// Build implements Buildable.
func (c *TLSConfig) Build() (proto.Message, error) {
config := new(tls.Config)
config.Certificate = make([]*tls.Certificate, len(c.Certs))
for idx, certConf := range c.Certs {
cert, err := certConf.Build()
if err != nil {
return nil, err
}
config.Certificate[idx] = cert
}
serverName := c.ServerName
if len(c.ServerName) > 0 {
config.ServerName = serverName
}
if c.ALPN != nil && len(*c.ALPN) > 0 {
config.NextProtocol = []string(*c.ALPN)
}
if len(config.NextProtocol) > 1 {
for _, p := range config.NextProtocol {
if tls.IsFromMitm(p) {
return nil, errors.New(`only one element is allowed in "alpn" when using "fromMitm" in it`)
}
}
}
if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 {
config.CurvePreferences = []string(*c.CurvePreferences)
}
config.EnableSessionResumption = c.EnableSessionResumption
config.DisableSystemRoot = c.DisableSystemRoot
config.MinVersion = c.MinVersion
config.MaxVersion = c.MaxVersion
config.CipherSuites = c.CipherSuites
config.Fingerprint = strings.ToLower(c.Fingerprint)
if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil {
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint)
}
config.RejectUnknownSni = c.RejectUnknownSNI
config.MasterKeyLog = c.MasterKeyLog
if c.AllowInsecure {
return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`)
}
if c.PinnedPeerCertSha256 != "" {
for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") {
v = strings.TrimSpace(v)
if v == "" {
continue
}
// remove colons for OpenSSL format
hashValue, err := hex.DecodeString(strings.ReplaceAll(v, ":", ""))
if err != nil {
return nil, err
}
if len(hashValue) != 32 {
return nil, errors.New("incorrect pinnedPeerCertSha256 length: ", v)
}
config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue)
}
}
if c.VerifyPeerCertByName != "" {
for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") {
v = strings.TrimSpace(v)
if v == "" {
continue
}
config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v)
}
}
if c.ECHServerKeys != "" {
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys)
if err != nil {
return nil, errors.New("invalid ECH Config", c.ECHServerKeys)
}
config.EchServerKeys = EchPrivateKey
}
config.EchConfigList = c.ECHConfigList
if c.ECHSocketSettings != nil {
ss, err := c.ECHSocketSettings.Build()
if err != nil {
return nil, errors.New("Failed to build ech sockopt.").Base(err)
}
config.EchSocketSettings = ss
}
return config, nil
}
+187
View File
@@ -0,0 +1,187 @@
package conf
import (
"encoding/json"
"math"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet"
)
type CustomSockoptConfig struct {
Syetem string `json:"system"`
Network string `json:"network"`
Level string `json:"level"`
Opt string `json:"opt"`
Value string `json:"value"`
Type string `json:"type"`
}
type HappyEyeballsConfig struct {
PrioritizeIPv6 bool `json:"prioritizeIPv6"`
TryDelayMs uint64 `json:"tryDelayMs"`
Interleave uint32 `json:"interleave"`
MaxConcurrentTry uint32 `json:"maxConcurrentTry"`
}
func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error {
innerHappyEyeballsConfig := struct {
PrioritizeIPv6 bool `json:"prioritizeIPv6"`
TryDelayMs uint64 `json:"tryDelayMs"`
Interleave uint32 `json:"interleave"`
MaxConcurrentTry uint32 `json:"maxConcurrentTry"`
}{PrioritizeIPv6: false, Interleave: 1, TryDelayMs: 0, MaxConcurrentTry: 4}
if err := json.Unmarshal(data, &innerHappyEyeballsConfig); err != nil {
return err
}
h.PrioritizeIPv6 = innerHappyEyeballsConfig.PrioritizeIPv6
h.TryDelayMs = innerHappyEyeballsConfig.TryDelayMs
h.Interleave = innerHappyEyeballsConfig.Interleave
h.MaxConcurrentTry = innerHappyEyeballsConfig.MaxConcurrentTry
return nil
}
type SocketConfig struct {
Mark int32 `json:"mark"`
TFO interface{} `json:"tcpFastOpen"`
TProxy string `json:"tproxy"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
DomainStrategy string `json:"domainStrategy"`
DialerProxy string `json:"dialerProxy"`
TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"`
TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"`
TCPCongestion string `json:"tcpCongestion"`
TCPWindowClamp int32 `json:"tcpWindowClamp"`
TCPMaxSeg int32 `json:"tcpMaxSeg"`
Penetrate bool `json:"penetrate"`
TCPUserTimeout int32 `json:"tcpUserTimeout"`
V6only bool `json:"v6only"`
Interface string `json:"interface"`
TcpMptcp bool `json:"tcpMptcp"`
CustomSockopt []*CustomSockoptConfig `json:"customSockopt"`
AddressPortStrategy string `json:"addressPortStrategy"`
HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"`
TrustedXForwardedFor []string `json:"trustedXForwardedFor"`
}
// Build implements Buildable.
func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
tfo := int32(0) // don't invoke setsockopt() for TFO
if c.TFO != nil {
switch v := c.TFO.(type) {
case bool:
if v {
tfo = 256
} else {
tfo = -1 // TFO need to be disabled
}
case float64:
tfo = int32(math.Min(v, math.MaxInt32))
default:
return nil, errors.New("tcpFastOpen: only boolean and integer value is acceptable")
}
}
var tproxy internet.SocketConfig_TProxyMode
switch strings.ToLower(c.TProxy) {
case "tproxy":
tproxy = internet.SocketConfig_TProxy
case "redirect":
tproxy = internet.SocketConfig_Redirect
default:
tproxy = internet.SocketConfig_Off
}
dStrategy := internet.DomainStrategy_AS_IS
switch strings.ToLower(c.DomainStrategy) {
case "asis", "":
dStrategy = internet.DomainStrategy_AS_IS
case "useip":
dStrategy = internet.DomainStrategy_USE_IP
case "useipv4":
dStrategy = internet.DomainStrategy_USE_IP4
case "useipv6":
dStrategy = internet.DomainStrategy_USE_IP6
case "useipv4v6":
dStrategy = internet.DomainStrategy_USE_IP46
case "useipv6v4":
dStrategy = internet.DomainStrategy_USE_IP64
case "forceip":
dStrategy = internet.DomainStrategy_FORCE_IP
case "forceipv4":
dStrategy = internet.DomainStrategy_FORCE_IP4
case "forceipv6":
dStrategy = internet.DomainStrategy_FORCE_IP6
case "forceipv4v6":
dStrategy = internet.DomainStrategy_FORCE_IP46
case "forceipv6v4":
dStrategy = internet.DomainStrategy_FORCE_IP64
default:
return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy)
}
var customSockopts []*internet.CustomSockopt
for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{
System: copt.Syetem,
Network: copt.Network,
Level: copt.Level,
Opt: copt.Opt,
Value: copt.Value,
Type: copt.Type,
}
customSockopts = append(customSockopts, customSockopt)
}
addressPortStrategy := internet.AddressPortStrategy_None
switch strings.ToLower(c.AddressPortStrategy) {
case "none", "":
addressPortStrategy = internet.AddressPortStrategy_None
case "srvportonly":
addressPortStrategy = internet.AddressPortStrategy_SrvPortOnly
case "srvaddressonly":
addressPortStrategy = internet.AddressPortStrategy_SrvAddressOnly
case "srvportandaddress":
addressPortStrategy = internet.AddressPortStrategy_SrvPortAndAddress
case "txtportonly":
addressPortStrategy = internet.AddressPortStrategy_TxtPortOnly
case "txtaddressonly":
addressPortStrategy = internet.AddressPortStrategy_TxtAddressOnly
case "txtportandaddress":
addressPortStrategy = internet.AddressPortStrategy_TxtPortAndAddress
default:
return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy)
}
happyEyeballs := &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4}
if c.HappyEyeballsSettings != nil {
happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6
happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave
happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs
happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry
}
return &internet.SocketConfig{
Mark: c.Mark,
Tfo: tfo,
Tproxy: tproxy,
DomainStrategy: dStrategy,
AcceptProxyProtocol: c.AcceptProxyProtocol,
DialerProxy: c.DialerProxy,
TcpKeepAliveInterval: c.TCPKeepAliveInterval,
TcpKeepAliveIdle: c.TCPKeepAliveIdle,
TcpCongestion: c.TCPCongestion,
TcpWindowClamp: c.TCPWindowClamp,
TcpMaxSeg: c.TCPMaxSeg,
Penetrate: c.Penetrate,
TcpUserTimeout: c.TCPUserTimeout,
V6Only: c.V6only,
Interface: c.Interface,
TcpMptcp: c.TcpMptcp,
CustomSockopt: customSockopts,
AddressPortStrategy: addressPortStrategy,
HappyEyeballs: happyEyeballs,
TrustedXForwardedFor: c.TrustedXForwardedFor,
}, nil
}
-4
View File
@@ -31,10 +31,6 @@ func (a *VMessAccount) Build() *vmess.Account {
st = protocol.SecurityType_CHACHA20_POLY1305
case "auto":
st = protocol.SecurityType_AUTO
case "none":
st = protocol.SecurityType_NONE
case "zero":
st = protocol.SecurityType_ZERO
default:
st = protocol.SecurityType_AUTO
}
+31 -9
View File
@@ -3,9 +3,13 @@ package conf
import (
"encoding/base64"
"encoding/hex"
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/wireguard"
"google.golang.org/protobuf/proto"
)
@@ -16,9 +20,12 @@ type WireGuardPeerConfig struct {
Endpoint string `json:"endpoint"`
KeepAlive uint32 `json:"keepAlive"`
AllowedIPs []string `json:"allowedIPs,omitempty"`
Level uint32 `json:"level"`
Email string `json:"email"`
}
func (c *WireGuardPeerConfig) Build() (proto.Message, error) {
func (c *WireGuardPeerConfig) Build() (*wireguard.PeerConfig, error) {
var err error
config := new(wireguard.PeerConfig)
@@ -37,8 +44,9 @@ func (c *WireGuardPeerConfig) Build() (proto.Message, error) {
}
config.Endpoint = c.Endpoint
// default 0
config.KeepAlive = c.KeepAlive
if c.KeepAlive != 0 {
config.KeepAlive = strconv.FormatUint(uint64(c.KeepAlive), 10)
}
if c.AllowedIPs == nil {
config.AllowedIps = []string{"0.0.0.0/0", "::0/0"}
} else {
@@ -56,7 +64,6 @@ type WireGuardConfig struct {
Address []string `json:"address"`
Peers []*WireGuardPeerConfig `json:"peers"`
MTU int32 `json:"mtu"`
NumWorkers int32 `json:"workers"`
Reserved []byte `json:"reserved"`
DomainStrategy string `json:"domainStrategy"`
}
@@ -77,14 +84,32 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
config.Endpoint = c.Address
}
if c.Peers != nil {
if c.IsClient {
config.Peers = make([]*wireguard.PeerConfig, len(c.Peers))
for i, p := range c.Peers {
msg, err := p.Build()
if err != nil {
return nil, err
}
config.Peers[i] = msg.(*wireguard.PeerConfig)
config.Peers[i] = msg
}
} else {
config.Users = make([]*protocol.User, len(c.Peers))
processUser := func(idx int) error {
p := c.Peers[idx]
m, err := p.Build()
if err != nil {
return err
}
config.Users[idx] = &protocol.User{
Email: p.Email,
Level: p.Level,
Account: serial.ToTypedMessage(m),
}
return nil
}
if err := task.ParallelForN(len(c.Peers), processUser); err != nil {
return nil, err
}
}
@@ -93,9 +118,6 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
} else {
config.Mtu = c.MTU
}
// these a fallback code exists in wireguard-go code,
// we don't need to process fallback manually
config.NumWorkers = c.NumWorkers
if len(c.Reserved) != 0 && len(c.Reserved) != 3 {
return nil, errors.New(`"reserved" should be empty or 3 bytes`)
-52
View File
@@ -1,52 +0,0 @@
package conf_test
import (
"testing"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/wireguard"
)
func TestWireGuardConfig(t *testing.T) {
creator := func() Buildable {
return new(WireGuardConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"secretKey": "uJv5tZMDltsiYEn+kUwb0Ll/CXWhMkaSCWWhfPEZM3A=",
"address": ["10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"],
"peers": [
{
"publicKey": "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a",
"endpoint": "127.0.0.1:1234"
}
],
"mtu": 1300,
"workers": 2,
"domainStrategy": "ForceIPv6v4",
"noKernelTun": false
}`,
Parser: loadJSON(creator),
Output: &wireguard.DeviceConfig{
// key converted into hex form
SecretKey: "b89bf9b5930396db226049fe914c1bd0b97f0975a13246920965a17cf1193370",
Endpoint: []string{"10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"},
Peers: []*wireguard.PeerConfig{
{
// also can read from hex form directly
PublicKey: "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a",
Endpoint: "127.0.0.1:1234",
KeepAlive: 0,
AllowedIps: []string{"0.0.0.0/0", "::0/0"},
},
},
Mtu: 1300,
NumWorkers: 2,
DomainStrategy: wireguard.DeviceConfig_FORCE_IP64,
NoKernelTun: false,
},
},
})
}
+37 -21
View File
@@ -173,27 +173,6 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
return nil, err
}
receiverSettings.StreamSettings = ss
// TODO: Actually implement this breaking change
protocol := ss.GetEffectiveProtocol()
if (protocol == "websocket" || protocol == "httpupgrade" || protocol == "splithttp") &&
(c.StreamSetting.SocketSettings == nil || len(c.StreamSetting.SocketSettings.TrustedXForwardedFor) == 0) {
errors.LogWarning(
context.Background(),
`====== SECURITY WARNING ======`,
"\n",
`inbound "`, c.Tag, `" using `, protocol, ` has not configured "sockopt.trustedXForwardedFor".`,
"\n",
`THIS IS VERY INSECURE!!!`,
"\n",
`For compatibility, Xray still allows this for now and still trusts X-Forwarded-For implicitly.`,
"\n",
`Please configure "sockopt.trustedXForwardedFor" immediately.`,
"\n",
`In future versions, this option must be explicitly set.`,
"\n",
`====== SECURITY WARNING ======`,
)
}
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)
@@ -251,6 +230,40 @@ func (c *OutboundDetourConfig) checkChainProxyConfig() error {
return nil
}
func requiresTransportSecurity(address *Address) bool {
if address == nil || address.Address == nil {
return false
}
if address.Family().IsIP() {
return !geodata.GetPrivateIPMatcher().Match(address.IP())
}
domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".")
return !geodata.GetPrivateDomainMatcher().MatchAny(domain)
}
func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *proxyman.SenderConfig) error {
if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.GetSecurityType() != "" {
return nil
}
if vlessCfg, ok := rawConfig.(*VLessOutboundConfig); ok {
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
return nil
}
if requiresTransportSecurity(vlessCfg.Address) {
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
}
}
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
if requiresTransportSecurity(tjCfg.Address) {
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
}
}
return nil
}
// Build implements Buildable.
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
senderSettings := &proxyman.SenderConfig{}
@@ -344,6 +357,9 @@ 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)
+1 -20
View File
@@ -62,26 +62,7 @@ func init() {
defaultBlockPrivateRule = &FinalRule{
action: RuleAction_Block,
network: allNetworks,
ip: common.Must2(geodata.IPReg.BuildIPMatcher(common.Must2(geodata.ParseIPRules([]string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
"ff00::/8",
})))),
ip: geodata.GetPrivateIPMatcher(),
}
defaultBlockAllRule = &FinalRule{
+94 -79
View File
@@ -3,25 +3,32 @@ package account
import (
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/google/uuid"
"google.golang.org/protobuf/proto"
)
func (a *Account) AsAccount() (protocol.Account, error) {
var VR net.Port
if id, err := uuid.Parse(a.Auth); err == nil {
VR = net.PortFromBytes(id[6:8])
}
return &MemoryAccount{
Auth: a.Auth,
VR: VR,
}, nil
}
type MemoryAccount struct {
Auth string
VR net.Port
}
func (a *MemoryAccount) Equals(another protocol.Account) bool {
if account, ok := another.(*MemoryAccount); ok {
return a.Auth == account.Auth
func (a *MemoryAccount) Equals(other protocol.Account) bool {
if b, ok := other.(*MemoryAccount); ok {
return a.Auth == b.Auth
}
return false
}
@@ -33,97 +40,105 @@ func (a *MemoryAccount) ToProto() proto.Message {
}
type Validator struct {
emails map[string]struct{}
users map[string]*protocol.MemoryUser
mutex sync.Mutex
users sync.Map
ids sync.Map
mu sync.Mutex
}
func NewValidator() *Validator {
return &Validator{
emails: make(map[string]struct{}),
users: make(map[string]*protocol.MemoryUser),
}
return &Validator{}
}
func (v *Validator) Add(u *protocol.MemoryUser) error {
v.mutex.Lock()
defer v.mutex.Unlock()
if u.Email != "" {
if _, ok := v.emails[u.Email]; ok {
return errors.New("User ", u.Email, " already exists.")
}
v.emails[u.Email] = struct{}{}
func (v *Validator) Add(user *protocol.MemoryUser) (err error) {
v.mu.Lock()
v.users.Store(user.Account.(*MemoryAccount).Auth, user)
if id, err := uuid.Parse(user.Account.(*MemoryAccount).Auth); err == nil {
id[6] = 0
id[7] = 0
v.ids.Store(id, user)
}
v.users[u.Account.(*MemoryAccount).Auth] = u
return nil
v.mu.Unlock()
return
}
func (v *Validator) Del(email string) error {
if email == "" {
return errors.New("Email must not be empty.")
}
v.mutex.Lock()
defer v.mutex.Unlock()
if _, ok := v.emails[email]; !ok {
return errors.New("User ", email, " not found.")
}
delete(v.emails, email)
for key, user := range v.users {
if user.Email == email {
delete(v.users, key)
break
func (v *Validator) DelByEmail(email string) (err error) {
v.mu.Lock()
if user := v.GetByEmail(email); user != nil {
auth := user.Account.(*MemoryAccount).Auth
v.users.Delete(auth)
if id, err := uuid.Parse(auth); err == nil {
id[6] = 0
id[7] = 0
v.ids.Delete(id)
}
}
return nil
v.mu.Unlock()
return
}
func (v *Validator) Get(auth string) *protocol.MemoryUser {
v.mutex.Lock()
defer v.mutex.Unlock()
return v.users[auth]
}
func (v *Validator) GetByEmail(email string) *protocol.MemoryUser {
if email == "" {
return nil
}
v.mutex.Lock()
defer v.mutex.Unlock()
if _, ok := v.emails[email]; ok {
for _, user := range v.users {
if user.Email == email {
return user
func (v *Validator) Get(auth string) (user *protocol.MemoryUser) {
if id, err := uuid.Parse(auth); err == nil {
if user = v.GetByID(id); user != nil {
VR := net.PortFromBytes(id[6:8])
if user.Account.(*MemoryAccount).VR != VR {
user = &protocol.MemoryUser{
Email: user.Email,
Level: user.Level,
Account: &MemoryAccount{
Auth: auth,
VR: VR,
},
}
}
}
return
}
return nil
}
func (v *Validator) GetAll() []*protocol.MemoryUser {
v.mutex.Lock()
defer v.mutex.Unlock()
users := make([]*protocol.MemoryUser, 0, len(v.users))
for _, user := range v.users {
users = append(users, user)
if value, ok := v.users.Load(auth); ok {
user = value.(*protocol.MemoryUser)
}
return users
return
}
func (v *Validator) GetCount() int64 {
v.mutex.Lock()
defer v.mutex.Unlock()
return int64(len(v.users))
func (v *Validator) GetByID(id uuid.UUID) (user *protocol.MemoryUser) {
id[6] = 0
id[7] = 0
if value, ok := v.ids.Load(id); ok {
user = value.(*protocol.MemoryUser)
}
return
}
func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) {
v.users.Range(func(key, value any) bool {
if value.(*protocol.MemoryUser).Email == email {
user = value.(*protocol.MemoryUser)
return false
}
return true
})
return
}
func (v *Validator) GetAll() (users []*protocol.MemoryUser) {
v.users.Range(func(key, value any) bool {
users = append(users, value.(*protocol.MemoryUser))
return true
})
return
}
func (v *Validator) GetCount() (count int64) {
v.users.Range(func(key, value any) bool {
count++
return true
})
return
}
func (v *Validator) NotEmpty() (not_empty bool) {
v.users.Range(func(key, value any) bool {
not_empty = true
return false
})
return
}
+10 -5
View File
@@ -29,6 +29,13 @@ type Client struct {
}
func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
v := core.MustFromContext(ctx)
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
if _, ok := streamSettings.ProtocolSettings.(*hysteria.Config); !ok {
return nil, errors.New("not hysteria transport")
}
if config.Server == nil {
return nil, errors.New(`no target server found`)
}
@@ -37,12 +44,10 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
return nil, errors.New("failed to get server spec").Base(err)
}
v := core.MustFromContext(ctx)
client := &Client{
return &Client{
server: server,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}
return client, nil
policyManager: p,
}, nil
}
func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
+4 -13
View File
@@ -24,8 +24,7 @@ const (
type ClientConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
Server *protocol.ServerEndpoint `protobuf:"bytes,2,opt,name=server,proto3" json:"server,omitempty"`
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -60,13 +59,6 @@ func (*ClientConfig) Descriptor() ([]byte, []int) {
return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{0}
}
func (x *ClientConfig) GetVersion() int32 {
if x != nil {
return x.Version
}
return 0
}
func (x *ClientConfig) GetServer() *protocol.ServerEndpoint {
if x != nil {
return x.Server
@@ -122,10 +114,9 @@ var File_proxy_hysteria_config_proto protoreflect.FileDescriptor
const file_proxy_hysteria_config_proto_rawDesc = "" +
"\n" +
"\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"f\n" +
"\fClientConfig\x12\x18\n" +
"\aversion\x18\x01 \x01(\x05R\aversion\x12<\n" +
"\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" +
"\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"L\n" +
"\fClientConfig\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" +
"\fServerConfig\x120\n" +
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05usersB[\n" +
"\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3"
+1 -2
View File
@@ -10,8 +10,7 @@ import "common/protocol/server_spec.proto";
import "common/protocol/user.proto";
message ClientConfig {
int32 version = 1;
xray.common.protocol.ServerEndpoint server = 2;
xray.common.protocol.ServerEndpoint server = 1;
}
message ServerConfig {
+22 -13
View File
@@ -16,6 +16,7 @@ import (
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/proxy/hysteria/account"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/stat"
)
@@ -27,6 +28,14 @@ type Server struct {
}
func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
v := core.MustFromContext(ctx)
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
if _, ok := streamSettings.ProtocolSettings.(*hysteria.Config); !ok {
return nil, errors.New("not hysteria transport")
}
validator := account.NewValidator()
for _, user := range config.Users {
u, err := user.ToMemoryUser()
@@ -39,26 +48,23 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
}
}
v := core.MustFromContext(ctx)
s := &Server{
return &Server{
config: config,
validator: validator,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}
return s, nil
policyManager: p,
}, nil
}
func (s *Server) HysteriaInboundValidator() *account.Validator {
return s.validator
}
func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error {
return s.validator.Add(u)
func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error {
return s.validator.Add(user)
}
func (s *Server) RemoveUser(ctx context.Context, e string) error {
return s.validator.Del(e)
func (s *Server) RemoveUser(ctx context.Context, email string) error {
return s.validator.DelByEmail(email)
}
func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser {
@@ -85,9 +91,12 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
iConn := stat.TryUnwrapStatsConn(conn)
type User interface{ User() *protocol.MemoryUser }
if v, ok := iConn.(User); ok && v.User() != nil {
inbound.User = v.User()
if v, ok := iConn.(interface{ User() *protocol.MemoryUser }); ok {
user := v.User()
if user != nil {
inbound.User = user
inbound.VlessRoute = user.Account.(*account.MemoryAccount).VR
}
}
if _, ok := iConn.(*hysteria.InterConn); ok {
+22 -10
View File
@@ -7,6 +7,7 @@
package loopback
import (
proxyman "github.com/xtls/xray-core/app/proxyman"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -22,8 +23,9 @@ const (
)
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
InboundTag string `protobuf:"bytes,1,opt,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
InboundTag string `protobuf:"bytes,1,opt,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
Sniffing *proxyman.SniffingConfig `protobuf:"bytes,2,opt,name=sniffing,proto3" json:"sniffing,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -65,14 +67,22 @@ func (x *Config) GetInboundTag() string {
return ""
}
func (x *Config) GetSniffing() *proxyman.SniffingConfig {
if x != nil {
return x.Sniffing
}
return nil
}
var File_proxy_loopback_config_proto protoreflect.FileDescriptor
const file_proxy_loopback_config_proto_rawDesc = "" +
"\n" +
"\x1bproxy/loopback/config.proto\x12\x13xray.proxy.loopback\")\n" +
"\x1bproxy/loopback/config.proto\x12\x13xray.proxy.loopback\x1a\x19app/proxyman/config.proto\"h\n" +
"\x06Config\x12\x1f\n" +
"\vinbound_tag\x18\x01 \x01(\tR\n" +
"inboundTagB[\n" +
"inboundTag\x12=\n" +
"\bsniffing\x18\x02 \x01(\v2!.xray.app.proxyman.SniffingConfigR\bsniffingB[\n" +
"\x17com.xray.proxy.loopbackP\x01Z(github.com/xtls/xray-core/proxy/loopback\xaa\x02\x13Xray.Proxy.Loopbackb\x06proto3"
var (
@@ -89,14 +99,16 @@ func file_proxy_loopback_config_proto_rawDescGZIP() []byte {
var file_proxy_loopback_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proxy_loopback_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.loopback.Config
(*Config)(nil), // 0: xray.proxy.loopback.Config
(*proxyman.SniffingConfig)(nil), // 1: xray.app.proxyman.SniffingConfig
}
var file_proxy_loopback_config_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
1, // 0: xray.proxy.loopback.Config.sniffing:type_name -> xray.app.proxyman.SniffingConfig
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
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proxy_loopback_config_proto_init() }
+3
View File
@@ -6,6 +6,9 @@ option go_package = "github.com/xtls/xray-core/proxy/loopback";
option java_package = "com.xray.proxy.loopback";
option java_multiple_files = true;
import "app/proxyman/config.proto";
message Config {
string inbound_tag = 1;
xray.app.proxyman.SniffingConfig sniffing = 2;
}
+14 -5
View File
@@ -3,6 +3,7 @@ package loopback
import (
"context"
proxyman "github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/session"
@@ -13,7 +14,8 @@ import (
)
type Loopback struct {
config *Config
inboundTag string
sniffingRequest session.SniffingRequest
dispatcherInstance routing.Dispatcher
}
@@ -29,6 +31,7 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
errors.LogInfo(ctx, "opening connection to ", destination)
content := new(session.Content)
content.SkipDNSResolve = true
content.SniffingRequest = l.sniffingRequest
ctx = session.ContextWithContent(ctx, content)
inbound := &session.Inbound{}
@@ -37,20 +40,26 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
// get a shallow copy to avoid modifying the inbound tag in upstream context
*inbound = *originInbound
}
inbound.Tag = l.config.InboundTag
inbound.Tag = l.inboundTag
ctx = session.ContextWithInbound(ctx, inbound)
err := l.dispatcherInstance.DispatchLink(ctx, destination, link)
if err != nil {
errors.New(ctx, "failed to process loopback connection").Base(err)
return err
return errors.New(ctx, "failed to process loopback connection").Base(err)
}
return nil
}
func (l *Loopback) init(config *Config, dispatcherInstance routing.Dispatcher) error {
l.dispatcherInstance = dispatcherInstance
l.config = config
l.inboundTag = config.InboundTag
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()
}
l.sniffingRequest = request
}
return nil
}
-26
View File
@@ -85,8 +85,6 @@ func (a *Account) getCipher() (Cipher, error) {
IVBytes: 32,
AEADAuthCreator: createXChaCha20Poly1305,
}, nil
case CipherType_NONE:
return NoneCipher{}, nil
default:
return nil, errors.New("Unsupported cipher.")
}
@@ -186,30 +184,6 @@ func (c *AEADCipher) DecodePacket(key []byte, b *buf.Buffer) error {
return nil
}
type NoneCipher struct{}
func (NoneCipher) KeySize() int32 { return 0 }
func (NoneCipher) IVSize() int32 { return 0 }
func (NoneCipher) IsAEAD() bool {
return false
}
func (NoneCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) {
return buf.NewReader(reader), nil
}
func (NoneCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) {
return buf.NewWriter(writer), nil
}
func (NoneCipher) EncodePacket(key []byte, b *buf.Buffer) error {
return nil
}
func (NoneCipher) DecodePacket(key []byte, b *buf.Buffer) error {
return nil
}
func passwordToCipherKey(password []byte, keySize int32) []byte {
key := make([]byte, 0, keySize)
+2 -6
View File
@@ -31,7 +31,6 @@ const (
CipherType_AES_256_GCM CipherType = 6
CipherType_CHACHA20_POLY1305 CipherType = 7
CipherType_XCHACHA20_POLY1305 CipherType = 8
CipherType_NONE CipherType = 9
)
// Enum value maps for CipherType.
@@ -42,7 +41,6 @@ var (
6: "AES_256_GCM",
7: "CHACHA20_POLY1305",
8: "XCHACHA20_POLY1305",
9: "NONE",
}
CipherType_value = map[string]int32{
"UNKNOWN": 0,
@@ -50,7 +48,6 @@ var (
"AES_256_GCM": 6,
"CHACHA20_POLY1305": 7,
"XCHACHA20_POLY1305": 8,
"NONE": 9,
}
)
@@ -251,15 +248,14 @@ const file_proxy_shadowsocks_config_proto_rawDesc = "" +
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" +
"\anetwork\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"L\n" +
"\fClientConfig\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*t\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*j\n" +
"\n" +
"CipherType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\x0f\n" +
"\vAES_128_GCM\x10\x05\x12\x0f\n" +
"\vAES_256_GCM\x10\x06\x12\x15\n" +
"\x11CHACHA20_POLY1305\x10\a\x12\x16\n" +
"\x12XCHACHA20_POLY1305\x10\b\x12\b\n" +
"\x04NONE\x10\tBd\n" +
"\x12XCHACHA20_POLY1305\x10\bBd\n" +
"\x1acom.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3"
var (
-1
View File
@@ -23,7 +23,6 @@ enum CipherType {
AES_256_GCM = 6;
CHACHA20_POLY1305 = 7;
XCHACHA20_POLY1305 = 8;
NONE = 9;
}
message ServerConfig {
-17
View File
@@ -38,19 +38,6 @@ func TestUDPEncodingDecoding(t *testing.T) {
}),
},
},
{
Version: Version,
Command: protocol.RequestCommandUDP,
Address: net.LocalHostIP,
Port: 1234,
User: &protocol.MemoryUser{
Email: "love@example.com",
Account: toAccount(&Account{
Password: "123",
CipherType: CipherType_NONE,
}),
},
},
}
for _, request := range testRequests {
@@ -80,10 +67,6 @@ func TestUDPDecodingWithPayloadTooShort(t *testing.T) {
Password: "password",
CipherType: CipherType_AES_128_GCM,
}),
toAccount(&Account{
Password: "password",
CipherType: CipherType_NONE,
}),
}
for _, account := range testAccounts {
-1
View File
@@ -145,7 +145,6 @@ func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol
} else {
u = user
ivLen = user.Account.(*MemoryAccount).Cipher.IVSize()
// err = user.Account.(*MemoryAccount).CheckIV(bs[:ivLen]) // The IV size of None Cipher is 0.
return
}
}
+9 -28
View File
@@ -356,15 +356,13 @@ func (x *Account) GetKey() string {
}
type ClientConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"`
Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"`
UdpOverTcp bool `protobuf:"varint,5,opt,name=udp_over_tcp,json=udpOverTcp,proto3" json:"udp_over_tcp,omitempty"`
UdpOverTcpVersion uint32 `protobuf:"varint,6,opt,name=udp_over_tcp_version,json=udpOverTcpVersion,proto3" json:"udp_over_tcp_version,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"`
Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClientConfig) Reset() {
@@ -425,20 +423,6 @@ func (x *ClientConfig) GetKey() string {
return ""
}
func (x *ClientConfig) GetUdpOverTcp() bool {
if x != nil {
return x.UdpOverTcp
}
return false
}
func (x *ClientConfig) GetUdpOverTcpVersion() uint32 {
if x != nil {
return x.UdpOverTcpVersion
}
return 0
}
var File_proxy_shadowsocks_2022_config_proto protoreflect.FileDescriptor
const file_proxy_shadowsocks_2022_config_proto_rawDesc = "" +
@@ -467,15 +451,12 @@ const file_proxy_shadowsocks_2022_config_proto_rawDesc = "" +
"\fdestinations\x18\x03 \x03(\v2-.xray.proxy.shadowsocks_2022.RelayDestinationR\fdestinations\x122\n" +
"\anetwork\x18\x04 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"\x1b\n" +
"\aAccount\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\"\xd6\x01\n" +
"\x03key\x18\x01 \x01(\tR\x03key\"\x83\x01\n" +
"\fClientConfig\x125\n" +
"\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" +
"\x04port\x18\x02 \x01(\rR\x04port\x12\x16\n" +
"\x06method\x18\x03 \x01(\tR\x06method\x12\x10\n" +
"\x03key\x18\x04 \x01(\tR\x03key\x12 \n" +
"\fudp_over_tcp\x18\x05 \x01(\bR\n" +
"udpOverTcp\x12/\n" +
"\x14udp_over_tcp_version\x18\x06 \x01(\rR\x11udpOverTcpVersionBr\n" +
"\x03key\x18\x04 \x01(\tR\x03keyBr\n" +
"\x1fcom.xray.proxy.shadowsocks_2022P\x01Z0github.com/xtls/xray-core/proxy/shadowsocks_2022\xaa\x02\x1aXray.Proxy.Shadowsocks2022b\x06proto3"
var (
-2
View File
@@ -49,6 +49,4 @@ message ClientConfig {
uint32 port = 2;
string method = 3;
string key = 4;
bool udp_over_tcp = 5;
uint32 udp_over_tcp_version = 6;
}
+6 -23
View File
@@ -10,7 +10,6 @@ import (
B "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -29,10 +28,9 @@ func init() {
}
type Outbound struct {
ctx context.Context
server net.Destination
method shadowsocks.Method
uotClient *uot.Client
ctx context.Context
server net.Destination
method shadowsocks.Method
}
func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
@@ -56,9 +54,6 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
} else {
return nil, errors.New("unknown method ", config.Method)
}
if config.UdpOverTcp {
o.uotClient = &uot.Client{Version: uint8(config.UdpOverTcpVersion)}
}
return o, nil
}
@@ -82,11 +77,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
errors.LogInfo(ctx, "tunneling request to ", destination, " via ", o.server.NetAddr())
serverDestination := o.server
if o.uotClient != nil {
serverDestination.Network = net.Network_TCP
} else {
serverDestination.Network = network
}
serverDestination.Network = network
connection, err := dialer.Dial(ctx, serverDestination)
if err != nil {
return errors.New("failed to connect to server").Base(err)
@@ -149,15 +140,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
}
}
if o.uotClient != nil {
uConn, err := o.uotClient.DialEarlyConn(o.method.DialEarlyConn(connection, uot.RequestDestination(o.uotClient.Version)), false, singbridge.ToSocksaddr(destination))
if err != nil {
return err
}
return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, uConn))
} else {
serverConn := o.method.DialPacketConn(connection)
return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
}
serverConn := o.method.DialPacketConn(connection)
return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
}
}
+4 -3
View File
@@ -209,11 +209,12 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co
}
responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port)
expectedRemote := &gonet.UDPAddr{}
if request.Address.IP().IsUnspecified() {
// UDP Associate should not specify a domain as source IP
if request.Address.Family().IsDomain() || request.Address.IP().IsUnspecified() {
expectedRemote.IP = writer.RemoteAddr().(*net.TCPAddr).IP // unix?
} else {
expectedRemote.IP = request.Address.IP() // panic?
expectedRemote.Port = int(request.Port) // 0 is allowed
expectedRemote.IP = request.Address.IP()
expectedRemote.Port = int(request.Port) // 0 is allowed
}
tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemote)
}
+13 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
goerrors "errors"
"io"
"sync"
"time"
"github.com/xtls/xray-core/common"
@@ -216,18 +217,27 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
defer udpServer.RemoveRay()
inbound := session.InboundFromContext(ctx)
if inbound != nil && inbound.Source.IsValid() {
errors.LogInfo(ctx, "client UDP connection from ", inbound.Source)
}
var dest *net.Destination
reader := buf.NewPacketReader(conn)
var changeRemote sync.Once
for {
mpayload, err := reader.ReadMultiBuffer()
if err != nil {
return err
}
changeRemote.Do(func() {
if inbound != nil {
newInbound := *inbound
// change source to real remote UDP address
newInbound.Source = net.DestinationFromAddr(conn.RemoteAddr())
newInbound.Local = net.DestinationFromAddr(conn.LocalAddr())
inbound = &newInbound
ctx = session.ContextWithInbound(ctx, inbound)
errors.LogInfo(ctx, "client UDP connection from ", inbound.Source)
}
})
for _, payload := range mpayload {
request, err := DecodeUDPPacket(payload)
+12 -5
View File
@@ -14,11 +14,12 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
## DETAILS
Current implementation does not contain options to configure network level addresses, routing or rules.
Enabling the feature will result only tun interface up, and that's it. \
This is explicit decision, significantly simplifying implementation, and allowing any number of custom configurations, consumers could come up with. Network interface is OS level entity, and OS is what should manage it. \
Working configuration, is tun enabled in Xray config with specific name (e.g. xray0), and OS level configuration to manage "xray0" interface, applying routing and rules on interface up.
This way consistency of system level routing and rules is ensured from single place of responsibility - the OS itself. \
By default, enabling the feature will only bring the tun interface up. \
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
Linux and macOS do not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \
@@ -206,6 +207,11 @@ ifconfig
Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists.
It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number.
macOS requires the utun interface to have a point-to-point IPv4 address before IPv4 routes can use it. \
By default Xray uses `169.254.10.1/30` as the remote gateway address and assigns the next address in the prefix to the local utun side. \
You can override this by setting `gateway`; macOS uses the first IPv4 prefix in the list. IPv6 `gateway` entries are not used for utun addressing, and IPv6 routes use the interface route instead. \
The `dns` field does not change macOS system DNS.
To attach routing to the interface, route command like following can be executed:
```
sudo route add -net 1.1.1.0/24 -iface utun10
@@ -214,6 +220,7 @@ sudo route add -net 1.1.1.0/24 -iface utun10
sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10
sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10
```
Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running.
Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure.
## ANDROID SUPPORT
+7 -81
View File
@@ -3,8 +3,6 @@ package tun
import (
"context"
"net"
"sort"
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
@@ -31,95 +29,23 @@ func (updater *InterfaceUpdater) Update() {
updater.Lock()
defer updater.Unlock()
if updater.iface != nil {
iface, err := net.InterfaceByIndex(updater.iface.Index)
if err == nil && iface.Name == updater.iface.Name {
return
}
}
updater.iface = nil
interfaces, err := net.Interfaces()
got, err := findOutboundInterface(updater.tunIndex, updater.fixedName)
if err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] failed to update interface")
updater.iface = nil
return
}
var got *net.Interface
if updater.fixedName != "" {
for _, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if iface.Name == updater.fixedName {
got = &iface
break
}
}
} else {
var ifs []struct {
index int
score int
}
for i, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
ifs = append(ifs, struct {
index int
score int
}{i, score(&iface, addrs)})
}
sort.Slice(ifs, func(i, j int) bool {
if ifs[i].score != ifs[j].score {
return ifs[i].score > ifs[j].score
}
return interfaces[ifs[i].index].Name < interfaces[ifs[j].index].Name
})
if len(ifs) > 0 {
iface := interfaces[ifs[0].index]
got = &iface
}
}
if got == nil {
errors.LogInfo(context.Background(), "[tun] failed to update interface > got == nil")
updater.iface = nil
return
}
if updater.iface != nil && updater.iface.Index == got.Index && updater.iface.Name == got.Name {
return
}
updater.iface = got
errors.LogInfo(context.Background(), "[tun] update interface ", got.Name, " ", got.Index)
}
func score(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score += 1
break
}
}
return score
}
+52 -5
View File
@@ -2,6 +2,8 @@ package tun
import (
"context"
"net/netip"
"strings"
"syscall"
"github.com/xtls/xray-core/common"
@@ -15,6 +17,7 @@ import (
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
@@ -30,6 +33,8 @@ type Handler struct {
dispatcher routing.Dispatcher
tag string
sniffingRequest session.SniffingRequest
uplinkCounter stats.Counter
downlinkCounter stats.Counter
}
// ConnectionHandler interface with the only method that stack is going to push new connections to
@@ -40,10 +45,11 @@ type ConnectionHandler interface {
// Handler implements ConnectionHandler
var _ ConnectionHandler = (*Handler)(nil)
// Handler implements common.Runnable
var _ common.Runnable = (*Handler)(nil)
// Init the Handler instance with necessary parameters
func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routing.Dispatcher) error {
var err error
// Retrieve tag and sniffing config from context (set by AlwaysOnInboundHandler)
if inbound := session.InboundFromContext(ctx); inbound != nil {
t.tag = inbound.Tag
@@ -56,6 +62,27 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
t.policyManager = pm
t.dispatcher = dispatcher
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
t.uplinkCounter = c
}
}
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
t.downlinkCounter = c
}
}
return nil
}
func (t *Handler) Start() error {
tunName := t.config.Name
tunInterface, err := NewTun(t.config)
if err != nil {
@@ -80,6 +107,11 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
return nil
}
return c.Control(func(fd uintptr) {
addrPort, _ := netip.ParseAddrPort(address)
// skip loopback
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
return
}
err := setinterface(network, address, fd, iface)
if err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
@@ -92,7 +124,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
tunStackOptions := StackOptions{
Tun: tunInterface,
IdleTimeout: pm.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle,
IdleTimeout: t.policyManager.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle,
}
tunStack, err := NewStack(t.ctx, tunStackOptions, t)
if err != nil {
@@ -131,7 +163,22 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
defer cancel()
ctx = c.ContextWithID(ctx, session.NewID())
source := net.DestinationFromAddr(conn.RemoteAddr())
// if the connection is already closed, conn.RemoteAddr() will be nil
// due to gvisor weird behavior
remote := conn.RemoteAddr()
if remote == nil {
errors.LogInfo(t.ctx, "dropped quickly closed connection")
return
}
source := net.DestinationFromAddr(remote)
if t.uplinkCounter != nil || t.downlinkCounter != nil {
conn = &stat.CounterConnection{
Connection: conn,
ReadCounter: t.uplinkCounter,
WriteCounter: t.downlinkCounter,
}
}
inbound := session.Inbound{
Name: "tun",
Tag: t.tag,
@@ -167,7 +214,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
// Close implements common.Closable.
func (t *Handler) Close() error {
return errors.Combine(t.stack.Close(), t.tun.Close())
return errors.Combine(common.CloseIfExists(t.stack), common.CloseIfExists(t.tun))
}
// Network implements proxy.Inbound
+133
View File
@@ -0,0 +1,133 @@
package tun
import (
"bytes"
"context"
"net"
"sync/atomic"
"testing"
"time"
"github.com/xtls/xray-core/common/buf"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
)
type testCounter struct {
value int64
}
func (c *testCounter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
func (c *testCounter) Set(value int64) int64 {
return atomic.SwapInt64(&c.value, value)
}
func (c *testCounter) Add(value int64) int64 {
return atomic.AddInt64(&c.value, value) - value
}
type testConn struct {
reader *bytes.Reader
writer bytes.Buffer
}
func newTestConn(input []byte) *testConn {
return &testConn{reader: bytes.NewReader(input)}
}
func (c *testConn) Read(payload []byte) (int, error) {
return c.reader.Read(payload)
}
func (c *testConn) Write(payload []byte) (int, error) {
return c.writer.Write(payload)
}
func (c *testConn) Close() error {
return nil
}
func (c *testConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 1080}
}
func (c *testConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(10, 0, 0, 2), Port: 12345}
}
func (c *testConn) SetDeadline(time.Time) error {
return nil
}
func (c *testConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *testConn) SetWriteDeadline(time.Time) error {
return nil
}
type testDispatcher struct {
writePayload []byte
readBytes int32
}
func (d *testDispatcher) Type() interface{} {
return routing.DispatcherType()
}
func (d *testDispatcher) Start() error {
return nil
}
func (d *testDispatcher) Close() error {
return nil
}
func (d *testDispatcher) Dispatch(context.Context, xnet.Destination) (*transport.Link, error) {
return nil, nil
}
func (d *testDispatcher) DispatchLink(ctx context.Context, dest xnet.Destination, link *transport.Link) error {
mb, err := link.Reader.ReadMultiBuffer()
if err != nil {
return err
}
atomic.StoreInt32(&d.readBytes, mb.Len())
buf.ReleaseMulti(mb)
return link.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(d.writePayload)})
}
func TestHandlerCountsTunConnectionTraffic(t *testing.T) {
uplinkCounter := new(testCounter)
downlinkCounter := new(testCounter)
dispatcher := &testDispatcher{writePayload: []byte("downlink")}
conn := newTestConn([]byte("uplink"))
handler := &Handler{
ctx: context.Background(),
config: &Config{},
dispatcher: dispatcher,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
}
handler.HandleConnection(conn, xnet.TCPDestination(xnet.LocalHostIP, 443))
if got := uplinkCounter.Value(); got != int64(len("uplink")) {
t.Fatalf("unexpected uplink counter: got %d, want %d", got, len("uplink"))
}
if got := downlinkCounter.Value(); got != int64(len("downlink")) {
t.Fatalf("unexpected downlink counter: got %d, want %d", got, len("downlink"))
}
if got := int(atomic.LoadInt32(&dispatcher.readBytes)); got != len("uplink") {
t.Fatalf("dispatcher read unexpected bytes: got %d, want %d", got, len("uplink"))
}
if got := conn.writer.String(); got != "downlink" {
t.Fatalf("connection write mismatch: got %q, want %q", got, "downlink")
}
}
+14
View File
@@ -81,3 +81,17 @@ func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.BindToDevice(int(fd), iface.Name)
}
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")
}
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
}
+327 -15
View File
@@ -3,16 +3,20 @@
package tun
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"os"
"strconv"
"sync"
"unsafe"
"github.com/xtls/xray-core/common/buf"
xerrors "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
@@ -20,11 +24,11 @@ import (
)
const (
utunControlName = "com.apple.net.utun_control"
sysprotoControl = 2
gateway = "169.254.10.1/30"
utunHeaderSize = 4
UTUN_OPT_IFNAME = 2
utunControlName = "com.apple.net.utun_control"
sysprotoControl = 2
defaultDarwinGateway = "169.254.10.1/30"
utunHeaderSize = 4
UTUN_OPT_IFNAME = 2
)
const (
@@ -42,6 +46,11 @@ type DarwinTun struct {
options *Config
tunFd int
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
routeMonitor *os.File
routeMonitorOnce sync.Once
systemRoutes []netip.Prefix
gateway netip.Prefix
}
var (
@@ -77,7 +86,13 @@ func NewTun(options *Config) (Tun, error) {
return nil, err
}
err = setup(options.Name, options.MTU)
gateway, err := selectDarwinGateway(options.Gateway)
if err != nil {
_ = tunFile.Close()
return nil, err
}
err = setup(options.Name, options.MTU, gateway)
if err != nil {
_ = tunFile.Close()
return nil, err
@@ -88,19 +103,58 @@ func NewTun(options *Config) (Tun, error) {
options: options,
tunFd: int(tunFile.Fd()),
ownsFd: true,
gateway: gateway,
}, nil
}
func (t *DarwinTun) Start() error {
if !t.ownsFd {
return nil
}
if err := t.setSystemRoutes(); err != nil {
return err
}
if updater != nil {
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
_ = t.unsetSystemRoutes()
return err
}
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
go t.monitorRouteChanges()
}
return nil
}
func (t *DarwinTun) Close() error {
t.routeMonitorOnce.Do(func() {
if t.routeMonitor != nil {
_ = t.routeMonitor.Close()
}
})
routeErr := t.unsetSystemRoutes()
if t.ownsFd {
return t.tunFile.Close()
return xerrors.Combine(routeErr, t.tunFile.Close())
}
// iOS: don't close the fd, it's owned by NetworkExtension
return nil
return routeErr
}
func (t *DarwinTun) 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()
}
}
}
func (t *DarwinTun) Name() (string, error) {
@@ -235,24 +289,56 @@ func open(name string) (*os.File, error) {
}
// setup the interface by name
func setup(name string, MTU uint32) error {
func setup(name string, MTU uint32, gateway netip.Prefix) error {
if err := setMTU(name, MTU); err != nil {
return err
}
/*
* Darwin routing require tunnel type interface to have local and remote address, to be routable.
* To simplify inevitable task, assign the interface static ip address, which in current implementation
* is just some random ip from link-local pool, allowing to not bother about existing routing intersection.
* To simplify inevitable task, assign the interface static ip address.
*/
syntheticIP, _ := netip.ParsePrefix(gateway)
if err := setIPAddress(name, syntheticIP); err != nil {
if err := setIPAddress(name, gateway); err != nil {
return err
}
return nil
}
func selectDarwinGateway(configured []string) (netip.Prefix, error) {
if len(configured) == 0 {
return netip.ParsePrefix(defaultDarwinGateway)
}
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return netip.Prefix{}, xerrors.New("invalid macOS gateway ", value).Base(err)
}
if !prefix.Addr().Is4() {
continue
}
local, ok := nextDarwinLocalIPv4(prefix)
if !ok || !prefix.Contains(local) {
return netip.Prefix{}, xerrors.New("macOS gateway ", value, " must contain at least one usable local IPv4 address after the gateway address")
}
return prefix, nil
}
return netip.Prefix{}, xerrors.New("macOS gateway requires at least one IPv4 prefix")
}
func nextDarwinLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
local4 := gateway.Addr().As4()
for i := len(local4) - 1; i >= 0; i-- {
local4[i]++
if local4[i] != 0 {
return netip.AddrFrom4(local4), true
}
}
return netip.Addr{}, false
}
// setMTU sets MTU on the interface by given name
func setMTU(name string, mtu uint32) error {
socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
@@ -298,8 +384,11 @@ func setIPAddress(name string, gateway netip.Prefix) error {
defer unix.Close(socket4)
// assume local ip address is next one from the remote address
local4 := gateway.Addr().As4()
local4[3]++
local, ok := nextDarwinLocalIPv4(gateway)
if !ok || !gateway.Contains(local) {
return xerrors.New("macOS gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
}
local4 := local.As4()
// fill the configuration for ipv4
ifReq4 := ifAliasReq4{
@@ -388,3 +477,226 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
return errors.Join(err1, err2)
}
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
}
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
}
var ipv6Index int
for _, message := range messages {
routeMessage, ok := message.(*route.RouteMessage)
if !ok || routeMessage.Index == tunIndex {
continue
}
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
}
family, ok := defaultRouteFamily(routeMessage)
if !ok {
continue
}
if family == unix.AF_INET {
return usableDarwinInterface(routeMessage.Index)
}
if family == unix.AF_INET6 && ipv6Index == 0 {
ipv6Index = routeMessage.Index
}
}
if ipv6Index != 0 {
return usableDarwinInterface(ipv6Index)
}
return nil, errors.New("default route not found")
}
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 usableDarwinInterface(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 (t *DarwinTun) setSystemRoutes() error {
routes, err := buildDarwinSystemRoutes(t.options.AutoSystemRoutingTable)
if err != nil {
return err
}
if len(routes) == 0 {
return nil
}
tunIndex, err := t.Index()
if err != nil {
return err
}
for _, destination := range routes {
if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination, t.gateway); 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 *DarwinTun) unsetSystemRoutes() error {
var errs []error
tunIndex, indexErr := t.Index()
if indexErr != nil && len(t.systemRoutes) > 0 {
errs = append(errs, indexErr)
}
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
destination := t.systemRoutes[i]
if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination, t.gateway); err != nil && !errors.Is(err, unix.ESRCH) {
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
}
}
t.systemRoutes = nil
return xerrors.Combine(errs...)
}
func buildDarwinSystemRoutes(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)
}
prefix = prefix.Masked()
if prefix.Bits() == 0 {
for _, protected := range darwinProtectedDefaultRoutes(prefix.Addr().Is4()) {
appendRoute(protected)
}
continue
}
appendRoute(prefix)
}
return routes, nil
}
func darwinProtectedDefaultRoutes(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
}
func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Prefix) 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
}
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: &route.Inet4Addr{IP: gateway.Addr().As4()},
}
} else {
message.Flags &^= unix.RTF_GATEWAY
message.Index = interfaceIndex
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: &route.LinkAddr{Index: interfaceIndex},
}
}
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)
_, err = unix.Write(fd, request)
return err
}
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
}
+49
View File
@@ -0,0 +1,49 @@
//go:build darwin
package tun
import (
"testing"
)
func TestSelectDarwinGatewayDefault(t *testing.T) {
gateway, err := selectDarwinGateway(nil)
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != defaultDarwinGateway {
t.Fatalf("unexpected default gateway: got %s, want %s", got, defaultDarwinGateway)
}
}
func TestSelectDarwinGatewayConfiguredIPv4(t *testing.T) {
gateway, err := selectDarwinGateway([]string{"198.18.0.1/15"})
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != "198.18.0.1/15" {
t.Fatalf("unexpected gateway: got %s", got)
}
}
func TestSelectDarwinGatewaySkipsIPv6(t *testing.T) {
gateway, err := selectDarwinGateway([]string{"fc00::1/64", "198.18.0.1/15"})
if err != nil {
t.Fatal(err)
}
if got := gateway.String(); got != "198.18.0.1/15" {
t.Fatalf("unexpected gateway: got %s", got)
}
}
func TestSelectDarwinGatewayRequiresIPv4(t *testing.T) {
if _, err := selectDarwinGateway([]string{"fc00::1/64"}); err == nil {
t.Fatal("expected error")
}
}
func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
if _, err := selectDarwinGateway([]string{"198.18.0.1/32"}); err == nil {
t.Fatal("expected error")
}
}
+14
View File
@@ -42,3 +42,17 @@ func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(string, string, uintptr, *net.Interface) error {
return errors.New("Tun is not supported on your platform")
}
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")
}
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
}
+14
View File
@@ -147,3 +147,17 @@ func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
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")
}
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
}
+280 -5
View File
@@ -3,9 +3,15 @@
package tun
import (
"context"
"net"
"net/netip"
"strconv"
"sync"
"github.com/vishvananda/netlink"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -18,6 +24,12 @@ type LinuxTun struct {
tunFd int
tunLink netlink.Link
options *Config
ownsTun bool
interfaceAddresses []netlink.Addr
systemRoutes []netlink.Route
routeMonitorStop chan struct{}
routeMonitorOnce sync.Once
}
// LinuxTun implements Tun
@@ -25,12 +37,24 @@ var _ Tun = (*LinuxTun)(nil)
// NewTun builds new tun interface handler (linux specific)
func NewTun(options *Config) (Tun, error) {
tunFd, err := open(options.Name)
tunFd, tunLink, fdProvided, err := openFromEnv(options.Name)
if err != nil {
return nil, err
}
if fdProvided {
return &LinuxTun{
tunFd: tunFd,
tunLink: tunLink,
options: options,
}, nil
}
tunFd, err = open(options.Name)
if err != nil {
return nil, err
}
tunLink, err := setup(options.Name, int(options.MTU))
tunLink, err = setup(options.Name, int(options.MTU))
if err != nil {
_ = unix.Close(tunFd)
return nil, err
@@ -40,11 +64,59 @@ func NewTun(options *Config) (Tun, error) {
tunFd: tunFd,
tunLink: tunLink,
options: options,
ownsTun: true,
}
return linuxTun, nil
}
func openFromEnv(expectedName string) (int, netlink.Link, bool, error) {
fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" })
if fdStr == "" {
return -1, nil, false, nil
}
fd, err := strconv.Atoi(fdStr)
if err != nil {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey).Base(err)
}
if fd < 3 {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor must be >= 3")
}
ifr, err := unix.NewIfreq("")
if err != nil {
return -1, nil, true, err
}
if err = unix.IoctlIfreq(fd, unix.TUNGETIFF, ifr); err != nil {
return -1, nil, true, err
}
flags := ifr.Uint16()
if flags&unix.IFF_TUN == 0 {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor is not a TUN device")
}
if flags&unix.IFF_NO_PI == 0 {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device must use IFF_NO_PI")
}
actualName := ifr.Name()
if expectedName != "" && actualName != expectedName {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device name ", actualName, " does not match configured name ", expectedName)
}
tunLink, err := netlink.LinkByName(actualName)
if err != nil {
return -1, nil, true, err
}
if err = unix.SetNonblock(fd, true); err != nil {
return -1, nil, true, err
}
return fd, tunLink, true, nil
}
// open the file that implements tun interface in the OS
func open(name string) (int, error) {
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
@@ -93,17 +165,47 @@ func setup(name string, MTU int) (netlink.Link, error) {
// Start is called by handler to bring tun interface to life
func (t *LinuxTun) Start() error {
err := netlink.LinkSetUp(t.tunLink)
if err != nil {
if !t.ownsTun {
return nil
}
if err := netlink.LinkSetUp(t.tunLink); err != nil {
return err
}
if err := t.setInterfaceAddresses(); err != nil {
_ = netlink.LinkSetDown(t.tunLink)
return err
}
if err := t.setSystemRoutes(); err != nil {
_ = t.unsetInterfaceAddresses()
_ = netlink.LinkSetDown(t.tunLink)
return err
}
if updater != nil {
t.routeMonitorStop = make(chan struct{})
go t.monitorRouteChanges()
}
return nil
}
// Close is called to shut down the tun interface
func (t *LinuxTun) Close() error {
_ = netlink.LinkSetDown(t.tunLink)
t.routeMonitorOnce.Do(func() {
if t.routeMonitorStop != nil {
close(t.routeMonitorStop)
}
})
_ = t.unsetSystemRoutes()
_ = t.unsetInterfaceAddresses()
if t.ownsTun {
_ = netlink.LinkSetDown(t.tunLink)
}
_ = unix.Close(t.tunFd)
return nil
@@ -129,3 +231,176 @@ func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.BindToDevice(int(fd), iface.Name)
}
func (t *LinuxTun) setInterfaceAddresses() error {
if len(t.options.Gateway) == 0 {
return nil
}
for _, address := range t.options.Gateway {
addr, err := netlink.ParseAddr(address)
if err != nil {
_ = t.unsetInterfaceAddresses()
return errors.New("invalid interface address ", address).Base(err)
}
if err := netlink.AddrAdd(t.tunLink, addr); err != nil {
_ = t.unsetInterfaceAddresses()
return errors.New("failed to add interface address ", address).Base(err)
}
t.interfaceAddresses = append(t.interfaceAddresses, *addr)
}
return nil
}
func (t *LinuxTun) unsetInterfaceAddresses() error {
var errs []error
for i := len(t.interfaceAddresses) - 1; i >= 0; i-- {
address := t.interfaceAddresses[i]
if err := netlink.AddrDel(t.tunLink, &address); err != nil {
errs = append(errs, errors.New("failed to delete interface address ", address.String()).Base(err))
}
}
t.interfaceAddresses = nil
return errors.Combine(errs...)
}
func (t *LinuxTun) setSystemRoutes() error {
if len(t.options.AutoSystemRoutingTable) == 0 {
return nil
}
tunIndex := t.tunLink.Attrs().Index
for _, cidr := range t.options.AutoSystemRoutingTable {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
return errors.New("invalid system route ", cidr).Base(err)
}
prefix = prefix.Masked()
_, ipNet, _ := net.ParseCIDR(prefix.String())
route := netlink.Route{
LinkIndex: tunIndex,
Dst: ipNet,
Priority: 1,
}
if err := netlink.RouteAdd(&route); err != nil {
_ = t.unsetSystemRoutes()
return errors.New("failed to add system route ", cidr).Base(err)
}
t.systemRoutes = append(t.systemRoutes, route)
}
return nil
}
func (t *LinuxTun) unsetSystemRoutes() error {
var errs []error
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
route := t.systemRoutes[i]
if err := netlink.RouteDel(&route); err != nil {
errs = append(errs, errors.New("failed to delete system route").Base(err))
}
}
t.systemRoutes = nil
return errors.Combine(errs...)
}
func (t *LinuxTun) monitorRouteChanges() {
routeCh := make(chan netlink.RouteUpdate)
if err := netlink.RouteSubscribe(routeCh, t.routeMonitorStop); err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] failed to subscribe route changes")
return
}
linkCh := make(chan netlink.LinkUpdate)
if err := netlink.LinkSubscribe(linkCh, t.routeMonitorStop); err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] failed to subscribe link changes")
return
}
for {
select {
case _, ok := <-routeCh:
if !ok {
return
}
if updater != nil {
updater.Update()
}
case _, ok := <-linkCh:
if !ok {
return
}
if updater != nil {
updater.Update()
}
case <-t.routeMonitorStop:
return
}
}
}
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
}
for _, family := range []int{
netlink.FAMILY_V4,
netlink.FAMILY_V6,
} {
iface, err := findDefaultInterface(family, tunIndex)
if err == nil {
return iface, nil
}
}
return nil, errors.New("no usable outbound interface found")
}
func findDefaultInterface(family int, tunIndex int) (*net.Interface, error) {
routes, err := netlink.RouteList(nil, family)
if err != nil {
return nil, err
}
var selected *net.Interface
selectedMetric := -1
for _, route := range routes {
if route.Dst != nil {
ones, _ := route.Dst.Mask.Size()
if ones != 0 {
continue
}
}
if route.LinkIndex == 0 || route.LinkIndex == tunIndex {
continue
}
iface, err := net.InterfaceByIndex(route.LinkIndex)
if err != nil {
continue
}
if iface.Flags&net.FlagUp == 0 ||
iface.Flags&net.FlagLoopback != 0 {
continue
}
if selected == nil || route.Priority < selectedMetric {
selected = iface
selectedMetric = route.Priority
}
}
if selected == nil {
return nil, errors.New("physical default route not found")
}
return selected, nil
}
+76
View File
@@ -8,6 +8,8 @@ import (
go_errors "errors"
"net"
"net/netip"
"sort"
"strings"
"sync"
"unsafe"
@@ -307,3 +309,77 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
return errors.Combine(err1, err2, err3, err4)
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
if fixedName != "" {
for _, iface := range interfaces {
if iface.Index != tunIndex && iface.Name == fixedName {
return &iface, nil
}
}
return nil, nil
}
var candidates []struct {
index int
score int
}
for i, iface := range interfaces {
if iface.Index == tunIndex {
continue
}
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
candidates = append(candidates, struct {
index int
score int
}{i, scoreWindowsInterface(&iface, addrs)})
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].score != candidates[j].score {
return candidates[i].score > candidates[j].score
}
return interfaces[candidates[i].index].Name < interfaces[candidates[j].index].Name
})
if len(candidates) == 0 {
return nil, nil
}
iface := interfaces[candidates[0].index]
return &iface, nil
}
func scoreWindowsInterface(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score++
break
}
}
return score
}
+19 -11
View File
@@ -98,18 +98,22 @@ type udpConn struct {
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
e, ok := <-c.egress
if !ok {
return nil, io.EOF
}
for {
e, ok := <-c.egress
if !ok {
return nil, io.EOF
}
b := buf.New()
if _, err := b.Write(e.data); err != nil {
return nil, err
}
b.UDP = e.dest
b := buf.New()
if _, err := b.Write(e.data); err != nil {
errors.LogErrorInner(context.Background(), err, "drop packet to ", e.dest, " with size ", len(e.data))
b.Release()
continue
}
b.UDP = e.dest
return buf.MultiBuffer{b}, nil
return buf.MultiBuffer{b}, nil
}
}
// Read packets from the connection
@@ -129,7 +133,11 @@ func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
for i, b := range mb {
dst := c.dst
if b.UDP != nil {
dst = *b.UDP
if b.UDP.Address.Family().IsDomain() {
errors.LogError(context.Background(), "impossible domain packet ", b.UDP, " reply via original target ", dst)
} else {
dst = *b.UDP
}
}
err := c.handler.writePacket(b.Bytes(), dst, c.src)
if err != nil {
-21
View File
@@ -17,27 +17,6 @@ func Authenticate(b []byte) uint32 {
return fnv1hash.Sum32()
}
// [DEPRECATED 2023-06]
type NoOpAuthenticator struct{}
func (NoOpAuthenticator) NonceSize() int {
return 0
}
func (NoOpAuthenticator) Overhead() int {
return 0
}
// Seal implements AEAD.Seal().
func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
return append(dst[:0], plaintext...)
}
// Open implements AEAD.Open().
func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
return append(dst[:0], ciphertext...), nil
}
// GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array.
func GenerateChacha20Poly1305Key(b []byte) []byte {
key := make([]byte, 32)
-30
View File
@@ -116,20 +116,6 @@ func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, write
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamWriter(sizeParser, writer), nil
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil
}
return buf.NewWriter(writer), nil
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(c.requestBodyKey[:])
auth := &crypto.AEADAuthenticator{
@@ -267,22 +253,6 @@ func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, read
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamReader(sizeParser, reader), nil
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil
}
return buf.NewReader(reader), nil
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(c.responseBodyKey[:])
-30
View File
@@ -262,21 +262,6 @@ func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reade
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamReader(sizeParser, reader), nil
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil
}
return buf.NewReader(reader), nil
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(s.requestBodyKey[:])
auth := &crypto.AEADAuthenticator{
@@ -384,21 +369,6 @@ func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writ
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamWriter(sizeParser, writer), nil
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil
}
return buf.NewWriter(writer), nil
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(s.responseBodyKey[:])
auth := &crypto.AEADAuthenticator{
+1 -7
View File
@@ -105,7 +105,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
account := request.User.Account.(*vmess.MemoryAccount)
request.Security = account.Security
if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_NONE || request.Security == protocol.SecurityType_CHACHA20_POLY1305 {
if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_CHACHA20_POLY1305 {
request.Option.Set(protocol.RequestOptionChunkMasking)
}
@@ -113,12 +113,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
request.Option.Set(protocol.RequestOptionGlobalPadding)
}
if request.Security == protocol.SecurityType_ZERO {
request.Security = protocol.SecurityType_NONE
request.Option.Clear(protocol.RequestOptionChunkStream)
request.Option.Clear(protocol.RequestOptionChunkMasking)
}
if account.AuthenticatedLengthExperiment {
request.Option.Set(protocol.RequestOptionAuthenticatedLength)
}
+122 -237
View File
@@ -2,265 +2,150 @@ package wireguard
import (
"context"
gonet "net"
goerrors "errors"
"io"
"net"
"net/netip"
"runtime"
"strconv"
"sync"
"syscall"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/transport/internet"
"golang.zx2c4.com/wireguard/conn"
)
type netReadInfo struct {
buff *buf.Buffer
endpoint conn.Endpoint
type bind struct {
resolveFunc func(host string) (net.IP, error)
listenFunc func() (net.PacketConn, error)
downFunc func() error
reserved []byte
net.PacketConn
closeCh chan struct{}
mu sync.Mutex
}
// reduce duplicated code
type netBind struct {
dns dns.Client
dnsOption dns.IPOption
func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
b.mu.Lock()
defer b.mu.Unlock()
workers int
readQueue chan *netReadInfo
closedCh chan struct{}
if b.PacketConn != nil {
return nil, 0, conn.ErrBindAlreadyOpen
}
c, err := b.listenFunc()
if err != nil {
return nil, 0, err
}
b.PacketConn = c
ch := make(chan struct{})
b.closeCh = ch
return []conn.ReceiveFunc{
func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) {
for {
n, addr, err := c.ReadFrom(bufs[0])
if err != nil {
if goerrors.Is(err, io.EOF) || goerrors.Is(err, io.ErrClosedPipe) || goerrors.Is(err, net.ErrClosed) {
select {
case <-ch:
default:
errors.LogErrorInner(context.Background(), err, "unexpected closed")
if b.downFunc != nil {
go func() {
common.Must(b.downFunc())
}()
}
}
return 0, net.ErrClosed
}
errors.LogErrorInner(context.Background(), err, "bind recv err")
continue
}
if n > 3 {
bufs[0][1] = 0
bufs[0][2] = 0
bufs[0][3] = 0
}
sizes[0] = n
eps[0] = &conn.StdNetEndpoint{AddrPort: addr.(*net.UDPAddr).AddrPort()}
return 1, nil
}
},
}, uint16(c.LocalAddr().(*net.UDPAddr).Port), nil
}
// SetMark implements conn.Bind
func (bind *netBind) SetMark(mark uint32) error {
func (b *bind) Close() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.PacketConn != nil {
close(b.closeCh)
_ = b.PacketConn.Close()
b.PacketConn = nil
}
return nil
}
// ParseEndpoint implements conn.Bind
func (n *netBind) ParseEndpoint(s string) (conn.Endpoint, error) {
ipStr, port, err := net.SplitHostPort(s)
if err != nil {
return nil, err
}
portNum, err := strconv.Atoi(port)
if err != nil {
return nil, err
func (b *bind) SetMark(mark uint32) error {
return nil
}
func (b *bind) Send(bufs [][]byte, ep conn.Endpoint) (err error) {
b.mu.Lock()
c := b.PacketConn
b.mu.Unlock()
if c == nil {
return syscall.EAFNOSUPPORT
}
addr := net.ParseAddress(ipStr)
if addr.Family() == net.AddressFamilyDomain {
ips, _, err := n.dns.LookupIP(addr.Domain(), n.dnsOption)
for i := range bufs {
if len(bufs[i]) > 3 && len(b.reserved) == 3 {
bufs[i][1] = b.reserved[0]
bufs[i][2] = b.reserved[1]
bufs[i][3] = b.reserved[2]
}
_, err = c.WriteTo(bufs[i], net.UDPAddrFromAddrPort(ep.(*conn.StdNetEndpoint).AddrPort))
if err != nil {
return nil, err
} else if len(ips) == 0 {
return nil, dns.ErrEmptyResponse
}
addr = net.IPAddress(ips[0])
}
dst := net.Destination{
Address: addr,
Port: net.Port(portNum),
Network: net.Network_UDP,
}
return &netEndpoint{
dst: dst,
}, nil
}
// BatchSize implements conn.Bind
func (bind *netBind) BatchSize() int {
return 1
}
// Open implements conn.Bind
func (bind *netBind) Open(uport uint16) ([]conn.ReceiveFunc, uint16, error) {
bind.closedCh = make(chan struct{})
errors.LogDebug(context.Background(), "bind opened")
fun := func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) {
select {
case r := <-bind.readQueue:
sizes[0], eps[0] = copy(bufs[0], r.buff.Bytes()), r.endpoint
r.buff.Release()
return 1, nil
case <-bind.closedCh:
errors.LogDebug(context.Background(), "recv func closed")
return 0, gonet.ErrClosed
errors.LogErrorInner(context.Background(), err, "bind send err")
break
}
}
workers := bind.workers
if workers <= 0 {
workers = runtime.NumCPU()
}
if workers <= 0 {
workers = 1
}
arr := make([]conn.ReceiveFunc, workers)
for i := 0; i < workers; i++ {
arr[i] = fun
}
return arr, uint16(uport), nil
}
// Close implements conn.Bind
func (bind *netBind) Close() error {
errors.LogDebug(context.Background(), "bind closed")
if bind.closedCh != nil {
close(bind.closedCh)
}
return nil
}
type netBindClient struct {
netBind
ctx context.Context
dialer internet.Dialer
reserved []byte
}
func (bind *netBindClient) connectTo(endpoint *netEndpoint) error {
c, err := bind.dialer.Dial(bind.ctx, endpoint.dst)
if err != nil {
return err
}
endpoint.conn = c
go func() {
for {
buff := buf.NewWithSize(device.MaxMessageSize)
n, err := buff.ReadFrom(c)
if err != nil {
buff.Release()
endpoint.conn = nil
c.Close()
return
}
rawBytes := buff.Bytes()
if n > 3 {
rawBytes[1] = 0
rawBytes[2] = 0
rawBytes[3] = 0
}
select {
case bind.readQueue <- &netReadInfo{
buff: buff,
endpoint: endpoint,
}:
case <-bind.closedCh:
buff.Release()
endpoint.conn = nil
c.Close()
return
}
}
}()
return nil
}
func (bind *netBindClient) Send(buff [][]byte, endpoint conn.Endpoint) error {
var err error
nend, ok := endpoint.(*netEndpoint)
if !ok {
return conn.ErrWrongEndpointType
}
if nend.conn == nil {
err = bind.connectTo(nend)
if err != nil {
return err
}
}
for _, buff := range buff {
if len(buff) > 3 && len(bind.reserved) == 3 {
copy(buff[1:], bind.reserved)
}
if _, err = nend.conn.Write(buff); err != nil {
return err
}
}
return nil
}
type netBindServer struct {
netBind
}
func (bind *netBindServer) Send(buff [][]byte, endpoint conn.Endpoint) error {
var err error
nend, ok := endpoint.(*netEndpoint)
if !ok {
return conn.ErrWrongEndpointType
}
if nend.conn == nil {
errors.LogDebug(context.Background(), nend.dst.NetAddr(), " send on closed peer")
return errors.New("peer closed")
}
for _, buff := range buff {
if _, err = nend.conn.Write(buff); err != nil {
return err
}
}
return err
}
type netEndpoint struct {
dst net.Destination
conn net.Conn
}
func (netEndpoint) ClearSrc() {}
func (e netEndpoint) DstIP() netip.Addr {
return netip.Addr{}
}
func (e netEndpoint) SrcIP() netip.Addr {
return netip.Addr{}
}
func (e netEndpoint) DstToBytes() []byte {
var dat []byte
if e.dst.Address.Family().IsIPv4() {
dat = e.dst.Address.IP().To4()[:]
} else {
dat = e.dst.Address.IP().To16()[:]
}
dat = append(dat, byte(e.dst.Port), byte(e.dst.Port>>8))
return dat
}
func (e netEndpoint) DstToString() string {
return e.dst.NetAddr()
}
func (e netEndpoint) SrcToString() string {
return ""
}
func toNetIpAddr(addr net.Address) netip.Addr {
if addr.Family().IsIPv4() {
ip := addr.IP()
return netip.AddrFrom4([4]byte{ip[0], ip[1], ip[2], ip[3]})
} else {
ip := addr.IP()
arr := [16]byte{}
for i := 0; i < 16; i++ {
arr[i] = ip[i]
func (b *bind) ParseEndpoint(s string) (conn.Endpoint, error) {
if b.resolveFunc == nil {
e, err := netip.ParseAddrPort(s)
if err != nil {
return nil, err
}
return netip.AddrFrom16(arr)
return &conn.StdNetEndpoint{
AddrPort: e,
}, nil
}
host, sport, err := net.SplitHostPort(s)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(sport)
if err != nil {
return nil, err
}
if port < 0 || port > 65535 {
return nil, errors.New("invalid port " + sport)
}
ip, err := b.resolveFunc(host)
if err != nil {
return nil, err
}
addr, _ := netip.AddrFromSlice(ip)
return &conn.StdNetEndpoint{
AddrPort: netip.AddrPortFrom(addr, uint16(port)),
}, nil
}
func (b *bind) BatchSize() int {
return 1
}
+351 -250
View File
@@ -1,148 +1,135 @@
/*
Some of codes are copied from https://github.com/octeep/wireproxy, license below.
Copyright (c) 2022 Wind T.F. Wong <octeep@pm.me>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package wireguard
import (
"context"
"fmt"
gonet "net"
"net/netip"
reflect "reflect"
"strings"
"sync"
"golang.zx2c4.com/wireguard/tun"
"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/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"golang.zx2c4.com/wireguard/device"
)
// Handler is an outbound connection that silently swallow the entire payload.
type Handler struct {
conf *DeviceConfig
net Tunnel
bind *netBindClient
policyManager policy.Manager
dns dns.Client
// cached configuration
endpoints []netip.Addr
hasIPv4, hasIPv6 bool
wgLock sync.Mutex
streamSettings *internet.MemoryStreamConfig
uplinkCounter stats.Counter
downlinkCounter stats.Counter
tun tun.Device
tnet *Net
dev *device.Device
mu sync.Mutex
}
// New creates a new wireguard handler.
func New(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
v := core.MustFromContext(ctx)
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
d := v.GetFeature(dns.ClientType()).(dns.Client)
endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf)
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
tag := session.FullHandlerFromContext(ctx).Tag()
var uplinkCounter stats.Counter
var downlinkCounter stats.Counter
if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
}
if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
}
if len(conf.Peers) == 0 {
return nil, errors.New("empty peers")
}
for _, peer := range conf.Peers {
if peer.PublicKey == "" {
return nil, errors.New("peer without publickey")
}
if peer.Endpoint == "" {
return nil, errors.New("peer without endpoint")
}
}
localAddresses := make([]netip.Addr, 0, len(conf.Endpoint))
for _, localaddress := range conf.Endpoint {
addr, err := netip.ParseAddr(localaddress)
if err == nil {
localAddresses = append(localAddresses, addr)
continue
}
prefix, err := netip.ParsePrefix(localaddress)
if err == nil {
localAddresses = append(localAddresses, prefix.Addr())
continue
}
return nil, err
}
kernelTunSupported, err := KernelTunSupported()
if err != nil {
errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support")
}
var tun tun.Device
var tnet *Net
if !conf.NoKernelTun && kernelTunSupported {
errors.LogWarning(context.Background(), "Using kernel TUN")
tun, tnet, err = createKernelTun(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu))
} else {
errors.LogWarning(context.Background(), "Using gVisor TUN")
tun, tnet, _, err = CreateNetTUN(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu), true)
}
if err != nil {
return nil, err
}
d := v.GetFeature(dns.ClientType()).(dns.Client)
return &Handler{
conf: conf,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
policyManager: p,
dns: d,
endpoints: endpoints,
hasIPv4: hasIPv4,
hasIPv6: hasIPv6,
streamSettings: streamSettings,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
tun: tun,
tnet: tnet,
}, nil
}
func (h *Handler) Close() (err error) {
go func() {
h.wgLock.Lock()
defer h.wgLock.Unlock()
if h.net != nil {
_ = h.net.Close()
h.net = nil
}
}()
return nil
}
func (h *Handler) processWireGuard(ctx context.Context, dialer internet.Dialer) (err error) {
h.wgLock.Lock()
defer h.wgLock.Unlock()
if h.bind != nil && h.bind.dialer == dialer && h.net != nil {
return nil
}
log.Record(&log.GeneralMessage{
Severity: log.Severity_Info,
Content: "switching dialer",
})
if h.net != nil {
_ = h.net.Close()
h.net = nil
}
if h.bind != nil {
_ = h.bind.Close()
h.bind = nil
}
// bind := conn.NewStdNetBind() // TODO: conn.Bind wrapper for dialer
h.bind = &netBindClient{
netBind: netBind{
dns: h.dns,
dnsOption: dns.IPOption{
IPv4Enable: h.hasIPv4,
IPv6Enable: h.hasIPv6,
},
workers: int(h.conf.NumWorkers),
readQueue: make(chan *netReadInfo),
},
ctx: ctx,
dialer: dialer,
reserved: h.conf.Reserved,
}
defer func() {
if err != nil {
h.bind.Close()
h.bind = nil
}
}()
h.net, err = h.makeVirtualTun()
if err != nil {
return errors.New("failed to create virtual tun interface").Base(err)
}
return nil
}
// Process implements OutboundHandler.Dispatch().
// Process implements proxy.Outbound.Process.
func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
@@ -152,40 +139,31 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.Name = "wireguard"
ob.CanSpliceCopy = 3
if err := h.processWireGuard(ctx, dialer); err != nil {
if h.dev == nil {
if err := h.init(ctx); err != nil {
return err
}
}
if err := h.dev.Up(); err != nil {
return err
}
// Destination of the inner request.
destination := ob.Target
command := protocol.RequestCommandTCP
if destination.Network == net.Network_UDP {
command = protocol.RequestCommandUDP
var addr netip.Addr
if ob.Target.Address.Family().IsDomain() {
ip, err := h.resolveRemote(ob.Target.Address.String())
if err != nil {
return errors.New("failed to resolve domain").Base(err)
}
addr, _ = netip.AddrFromSlice(ip)
} else {
addr, _ = netip.AddrFromSlice(ob.Target.Address.IP())
}
// resolve dns
addr := destination.Address
if addr.Family().IsDomain() {
ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.preferIP4(),
IPv6Enable: h.hasIPv6 && h.conf.preferIP6(),
})
{ // Resolve fallback
if (len(ips) == 0 || err != nil) && h.conf.hasFallback() {
ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.fallbackIP4(),
IPv6Enable: h.hasIPv6 && h.conf.fallbackIP6(),
})
}
}
if err != nil {
return errors.New("failed to lookup DNS").Base(err)
} else if len(ips) == 0 {
return dns.ErrEmptyResponse
}
addr = net.IPAddress(ips[dice.Roll(len(ips))])
addrPort := netip.AddrPortFrom(addr, ob.Target.Port.Value())
if !addrPort.IsValid() {
return errors.New("invalid target ", ob.Target)
}
destination.Address = addr
var newCtx context.Context
var newCancel context.CancelFunc
@@ -193,59 +171,64 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
newCtx, newCancel = context.WithCancel(context.Background())
}
p := h.policyManager.ForLevel(0)
sessionPolicy := h.policyManager.ForLevel(0)
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, func() {
cancel()
if newCancel != nil {
newCancel()
}
}, p.Timeouts.ConnectionIdle)
addrPort := netip.AddrPortFrom(toNetIpAddr(addr), destination.Port.Value())
}, sessionPolicy.Timeouts.ConnectionIdle)
var requestFunc func() error
var responseFunc func() error
if newCtx != nil {
ctx = newCtx
}
if command == protocol.RequestCommandTCP {
conn, err := h.net.DialContextTCPAddrPort(ctx, addrPort)
var reader buf.Reader
var writer buf.Writer
switch ob.Target.Network {
case net.Network_TCP:
var conn net.Conn
var err error
if sessionPolicy.Timeouts.Handshake != 0 {
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, sessionPolicy.Timeouts.Handshake)
conn, err = h.tnet.DialContextTCPAddrPort(timeoutCtx, addrPort)
timeoutCancel()
} else {
conn, err = h.tnet.DialContextTCPAddrPort(ctx, addrPort)
}
if err != nil {
return errors.New("failed to create TCP connection").Base(err)
}
defer conn.Close()
requestFunc = func() error {
defer timer.SetTimeout(p.Timeouts.DownlinkOnly)
return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer))
}
responseFunc = func() error {
defer timer.SetTimeout(p.Timeouts.UplinkOnly)
return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer))
}
} else if command == protocol.RequestCommandUDP {
conn, err := h.net.DialUDPAddrPort(netip.AddrPort{}, addrPort)
reader = buf.NewReader(conn)
writer = buf.NewWriter(conn)
case net.Network_UDP:
conn, err := h.tnet.DialUDPAddrPort(netip.AddrPort{}, addrPort)
if err != nil {
return errors.New("failed to create UDP connection").Base(err)
}
defer conn.Close()
conn = &udpConnClient{
Conn: conn,
dest: destination,
}
requestFunc = func() error {
defer timer.SetTimeout(p.Timeouts.DownlinkOnly)
return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer))
}
responseFunc = func() error {
defer timer.SetTimeout(p.Timeouts.UplinkOnly)
return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer))
c := &udpConnClient{
PacketConn: conn.(*internet.PacketConnWrapper).PacketConn,
resolveFunc: h.resolveRemote,
dest: gonet.UDPAddrFromAddrPort(addrPort),
}
reader = c
writer = c
default:
panic(ob.Target.Network)
}
if newCtx != nil {
ctx = newCtx
requestFunc := func() error {
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
}
responseFunc := func() error {
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
}
responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer))
@@ -258,108 +241,191 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return nil
}
// creates a tun interface on netstack given a configuration
func (h *Handler) makeVirtualTun() (Tunnel, error) {
t, err := h.conf.createTun()(h.endpoints, int(h.conf.Mtu), nil)
func (h *Handler) Close() (err error) {
h.mu.Lock()
defer h.mu.Unlock()
if h.dev != nil {
h.dev.Close()
h.dev = nil
h.tun = nil
} else if h.tun != nil {
h.tun.Close()
h.tun = nil
}
return nil
}
func (h *Handler) init(ctx context.Context) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.dev != nil {
return nil
}
resolveFunc := h.resolveLocal
listenFunc := func() (net.PacketConn, error) {
dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint)
if err != nil {
return nil, err
}
conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings)
if err != nil {
return nil, err
}
var pktConn net.PacketConn
switch c := conn.(type) {
case *internet.PacketConnWrapper:
pktConn = c.PacketConn
case *cnc.Connection:
pktConn = &internet.FakePacketConn{Conn: c}
default:
panic(reflect.TypeOf(c))
}
if h.streamSettings.UdpmaskManager != nil {
newConn, err := h.streamSettings.UdpmaskManager.WrapPacketConnClient(pktConn)
if err != nil {
pktConn.Close()
return nil, errors.New("mask err").Base(err)
}
pktConn = newConn
}
if h.uplinkCounter != nil || h.downlinkCounter != nil {
pktConn = &PacketCounterConnection{
PacketConn: pktConn,
ReadCounter: h.downlinkCounter,
WriteCounter: h.uplinkCounter,
}
}
return pktConn, nil
}
bind := &bind{}
logger := &device.Logger{
Verbosef: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Debug,
Content: fmt.Sprintf(format, args...),
})
},
Errorf: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Error,
Content: fmt.Sprintf(format, args...),
})
},
}
dev := device.NewDevice(h.tun, bind, logger)
bind.resolveFunc = resolveFunc
bind.listenFunc = listenFunc
bind.downFunc = dev.Down
bind.reserved = h.conf.Reserved
var cfg strings.Builder
cfg.WriteString("private_key=" + h.conf.SecretKey + "\n")
for _, peer := range h.conf.Peers {
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
if peer.PreSharedKey != "" {
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
}
cfg.WriteString("endpoint=" + peer.Endpoint + "\n")
for _, ip := range peer.AllowedIps {
cfg.WriteString("allowed_ip=" + ip + "\n")
}
if peer.KeepAlive != "" {
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
}
}
err := dev.IpcSet(cfg.String())
if err != nil {
return err
}
err = dev.Up()
if err != nil {
return err
}
h.dev = dev
return nil
}
func (h *Handler) resolveLocal(host string) (net.IP, error) {
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
return ips, err
})
}
func (h *Handler) resolveRemote(host string) (net.IP, error) {
return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) {
addrs, err := h.tnet.LookupHost(host)
if err != nil {
return nil, err
}
ips := make([]net.IP, 0, len(addrs))
for _, addr := range addrs {
ips = append(ips, net.ParseIP(addr))
}
return ips, nil
})
}
func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) {
if ip := net.ParseIP(host); ip != nil {
return ip, nil
}
ips, err := lookupIP(host)
if err != nil {
return nil, err
}
h.bind.dnsOption.IPv4Enable = h.hasIPv4
h.bind.dnsOption.IPv6Enable = h.hasIPv6
if err = t.BuildDevice(h.createIPCRequest(), h.bind); err != nil {
_ = t.Close()
return nil, err
if len(ips) == 0 {
return nil, dns.ErrEmptyResponse
}
return t, nil
}
// serialize the config into an IPC request
func (h *Handler) createIPCRequest() string {
var request strings.Builder
request.WriteString(fmt.Sprintf("private_key=%s\n", h.conf.SecretKey))
if !h.conf.IsClient {
// placeholder, we'll handle actual port listening on Xray
request.WriteString("listen_port=1337\n")
}
for _, peer := range h.conf.Peers {
if peer.PublicKey != "" {
request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey))
}
if peer.PreSharedKey != "" {
request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey))
}
address, port, err := net.SplitHostPort(peer.Endpoint)
if err != nil {
errors.LogError(h.bind.ctx, "failed to split endpoint ", peer.Endpoint, " into address and port")
}
addr := net.ParseAddress(address)
if addr.Family().IsDomain() {
dialerIp := h.bind.dialer.DestIpAddress()
if dialerIp != nil {
addr = net.ParseAddress(dialerIp.String())
errors.LogInfo(h.bind.ctx, "createIPCRequest use dialer dest ip: ", addr)
} else {
ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.conf.preferIP4(),
IPv6Enable: h.conf.preferIP6(),
})
{ // Resolve fallback
if (len(ips) == 0 || err != nil) && h.conf.hasFallback() {
ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.conf.fallbackIP4(),
IPv6Enable: h.conf.fallbackIP6(),
})
}
}
if err != nil {
errors.LogInfoInner(h.bind.ctx, err, "createIPCRequest failed to lookup DNS")
} else if len(ips) == 0 {
errors.LogInfo(h.bind.ctx, "createIPCRequest empty lookup DNS")
} else {
addr = net.IPAddress(ips[dice.Roll(len(ips))])
}
}
}
if peer.Endpoint != "" {
request.WriteString(fmt.Sprintf("endpoint=%s:%s\n", addr, port))
}
for _, ip := range peer.AllowedIps {
request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip))
}
if peer.KeepAlive != 0 {
request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive))
var got4, got6 []net.IP
for _, ip := range ips {
if ip.To4() != nil {
got4 = append(got4, ip)
} else {
got6 = append(got6, ip)
}
}
return request.String()[:request.Len()]
var got []net.IP
switch strategy {
case DeviceConfig_FORCE_IP:
got = ips
return ips[dice.Roll(len(ips))], nil
case DeviceConfig_FORCE_IP4:
got = got4
case DeviceConfig_FORCE_IP6:
got = got6
case DeviceConfig_FORCE_IP46:
got = got4
if len(got) == 0 {
got = got6
}
case DeviceConfig_FORCE_IP64:
got = got6
if len(got) == 0 {
got = got4
}
default:
panic(strategy)
}
if len(got) == 0 {
return nil, dns.ErrEmptyResponse
}
return got[dice.Roll(len(got))], nil
}
type udpConnClient struct {
net.Conn
dest net.Destination
net.PacketConn
resolveFunc func(host string) (net.IP, error)
dest *net.UDPAddr
}
func (c *udpConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) {
b := buf.New()
b.Resize(0, buf.Size)
n, addr, err := c.Conn.(net.PacketConn).ReadFrom(b.Bytes())
n, addr, err := c.PacketConn.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return nil, err
}
if addr == nil { // should never hit
addr = c.dest.RawNetAddr()
}
b.Resize(0, int32(n))
b.UDP = &net.Destination{
@@ -375,9 +441,22 @@ func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error {
for i, b := range mb {
dst := c.dest
if b.UDP != nil {
dst = *b.UDP
if b.UDP.Address.Family().IsDomain() {
ip, err := c.resolveFunc(b.UDP.Address.String())
if err != nil {
errors.LogErrorInner(context.Background(), err, "drop packet to ", b.UDP, " with size ", len(b.Bytes()))
b.Release()
continue
}
dst = &net.UDPAddr{
IP: ip,
Port: int(b.UDP.Port),
}
} else {
dst = b.UDP.RawNetAddr().(*net.UDPAddr)
}
}
_, err := c.Conn.(net.PacketConn).WriteTo(b.Bytes(), dst.RawNetAddr())
_, err := c.PacketConn.WriteTo(b.Bytes(), dst)
if err != nil {
buf.ReleaseMulti(mb[i:])
return err
@@ -386,3 +465,25 @@ func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error {
}
return nil
}
type PacketCounterConnection struct {
net.PacketConn
ReadCounter stats.Counter
WriteCounter stats.Counter
}
func (c *PacketCounterConnection) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, addr, err = c.PacketConn.ReadFrom(p)
if err == nil && c.ReadCounter != nil {
c.ReadCounter.Add(int64(n))
}
return
}
func (c *PacketCounterConnection) WriteTo(p []byte, addr net.Addr) (n int, err error) {
n, err = c.PacketConn.WriteTo(p, addr)
if err == nil && c.WriteCounter != nil {
c.WriteCounter.Add(int64(n))
}
return
}
+50 -44
View File
@@ -1,54 +1,60 @@
package wireguard
import (
"context"
"encoding/hex"
"net/netip"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"google.golang.org/protobuf/proto"
)
func (c *DeviceConfig) preferIP4() bool {
return c.DomainStrategy == DeviceConfig_FORCE_IP ||
c.DomainStrategy == DeviceConfig_FORCE_IP4 ||
c.DomainStrategy == DeviceConfig_FORCE_IP46
}
func (c *DeviceConfig) preferIP6() bool {
return c.DomainStrategy == DeviceConfig_FORCE_IP ||
c.DomainStrategy == DeviceConfig_FORCE_IP6 ||
c.DomainStrategy == DeviceConfig_FORCE_IP64
}
func (c *DeviceConfig) hasFallback() bool {
return c.DomainStrategy == DeviceConfig_FORCE_IP46 || c.DomainStrategy == DeviceConfig_FORCE_IP64
}
func (c *DeviceConfig) fallbackIP4() bool {
return c.DomainStrategy == DeviceConfig_FORCE_IP64
}
func (c *DeviceConfig) fallbackIP6() bool {
return c.DomainStrategy == DeviceConfig_FORCE_IP46
}
func (c *DeviceConfig) createTun() tunCreator {
if !c.IsClient {
// See tun_linux.go createKernelTun()
errors.LogWarning(context.Background(), "Using gVisor TUN. WG inbound doesn't support kernel TUN yet.")
return createGVisorTun
}
if c.NoKernelTun {
errors.LogWarning(context.Background(), "Using gVisor TUN. NoKernelTun is set to true.")
return createGVisorTun
}
kernelTunSupported, err := KernelTunSupported()
func (p *PeerConfig) AsAccount() (protocol.Account, error) {
pub, err := ParseKey(p.PublicKey)
if err != nil {
errors.LogWarning(context.Background(), "Using gVisor TUN. Failed to check kernel TUN support:", err)
return createGVisorTun
return nil, err
}
if !kernelTunSupported {
errors.LogWarning(context.Background(), "Using gVisor TUN. Kernel TUN is not supported on your OS, or your permission is insufficient.")
return createGVisorTun
allowedIPs := make([]netip.Prefix, 0, len(p.AllowedIps))
for i := range p.AllowedIps {
p, err := netip.ParsePrefix(p.AllowedIps[i])
if err != nil {
return nil, err
}
allowedIPs = append(allowedIPs, p)
}
return &MemoryAccount{
Pub: *pub,
AllowedIPs: allowedIPs,
PreSharedKey: p.PreSharedKey,
KeepAlive: p.KeepAlive,
}, nil
}
type MemoryAccount struct {
Pub [32]byte
AllowedIPs []netip.Prefix
PreSharedKey string
KeepAlive string
}
func (a *MemoryAccount) Equals(other protocol.Account) bool {
if b, ok := other.(*MemoryAccount); ok {
return a.Pub == b.Pub
}
return false
}
func (a *MemoryAccount) ToProto() proto.Message {
allowedIPs := make([]string, 0, len(a.AllowedIPs))
for i := range a.AllowedIPs {
allowedIPs = append(allowedIPs, a.AllowedIPs[i].String())
}
return &PeerConfig{
PublicKey: hex.EncodeToString(a.Pub[:]),
AllowedIps: allowedIPs,
PreSharedKey: a.PreSharedKey,
KeepAlive: a.KeepAlive,
}
errors.LogWarning(context.Background(), "Using kernel TUN.")
return createKernelTun
}
+26 -24
View File
@@ -7,6 +7,7 @@
package wireguard
import (
protocol "github.com/xtls/xray-core/common/protocol"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -81,7 +82,7 @@ type PeerConfig struct {
PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
PreSharedKey string `protobuf:"bytes,2,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty"`
Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
KeepAlive uint32 `protobuf:"varint,4,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty"`
KeepAlive string `protobuf:"bytes,4,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty"`
AllowedIps []string `protobuf:"bytes,5,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -138,11 +139,11 @@ func (x *PeerConfig) GetEndpoint() string {
return ""
}
func (x *PeerConfig) GetKeepAlive() uint32 {
func (x *PeerConfig) GetKeepAlive() string {
if x != nil {
return x.KeepAlive
}
return 0
return ""
}
func (x *PeerConfig) GetAllowedIps() []string {
@@ -157,8 +158,8 @@ type DeviceConfig struct {
SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"`
Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"`
Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"`
Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"`
NumWorkers int32 `protobuf:"varint,5,opt,name=num_workers,json=numWorkers,proto3" json:"num_workers,omitempty"`
Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"`
DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"`
IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"`
@@ -218,6 +219,13 @@ func (x *DeviceConfig) GetPeers() []*PeerConfig {
return nil
}
func (x *DeviceConfig) GetUsers() []*protocol.User {
if x != nil {
return x.Users
}
return nil
}
func (x *DeviceConfig) GetMtu() int32 {
if x != nil {
return x.Mtu
@@ -225,13 +233,6 @@ func (x *DeviceConfig) GetMtu() int32 {
return 0
}
func (x *DeviceConfig) GetNumWorkers() int32 {
if x != nil {
return x.NumWorkers
}
return 0
}
func (x *DeviceConfig) GetReserved() []byte {
if x != nil {
return x.Reserved
@@ -264,7 +265,7 @@ var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
const file_proxy_wireguard_config_proto_rawDesc = "" +
"\n" +
"\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\"\xad\x01\n" +
"\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\x1a\x1acommon/protocol/user.proto\"\xad\x01\n" +
"\n" +
"PeerConfig\x12\x1d\n" +
"\n" +
@@ -272,17 +273,16 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
"\x0epre_shared_key\x18\x02 \x01(\tR\fpreSharedKey\x12\x1a\n" +
"\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x1d\n" +
"\n" +
"keep_alive\x18\x04 \x01(\rR\tkeepAlive\x12\x1f\n" +
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
"\vallowed_ips\x18\x05 \x03(\tR\n" +
"allowedIps\"\xcb\x03\n" +
"allowedIps\"\xdc\x03\n" +
"\fDeviceConfig\x12\x1d\n" +
"\n" +
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
"\bendpoint\x18\x02 \x03(\tR\bendpoint\x126\n" +
"\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x12\x10\n" +
"\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1f\n" +
"\vnum_workers\x18\x05 \x01(\x05R\n" +
"numWorkers\x12\x1a\n" +
"\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x120\n" +
"\x05users\x18\x05 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x12\x10\n" +
"\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1a\n" +
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" +
"\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" +
@@ -315,15 +315,17 @@ var file_proxy_wireguard_config_proto_goTypes = []any{
(DeviceConfig_DomainStrategy)(0), // 0: xray.proxy.wireguard.DeviceConfig.DomainStrategy
(*PeerConfig)(nil), // 1: xray.proxy.wireguard.PeerConfig
(*DeviceConfig)(nil), // 2: xray.proxy.wireguard.DeviceConfig
(*protocol.User)(nil), // 3: xray.common.protocol.User
}
var file_proxy_wireguard_config_proto_depIdxs = []int32{
1, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig
0, // 1: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
3, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User
0, // 2: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proxy_wireguard_config_proto_init() }
+5 -2
View File
@@ -6,11 +6,13 @@ option go_package = "github.com/xtls/xray-core/proxy/wireguard";
option java_package = "com.xray.proxy.wireguard";
option java_multiple_files = true;
import "common/protocol/user.proto";
message PeerConfig {
string public_key = 1;
string pre_shared_key = 2;
string endpoint = 3;
uint32 keep_alive = 4;
string keep_alive = 4;
repeated string allowed_ips = 5;
}
@@ -25,8 +27,9 @@ message DeviceConfig {
string secret_key = 1;
repeated string endpoint = 2;
repeated PeerConfig peers = 3;
repeated xray.common.protocol.User users = 5;
int32 mtu = 4;
int32 num_workers = 5;
bytes reserved = 6;
DomainStrategy domain_strategy = 7;
bool is_client = 8;
-226
View File
@@ -1,226 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2022 WireGuard LLC. All Rights Reserved.
*/
package gvisortun
import (
"context"
"fmt"
"net/netip"
"os"
"sync"
"syscall"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
type netTun struct {
ep *channel.Endpoint
stack *stack.Stack
events chan tun.Event
notifyHandle *channel.NotificationHandle
incomingPacket chan *buffer.View
mtu int
hasV4, hasV6 bool
closeOnce sync.Once
}
type Net netTun
func CreateNetTUN(localAddresses []netip.Addr, mtu int, promiscuousMode bool) (tun.Device, *Net, *stack.Stack, error) {
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
HandleLocal: !promiscuousMode,
}
dev := &netTun{
ep: channel.New(1024, uint32(mtu), ""),
stack: stack.New(opts),
events: make(chan tun.Event, 10),
incomingPacket: make(chan *buffer.View),
mtu: mtu,
}
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default
tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt)
if tcpipErr != nil {
return nil, nil, dev.stack, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
dev.notifyHandle = dev.ep.AddNotify(dev)
tcpipErr = dev.stack.CreateNIC(1, dev.ep)
if tcpipErr != nil {
return nil, nil, dev.stack, fmt.Errorf("CreateNIC: %v", tcpipErr)
}
for _, ip := range localAddresses {
var protoNumber tcpip.NetworkProtocolNumber
if ip.Is4() {
protoNumber = ipv4.ProtocolNumber
} else if ip.Is6() {
protoNumber = ipv6.ProtocolNumber
}
protoAddr := tcpip.ProtocolAddress{
Protocol: protoNumber,
AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
}
tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{})
if tcpipErr != nil {
return nil, nil, dev.stack, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr)
}
if ip.Is4() {
dev.hasV4 = true
} else if ip.Is6() {
dev.hasV6 = true
}
}
if dev.hasV4 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
}
if dev.hasV6 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
}
if promiscuousMode {
// enable promiscuous mode to handle all packets processed by netstack
dev.stack.SetPromiscuousMode(1, true)
dev.stack.SetSpoofing(1, true)
}
dev.events <- tun.EventUp
return dev, (*Net)(dev), dev.stack, nil
}
// Name implements tun.Device
func (tun *netTun) Name() (string, error) {
return "go", nil
}
// File implements tun.Device
func (tun *netTun) File() *os.File {
return nil
}
// Events implements tun.Device
func (tun *netTun) Events() <-chan tun.Event {
return tun.events
}
// Read implements tun.Device
func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
view, ok := <-tun.incomingPacket
if !ok {
return 0, os.ErrClosed
}
n, err := view.Read(buf[0][offset:])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// Write implements tun.Device
func (tun *netTun) Write(buf [][]byte, offset int) (int, error) {
for _, buf := range buf {
packet := buf[offset:]
if len(packet) == 0 {
continue
}
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
switch packet[0] >> 4 {
case 4:
tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
case 6:
tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
default:
return 0, syscall.EAFNOSUPPORT
}
}
return len(buf), nil
}
// WriteNotify implements channel.Notification
func (tun *netTun) WriteNotify() {
pkt := tun.ep.Read()
if pkt == nil {
return
}
view := pkt.ToView()
pkt.DecRef()
tun.incomingPacket <- view
}
// Close implements tun.Device
func (tun *netTun) Close() error {
tun.closeOnce.Do(func() {
tun.stack.RemoveNIC(1)
tun.stack.Close()
tun.ep.RemoveNotify(tun.notifyHandle)
tun.ep.Close()
close(tun.events)
close(tun.incomingPacket)
})
return nil
}
// MTU implements tun.Device
func (tun *netTun) MTU() (int, error) {
return tun.mtu, nil
}
// BatchSize implements tun.Device
func (tun *netTun) BatchSize() int {
return 1
}
func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) {
var protoNumber tcpip.NetworkProtocolNumber
if endpoint.Addr().Is4() {
protoNumber = ipv4.ProtocolNumber
} else {
protoNumber = ipv6.ProtocolNumber
}
return tcpip.FullAddress{
NIC: 1,
Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()),
Port: endpoint.Port(),
}, protoNumber
}
func (net *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) {
fa, pn := convertToFullAddr(addr)
return gonet.DialContextTCP(ctx, net.stack, fa, pn)
}
func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, error) {
var lfa, rfa *tcpip.FullAddress
var pn tcpip.NetworkProtocolNumber
if laddr.IsValid() || laddr.Port() > 0 {
var addr tcpip.FullAddress
addr, pn = convertToFullAddr(laddr)
lfa = &addr
}
if raddr.IsValid() || raddr.Port() > 0 {
var addr tcpip.FullAddress
addr, pn = convertToFullAddr(raddr)
rfa = &addr
rfa = nil // do not ep connect
}
return gonet.DialUDP(net.stack, lfa, rfa, pn)
}
+690
View File
@@ -0,0 +1,690 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package wireguard
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"os"
"strings"
"syscall"
"time"
"github.com/xtls/xray-core/transport/internet"
"golang.zx2c4.com/wireguard/tun"
"golang.org/x/net/dns/dnsmessage"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
type netTun struct {
ep *channel.Endpoint
stack *stack.Stack
events chan tun.Event
notifyHandle *channel.NotificationHandle
incomingPacket chan *buffer.View
mtu int
dnsServers []netip.Addr
hasV4, hasV6 bool
}
func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handleLocal bool) (tun.Device, *Net, *stack.Stack, error) {
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
HandleLocal: handleLocal,
}
dev := &netTun{
ep: channel.New(1024, uint32(mtu), ""),
stack: stack.New(opts),
events: make(chan tun.Event, 10),
incomingPacket: make(chan *buffer.View),
dnsServers: dnsServers,
mtu: mtu,
}
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default
tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt)
if tcpipErr != nil {
return nil, nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
dev.notifyHandle = dev.ep.AddNotify(dev)
tcpipErr = dev.stack.CreateNIC(1, dev.ep)
if tcpipErr != nil {
return nil, nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr)
}
for _, ip := range localAddresses {
var protoNumber tcpip.NetworkProtocolNumber
if ip.Is4() {
protoNumber = ipv4.ProtocolNumber
} else if ip.Is6() {
protoNumber = ipv6.ProtocolNumber
}
protoAddr := tcpip.ProtocolAddress{
Protocol: protoNumber,
AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
}
tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{})
if tcpipErr != nil {
return nil, nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr)
}
if ip.Is4() {
dev.hasV4 = true
} else if ip.Is6() {
dev.hasV6 = true
}
}
if dev.hasV4 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
}
if dev.hasV6 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
}
tnet := &Net{
DialContextTCPAddrPort: dev.DialContextTCPAddrPort,
DialUDPAddrPort: dev.DialUDPAddrPort,
dnsServers: dev.dnsServers,
hasV4: dev.hasV4,
hasV6: dev.hasV6,
}
dev.events <- tun.EventUp
return dev, tnet, dev.stack, nil
}
func (tun *netTun) Name() (string, error) {
return "go", nil
}
func (tun *netTun) File() *os.File {
return nil
}
func (tun *netTun) Events() <-chan tun.Event {
return tun.events
}
func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
view, ok := <-tun.incomingPacket
if !ok {
return 0, os.ErrClosed
}
n, err := view.Read(buf[0][offset:])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (tun *netTun) Write(buf [][]byte, offset int) (int, error) {
for _, buf := range buf {
packet := buf[offset:]
if len(packet) == 0 {
continue
}
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
switch packet[0] >> 4 {
case 4:
tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
case 6:
tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
default:
return 0, syscall.EAFNOSUPPORT
}
}
return len(buf), nil
}
func (tun *netTun) WriteNotify() {
pkt := tun.ep.Read()
if pkt == nil {
return
}
view := pkt.ToView()
pkt.DecRef()
tun.incomingPacket <- view
}
func (tun *netTun) Close() error {
tun.stack.RemoveNIC(1)
tun.stack.Close()
tun.ep.RemoveNotify(tun.notifyHandle)
tun.ep.Close()
if tun.events != nil {
close(tun.events)
}
if tun.incomingPacket != nil {
close(tun.incomingPacket)
}
return nil
}
func (tun *netTun) MTU() (int, error) {
return tun.mtu, nil
}
func (tun *netTun) BatchSize() int {
return 1
}
func (tun *netTun) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) {
fa, pn := convertToFullAddr(addr)
return gonet.DialContextTCP(ctx, tun.stack, fa, pn)
}
func (tun *netTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) {
var pn tcpip.NetworkProtocolNumber = ipv6.ProtocolNumber
if raddr.IsValid() || raddr.Port() > 0 {
_, pn = convertToFullAddr(raddr)
}
conn, err := gonet.DialUDP(tun.stack, nil, nil, pn)
if err != nil {
return nil, err
}
return &internet.PacketConnWrapper{
PacketConn: conn,
Dest: net.UDPAddrFromAddrPort(raddr),
}, nil
}
type Net struct {
DialContextTCPAddrPort func(ctx context.Context, addr netip.AddrPort) (net.Conn, error)
DialUDPAddrPort func(laddr, raddr netip.AddrPort) (net.Conn, error)
dnsServers []netip.Addr
hasV4, hasV6 bool
}
func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) {
var protoNumber tcpip.NetworkProtocolNumber
if endpoint.Addr().Is4() {
protoNumber = ipv4.ProtocolNumber
} else {
protoNumber = ipv6.ProtocolNumber
}
return tcpip.FullAddress{
NIC: 1,
Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()),
Port: endpoint.Port(),
}, protoNumber
}
var (
errNoSuchHost = errors.New("no such host")
errLameReferral = errors.New("lame referral")
errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message")
errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message")
errServerMisbehaving = errors.New("server misbehaving")
errInvalidDNSResponse = errors.New("invalid DNS response")
errNoAnswerFromDNSServer = errors.New("no answer from DNS server")
errServerTemporarilyMisbehaving = errors.New("server misbehaving")
errCanceled = errors.New("operation was canceled")
errTimeout = errors.New("i/o timeout")
)
func (net *Net) LookupHost(host string) (addrs []string, err error) {
return net.LookupContextHost(context.Background(), host)
}
func isDomainName(s string) bool {
l := len(s)
if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
return false
}
last := byte('.')
nonNumeric := false
partlen := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch {
default:
return false
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
nonNumeric = true
partlen++
case '0' <= c && c <= '9':
partlen++
case c == '-':
if last == '.' {
return false
}
partlen++
nonNumeric = true
case c == '.':
if last == '.' || last == '-' {
return false
}
if partlen > 63 || partlen == 0 {
return false
}
partlen = 0
}
last = c
}
if last == '-' || partlen > 63 {
return false
}
return nonNumeric
}
func randU16() uint16 {
var b [2]byte
_, err := rand.Read(b[:])
if err != nil {
panic(err)
}
return binary.LittleEndian.Uint16(b[:])
}
func newRequest(q dnsmessage.Question) (id uint16, udpReq, tcpReq []byte, err error) {
id = randU16()
b := dnsmessage.NewBuilder(make([]byte, 2, 514), dnsmessage.Header{ID: id, RecursionDesired: true})
b.EnableCompression()
if err := b.StartQuestions(); err != nil {
return 0, nil, nil, err
}
if err := b.Question(q); err != nil {
return 0, nil, nil, err
}
tcpReq, err = b.Finish()
udpReq = tcpReq[2:]
l := len(tcpReq) - 2
tcpReq[0] = byte(l >> 8)
tcpReq[1] = byte(l)
return id, udpReq, tcpReq, err
}
func equalASCIIName(x, y dnsmessage.Name) bool {
if x.Length != y.Length {
return false
}
for i := 0; i < int(x.Length); i++ {
a := x.Data[i]
b := y.Data[i]
if 'A' <= a && a <= 'Z' {
a += 0x20
}
if 'A' <= b && b <= 'Z' {
b += 0x20
}
if a != b {
return false
}
}
return true
}
func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage.Header, respQues dnsmessage.Question) bool {
if !respHdr.Response {
return false
}
if reqID != respHdr.ID {
return false
}
if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(reqQues.Name, respQues.Name) {
return false
}
return true
}
func dnsPacketRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) {
if _, err := c.Write(b); err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
b = make([]byte, 512)
for {
n, err := c.Read(b)
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
var p dnsmessage.Parser
h, err := p.Start(b[:n])
if err != nil {
continue
}
q, err := p.Question()
if err != nil || !checkResponse(id, query, h, q) {
continue
}
return p, h, nil
}
}
func dnsStreamRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) {
if _, err := c.Write(b); err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
b = make([]byte, 1280)
if _, err := io.ReadFull(c, b[:2]); err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
l := int(b[0])<<8 | int(b[1])
if l > len(b) {
b = make([]byte, l)
}
n, err := io.ReadFull(c, b[:l])
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
var p dnsmessage.Parser
h, err := p.Start(b[:n])
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage
}
q, err := p.Question()
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage
}
if !checkResponse(id, query, h, q) {
return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse
}
return p, h, nil
}
func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) {
q.Class = dnsmessage.ClassINET
id, udpReq, tcpReq, err := newRequest(q)
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotMarshalDNSMessage
}
for _, useUDP := range []bool{true, false} {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
defer cancel()
var c net.Conn
var err error
if useUDP {
c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, netip.AddrPortFrom(server, 53))
} else {
c, err = tnet.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(server, 53))
}
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
if d, ok := ctx.Deadline(); ok && !d.IsZero() {
err := c.SetDeadline(d)
if err != nil {
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
}
var p dnsmessage.Parser
var h dnsmessage.Header
if useUDP {
p, h, err = dnsPacketRoundTrip(c, id, q, udpReq)
} else {
p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq)
}
c.Close()
if err != nil {
if err == context.Canceled {
err = errCanceled
} else if err == context.DeadlineExceeded {
err = errTimeout
}
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
if err := p.SkipQuestion(); err != dnsmessage.ErrSectionDone {
return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse
}
if h.Truncated {
continue
}
return p, h, nil
}
return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer
}
func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error {
if h.RCode == dnsmessage.RCodeNameError {
return errNoSuchHost
}
_, err := p.AnswerHeader()
if err != nil && err != dnsmessage.ErrSectionDone {
return errCannotUnmarshalDNSMessage
}
if h.RCode == dnsmessage.RCodeSuccess && !h.Authoritative && !h.RecursionAvailable && err == dnsmessage.ErrSectionDone {
return errLameReferral
}
if h.RCode != dnsmessage.RCodeSuccess && h.RCode != dnsmessage.RCodeNameError {
if h.RCode == dnsmessage.RCodeServerFailure {
return errServerTemporarilyMisbehaving
}
return errServerMisbehaving
}
return nil
}
func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error {
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
return errNoSuchHost
}
if err != nil {
return errCannotUnmarshalDNSMessage
}
if h.Type == qtype {
return nil
}
if err := p.SkipAnswer(); err != nil {
return errCannotUnmarshalDNSMessage
}
}
}
func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) {
var lastErr error
n, err := dnsmessage.NewName(name)
if err != nil {
return dnsmessage.Parser{}, "", errCannotMarshalDNSMessage
}
q := dnsmessage.Question{
Name: n,
Type: qtype,
Class: dnsmessage.ClassINET,
}
for i := 0; i < 2; i++ {
for _, server := range tnet.dnsServers {
p, h, err := tnet.exchange(ctx, server, q, time.Second*5)
if err != nil {
dnsErr := &net.DNSError{
Err: err.Error(),
Name: name,
Server: server.String(),
}
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
dnsErr.IsTimeout = true
}
if _, ok := err.(*net.OpError); ok {
dnsErr.IsTemporary = true
}
lastErr = dnsErr
continue
}
if err := checkHeader(&p, h); err != nil {
dnsErr := &net.DNSError{
Err: err.Error(),
Name: name,
Server: server.String(),
}
if err == errServerTemporarilyMisbehaving {
dnsErr.IsTemporary = true
}
if err == errNoSuchHost {
dnsErr.IsNotFound = true
return p, server.String(), dnsErr
}
lastErr = dnsErr
continue
}
err = skipToAnswer(&p, qtype)
if err == nil {
return p, server.String(), nil
}
lastErr = &net.DNSError{
Err: err.Error(),
Name: name,
Server: server.String(),
}
if err == errNoSuchHost {
lastErr.(*net.DNSError).IsNotFound = true
return p, server.String(), lastErr
}
}
}
return dnsmessage.Parser{}, "", lastErr
}
func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) {
if host == "" || (!tnet.hasV6 && !tnet.hasV4) {
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
zlen := len(host)
if strings.IndexByte(host, ':') != -1 {
if zidx := strings.LastIndexByte(host, '%'); zidx != -1 {
zlen = zidx
}
}
if ip, err := netip.ParseAddr(host[:zlen]); err == nil {
return []string{ip.String()}, nil
}
if !isDomainName(host) {
return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
type result struct {
p dnsmessage.Parser
server string
error
}
var addrsV4, addrsV6 []netip.Addr
lanes := 0
if tnet.hasV4 {
lanes++
}
if tnet.hasV6 {
lanes++
}
lane := make(chan result, lanes)
var lastErr error
if tnet.hasV4 {
go func() {
p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeA)
lane <- result{p, server, err}
}()
}
if tnet.hasV6 {
go func() {
p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeAAAA)
lane <- result{p, server, err}
}()
}
for l := 0; l < lanes; l++ {
result := <-lane
if result.error != nil {
if lastErr == nil {
lastErr = result.error
}
continue
}
loop:
for {
h, err := result.p.AnswerHeader()
if err != nil && err != dnsmessage.ErrSectionDone {
lastErr = &net.DNSError{
Err: errCannotMarshalDNSMessage.Error(),
Name: host,
Server: result.server,
}
}
if err != nil {
break
}
switch h.Type {
case dnsmessage.TypeA:
a, err := result.p.AResource()
if err != nil {
lastErr = &net.DNSError{
Err: errCannotMarshalDNSMessage.Error(),
Name: host,
Server: result.server,
}
break loop
}
addrsV4 = append(addrsV4, netip.AddrFrom4(a.A))
case dnsmessage.TypeAAAA:
aaaa, err := result.p.AAAAResource()
if err != nil {
lastErr = &net.DNSError{
Err: errCannotMarshalDNSMessage.Error(),
Name: host,
Server: result.server,
}
break loop
}
addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA))
default:
if err := result.p.SkipAnswer(); err != nil {
lastErr = &net.DNSError{
Err: errCannotMarshalDNSMessage.Error(),
Name: host,
Server: result.server,
}
break loop
}
continue
}
}
}
// We don't do RFC6724. Instead just put V6 addresses first if an IPv6 address is enabled
var addrs []netip.Addr
if tnet.hasV6 {
addrs = append(addrsV6, addrsV4...)
} else {
addrs = append(addrsV4, addrsV6...)
}
if len(addrs) == 0 && lastErr != nil {
return nil, lastErr
}
saddrs := make([]string, 0, len(addrs))
for _, ip := range addrs {
saddrs = append(saddrs, ip.String())
}
return saddrs, nil
}
+340 -112
View File
@@ -2,170 +2,398 @@ package wireguard
import (
"context"
"encoding/hex"
"fmt"
"net/netip"
"reflect"
"strings"
"sync"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
c "github.com/xtls/xray-core/common/ctx"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"golang.org/x/crypto/curve25519"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
var nullDestination = net.TCPDestination(net.AnyIP, 0)
type Server struct {
bindServer *netBindServer
info routingInfo
conf *DeviceConfig
ctx context.Context
policyManager policy.Manager
}
dispatcher routing.Dispatcher
type routingInfo struct {
ctx context.Context
dispatcher routing.Dispatcher
inboundTag *session.Inbound
contentTag *session.Content
tag string
src net.Destination
sniffingRequest session.SniffingRequest
streamSettings *internet.MemoryStreamConfig
uplinkCounter stats.Counter
downlinkCounter stats.Counter
tun tun.Device
stack *stack.Stack
dev *device.Device
mu sync.Mutex
pub [32]byte
users *sync.Map
}
func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
v := core.MustFromContext(ctx)
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
d := v.GetFeature(routing.DispatcherType()).(routing.Dispatcher)
endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf)
inbound := session.InboundFromContext(ctx)
content := session.ContentFromContext(ctx)
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
tag := inbound.Tag
var uplinkCounter stats.Counter
var downlinkCounter stats.Counter
if len(tag) > 0 && p.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
}
if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
}
localAddresses := make([]netip.Addr, 0, len(conf.Endpoint))
for _, localaddress := range conf.Endpoint {
addr, err := netip.ParseAddr(localaddress)
if err == nil {
localAddresses = append(localAddresses, addr)
continue
}
prefix, err := netip.ParsePrefix(localaddress)
if err == nil {
localAddresses = append(localAddresses, prefix.Addr())
continue
}
return nil, err
}
tun, _, stack, err := CreateNetTUN(localAddresses, nil, int(conf.Mtu), false)
if err != nil {
return nil, err
}
server := &Server{
bindServer: &netBindServer{
netBind: netBind{
dns: v.GetFeature(dns.ClientType()).(dns.Client),
dnsOption: dns.IPOption{
IPv4Enable: hasIPv4,
IPv6Enable: hasIPv6,
},
workers: int(conf.NumWorkers),
readQueue: make(chan *netReadInfo),
},
},
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
pri := common.Must2(ParseKey(conf.SecretKey))
var pub [32]byte
curve25519.ScalarBaseMult(&pub, pri)
users := &sync.Map{}
for _, u := range conf.Users {
user, err := u.ToMemoryUser()
if err != nil {
return nil, err
}
users.Store(user.Account.(*MemoryAccount).Pub, user)
}
tun, err := conf.createTun()(endpoints, int(conf.Mtu), server.forwardConnection)
if err != nil {
return nil, err
}
return &Server{
conf: conf,
ctx: core.ToBackgroundDetachedContext(ctx),
policyManager: p,
dispatcher: d,
if err = tun.BuildDevice(createIPCRequest(conf), server.bindServer); err != nil {
_ = tun.Close()
return nil, err
}
tag: inbound.Tag,
src: inbound.Source,
sniffingRequest: content.SniffingRequest,
streamSettings: streamSettings,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
return server, nil
tun: tun,
stack: stack,
pub: pub,
users: users,
}, nil
}
// Network implements proxy.Inbound.
func (*Server) Network() []net.Network {
return []net.Network{net.Network_UDP}
}
// Process implements proxy.Inbound.
func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error {
s.info = routingInfo{
ctx: ctx,
dispatcher: dispatcher,
inboundTag: session.InboundFromContext(ctx),
contentTag: session.ContentFromContext(ctx),
func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.dev == nil {
return errors.New("too early")
}
ep, err := s.bindServer.ParseEndpoint(conn.RemoteAddr().String())
peer := user.Account.(*MemoryAccount)
if peer.Pub == s.pub {
return errors.New("invalid public key")
}
var sb strings.Builder
sb.WriteString("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\n")
sb.WriteString("replace_allowed_ips=true\n")
for i := range peer.AllowedIPs {
sb.WriteString("allowed_ip=" + peer.AllowedIPs[i].String() + "\n")
}
if peer.PreSharedKey != "" {
sb.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
}
if peer.KeepAlive != "" {
sb.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
}
err := s.dev.IpcSet(sb.String())
if err != nil {
return err
}
nep := ep.(*netEndpoint)
nep.conn = conn
reader := buf.NewPacketReader(conn)
for {
mb, err := reader.ReadMultiBuffer()
if err != nil {
nep.conn = nil
buf.ReleaseMulti(mb)
return err
}
for i, b := range mb {
rawBytes := b.Bytes()
if b.Len() > 3 {
rawBytes[1] = 0
rawBytes[2] = 0
rawBytes[3] = 0
}
select {
case s.bindServer.readQueue <- &netReadInfo{
buff: b,
endpoint: nep,
}:
case <-s.bindServer.closedCh:
nep.conn = nil
buf.ReleaseMulti(mb[i:])
return errors.New("bind closed")
}
}
}
s.users.Store(peer.Pub, user)
return nil
}
func (s *Server) forwardConnection(dest net.Destination, conn net.Conn) {
if s.info.dispatcher == nil {
errors.LogError(s.info.ctx, "unexpected: dispatcher == nil")
func (s *Server) RemoveUser(ctx context.Context, email string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.dev == nil {
return errors.New("too early")
}
if user := s.GetUser(ctx, email); user != nil {
peer := user.Account.(*MemoryAccount)
err := s.dev.IpcSet("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\nremove=true\n")
if err != nil {
return err
}
s.users.Delete(peer.Pub)
}
return nil
}
func (s *Server) GetUser(ctx context.Context, email string) (user *protocol.MemoryUser) {
s.users.Range(func(key, value any) bool {
if value.(*protocol.MemoryUser).Email == email {
user = value.(*protocol.MemoryUser)
return false
}
return true
})
return
}
func (s *Server) GetUserByAddr(ctx context.Context, addr netip.Addr) (user *protocol.MemoryUser) {
s.users.Range(func(key, value any) bool {
peer := value.(*protocol.MemoryUser).Account.(*MemoryAccount)
for i := range peer.AllowedIPs {
if peer.AllowedIPs[i].Contains(addr) {
user = value.(*protocol.MemoryUser)
return false
}
}
return true
})
return
}
func (s *Server) GetUsers(ctx context.Context) (users []*protocol.MemoryUser) {
s.users.Range(func(key, value interface{}) bool {
users = append(users, value.(*protocol.MemoryUser))
return true
})
return
}
func (s *Server) GetUsersCount(context.Context) (count int64) {
s.users.Range(func(key, value interface{}) bool {
count++
return true
})
return
}
// Network implements proxy.Inbound.Network.
func (*Server) Network() []net.Network {
return []net.Network{}
}
// Process implements proxy.Inbound.Process.
func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error {
return nil
}
// Close implements common.Closable.Close.
func (s *Server) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.dev != nil {
s.dev.Close()
s.dev = nil
s.tun = nil
} else if s.tun != nil {
s.tun.Close()
s.tun = nil
}
return nil
}
// Start implements common.Runnable.Start.
func (s *Server) Start() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.dev != nil {
return nil
}
if s.src.Address.Family().IsDomain() {
return errors.New("address is domain")
}
listenFunc := func() (net.PacketConn, error) {
pktConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: s.src.Address.IP(), Port: int(s.src.Port)}, s.streamSettings.SocketSettings)
if err != nil {
return nil, err
}
if s.streamSettings.UdpmaskManager != nil {
newConn, err := s.streamSettings.UdpmaskManager.WrapPacketConnServer(pktConn)
if err != nil {
pktConn.Close()
return nil, errors.New("mask err").Base(err)
}
pktConn = newConn
}
if s.uplinkCounter != nil || s.downlinkCounter != nil {
pktConn = &PacketCounterConnection{
PacketConn: pktConn,
ReadCounter: s.uplinkCounter,
WriteCounter: s.downlinkCounter,
}
}
return pktConn, nil
}
bind := &bind{
listenFunc: listenFunc,
}
logger := &device.Logger{
Verbosef: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Debug,
Content: fmt.Sprintf(format, args...),
})
},
Errorf: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Error,
Content: fmt.Sprintf(format, args...),
})
},
}
dev := device.NewDevice(s.tun, bind, logger)
var cfg strings.Builder
cfg.WriteString("private_key=" + s.conf.SecretKey + "\n")
s.users.Range(func(key, value any) bool {
peer := value.(*protocol.MemoryUser).Account.(*MemoryAccount)
cfg.WriteString("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\n")
for i := range peer.AllowedIPs {
cfg.WriteString("allowed_ip=" + peer.AllowedIPs[i].String() + "\n")
}
if peer.PreSharedKey != "" {
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
}
if peer.KeepAlive != "" {
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
}
return true
})
err := dev.IpcSet(cfg.String())
if err != nil {
return err
}
err = dev.Up()
if err != nil {
return err
}
s.dev = dev
createForwarder(s.stack, s.HandleConnection)
return nil
}
func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
defer conn.Close()
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
ctx = c.ContextWithID(ctx, session.NewID())
remote := conn.RemoteAddr()
if remote == nil {
errors.LogError(context.Background(), "nil remote")
return
}
ctx, cancel := context.WithCancel(core.ToBackgroundDetachedContext(s.info.ctx))
sid := session.NewID()
ctx = c.ContextWithID(ctx, sid)
inbound := session.Inbound{} // since promiscuousModeHandler mixed-up context, we shallow copy inbound (tag) and content (configs)
if s.info.inboundTag != nil {
inbound = *s.info.inboundTag
var addr netip.Addr
switch v := remote.(type) {
case *net.TCPAddr:
addr, _ = netip.AddrFromSlice(v.IP)
case *net.UDPAddr:
addr, _ = netip.AddrFromSlice(v.IP)
default:
errors.LogError(context.Background(), "invalid addr type ", reflect.TypeOf(v))
return
}
inbound.Name = "wireguard"
inbound.CanSpliceCopy = 3
// overwrite the source to use the tun address for each sub context.
// Since gvisor.ForwarderRequest doesn't provide any info to associate the sub-context with the Parent context
// Currently we have no way to link to the original source address
inbound.Source = net.DestinationFromAddr(conn.RemoteAddr())
ctx = session.ContextWithInbound(ctx, &inbound)
content := new(session.Content)
if s.info.contentTag != nil {
content.SniffingRequest = s.info.contentTag.SniffingRequest
user := s.GetUserByAddr(context.TODO(), addr)
if user == nil {
errors.LogError(context.Background(), "nil user form ", remote, " to ", dest)
return
}
ctx = session.ContextWithContent(ctx, content)
source := net.DestinationFromAddr(remote)
inbound := session.Inbound{
Name: "wireguard",
Tag: s.tag,
CanSpliceCopy: 3,
Source: source,
User: user,
}
ctx = session.ContextWithInbound(ctx, &inbound)
ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: s.sniffingRequest,
})
ctx = session.SubContextFromMuxInbound(ctx)
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
From: nullDestination,
From: inbound.Source,
To: dest,
Status: log.AccessAccepted,
Reason: "",
})
errors.LogInfo(ctx, "processing from ", source, " to ", dest)
err := s.info.dispatcher.DispatchLink(ctx, dest, &transport.Link{
Reader: buf.NewReader(conn),
link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: buf.NewWriter(conn),
})
if err != nil {
errors.LogInfoInner(ctx, err, "connection ends")
}
cancel()
conn.Close()
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err))
}
}
func ParseKey(str string) (*[32]byte, error) {
slice, err := hex.DecodeString(str)
if err != nil {
return nil, err
}
if len(slice) != 32 {
return nil, errors.New("len(slice) != 32")
}
return (*[32]byte)(slice), nil
}
-53
View File
@@ -1,53 +0,0 @@
package wireguard_test
import (
"context"
"runtime/debug"
"testing"
"github.com/stretchr/testify/assert"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/wireguard"
)
// TestWireGuardServerInitializationError verifies that an error during TUN initialization
// (triggered by an empty SecretKey) in the WireGuard server does not cause a panic and returns an error instead.
func TestWireGuardServerInitializationError(t *testing.T) {
// Create a minimal core instance with default features
config := &core.Config{}
instance, err := core.New(config)
if err != nil {
t.Fatalf("Failed to create core instance: %v", err)
}
// Set the Xray instance in the context
ctx := context.WithValue(context.Background(), core.XrayKey(1), instance)
// Define the server configuration with an empty SecretKey to trigger error
conf := &wireguard.DeviceConfig{
IsClient: false,
Endpoint: []string{"10.0.0.1/32"},
Mtu: 1420,
SecretKey: "", // Empty SecretKey to trigger error
Peers: []*wireguard.PeerConfig{
{
PublicKey: "some_public_key",
AllowedIps: []string{"10.0.0.2/32"},
},
},
}
// Use defer to catch any panic and fail the test explicitly
defer func() {
if r := recover(); r != nil {
t.Errorf("TUN initialization panicked: %v", r)
debug.PrintStack()
}
}()
// Attempt to initialize the WireGuard server
_, err = wireguard.NewServer(ctx, conf)
// Check that an error is returned
assert.ErrorContains(t, err, "failed to set private_key: hex string does not fit the slice")
}
+68 -164
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"net/netip"
"runtime"
"strconv"
"strings"
@@ -13,9 +12,7 @@ import (
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/proxy/wireguard/gvisortun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
@@ -25,77 +22,8 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
)
type tunCreator func(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error)
type promiscuousModeHandler func(dest net.Destination, conn net.Conn)
type Tunnel interface {
BuildDevice(ipc string, bind conn.Bind) error
DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error)
DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error)
Close() error
}
type tunnel struct {
tun tun.Device
device *device.Device
rw sync.Mutex
}
func (t *tunnel) BuildDevice(ipc string, bind conn.Bind) (err error) {
t.rw.Lock()
defer t.rw.Unlock()
if t.device != nil {
return errors.New("device is already initialized")
}
logger := &device.Logger{
Verbosef: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Debug,
Content: fmt.Sprintf(format, args...),
})
},
Errorf: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Error,
Content: fmt.Sprintf(format, args...),
})
},
}
t.device = device.NewDevice(t.tun, bind, logger)
if err = t.device.IpcSet(ipc); err != nil {
return err
}
if err = t.device.Up(); err != nil {
return err
}
return nil
}
func (t *tunnel) Close() (err error) {
t.rw.Lock()
defer t.rw.Unlock()
if t.device == nil {
return nil
}
t.device.Close()
t.device = nil
err = t.tun.Close()
t.tun = nil
return nil
}
func CalculateInterfaceName(name string) (tunName string) {
if runtime.GOOS == "darwin" {
tunName = "utun"
@@ -121,93 +49,61 @@ func CalculateInterfaceName(name string) (tunName string) {
return
}
var _ Tunnel = (*gvisorNet)(nil)
func createForwarder(gstack *stack.Stack, handler func(conn net.Conn, dest net.Destination)) {
gstack.SetPromiscuousMode(1, true)
gstack.SetSpoofing(1, true)
type gvisorNet struct {
tunnel
net *gvisortun.Net
}
tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) {
go func(r *tcp.ForwarderRequest) {
var wq waiter.Queue
id := r.ID()
func (g *gvisorNet) Close() error {
return g.tunnel.Close()
}
func (g *gvisorNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (
net.Conn, error,
) {
return g.net.DialContextTCPAddrPort(ctx, addr)
}
func (g *gvisorNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) {
return g.net.DialUDPAddrPort(laddr, raddr)
}
func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error) {
out := &gvisorNet{}
tun, n, gstack, err := gvisortun.CreateNetTUN(localAddresses, mtu, handler != nil)
if err != nil {
return nil, err
}
if handler != nil {
// handler is only used for promiscuous mode
// capture all packets and send to handler
tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) {
go func(r *tcp.ForwarderRequest) {
var wq waiter.Queue
id := r.ID()
ep, err := r.CreateEndpoint(&wq)
if err != nil {
errors.LogError(context.Background(), err.String())
r.Complete(true)
return
}
options := ep.SocketOptions()
options.SetKeepAlive(false)
options.SetReuseAddress(true)
options.SetReusePort(true)
handler(net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), gonet.NewTCPConn(&wq, ep))
ep.Close()
r.Complete(false)
}(r)
})
gstack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
manager := &udpManager{
stack: gstack,
handler: handler,
m: make(map[string]*udpConn),
}
gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
data := pkt.Clone().Data().AsRange().ToSlice()
// if len(data) == 0 {
// return false
// }
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
if srcIP == nil || dstIP == nil {
panic(id)
ep, err := r.CreateEndpoint(&wq)
if err != nil {
errors.LogError(context.Background(), err.String())
r.Complete(true)
return
}
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
manager.feed(src, dst, data)
return true
})
options := ep.SocketOptions()
options.SetKeepAlive(false)
options.SetReuseAddress(true)
options.SetReusePort(true)
handler(gonet.NewTCPConn(&wq, ep), net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)))
ep.Close()
r.Complete(false)
}(r)
})
gstack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
manager := &udpManager{
stack: gstack,
handler: handler,
m: make(map[string]*udpConn),
}
out.tun, out.net = tun, n
return out, nil
gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
data := pkt.Clone().Data().AsRange().ToSlice()
// if len(data) == 0 {
// return false
// }
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
if srcIP == nil || dstIP == nil {
panic(id)
}
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
manager.feed(src, dst, data)
return true
})
}
type udpManager struct {
stack *stack.Stack
handler func(dest net.Destination, conn net.Conn)
handler func(conn net.Conn, dest net.Destination)
m map[string]*udpConn
mutex sync.RWMutex
}
@@ -246,7 +142,7 @@ func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte)
m.mutex.Unlock()
}
m.m[src.NetAddr()] = uc
go m.handler(dst, uc)
go m.handler(uc, dst)
}
select {
@@ -345,19 +241,23 @@ type udpConn struct {
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
q, ok := <-c.queue
if !ok {
return nil, io.EOF
for {
q, ok := <-c.queue
if !ok {
return nil, io.EOF
}
b := buf.New()
if _, err := b.Write(q.p); err != nil {
errors.LogErrorInner(context.Background(), err, "drop packet to ", q.dest, " with size ", len(q.p))
b.Release()
continue
}
b.UDP = q.dest
return buf.MultiBuffer{b}, nil
}
b := buf.New()
if _, err := b.Write(q.p); err != nil {
return nil, err
}
b.UDP = q.dest
return buf.MultiBuffer{b}, nil
}
func (c *udpConn) Read(p []byte) (int, error) {
@@ -376,7 +276,11 @@ func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
for i, b := range mb {
dst := c.dst
if b.UDP != nil {
dst = *b.UDP
if b.UDP.Address.Family().IsDomain() {
errors.LogError(context.Background(), "impossible domain packet ", b.UDP, " reply via original target ", dst)
} else {
dst = *b.UDP
}
}
err := c.writeFunc(b.Bytes(), dst, c.src)
if err != nil {
+5 -3
View File
@@ -1,14 +1,16 @@
//go:build !linux || android
//go:build !linux
package wireguard
import (
"errors"
"net/netip"
"golang.zx2c4.com/wireguard/tun"
)
func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (t Tunnel, err error) {
return nil, errors.New("not implemented")
func createKernelTun([]netip.Addr, []netip.Addr, int) (tdev tun.Device, tnet *Net, err error) {
return nil, nil, errors.New("not implemented")
}
func KernelTunSupported() (bool, error) {
+108 -118
View File
@@ -1,4 +1,4 @@
//go:build linux && !android
//go:build linux
package wireguard
@@ -20,17 +20,6 @@ import (
"golang.zx2c4.com/wireguard/tun"
)
type deviceNet struct {
tunnel
dialer *net.Dialer
lc *net.ListenConfig
handle *netlink.Handle
linkAddrs []netlink.Addr
routes []*netlink.Route
rules []*netlink.Rule
}
var (
tableIndex int = 10230
mu sync.Mutex
@@ -48,82 +37,18 @@ func allocateIPv6TableIndex() int {
return currentIndex
}
func newDeviceNet(interfaceName string) *deviceNet {
dialer := &net.Dialer{}
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
if err := syscall.BindToDevice(int(fd), interfaceName); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to bind to device")
}
})
}
lc := &net.ListenConfig{}
lc.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
if err := syscall.BindToDevice(int(fd), interfaceName); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to bind to device")
}
})
}
return &deviceNet{dialer: dialer, lc: lc}
type kernelTun struct {
tun.Device
dialer *net.Dialer
lc *net.ListenConfig
handle *netlink.Handle
linkAddrs []netlink.Addr
routes []*netlink.Route
rules []*netlink.Rule
}
func (d *deviceNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (
net.Conn, error,
) {
return d.dialer.DialContext(ctx, "tcp", addr.String())
}
func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) {
var conn net.PacketConn
var err error
if raddr.Addr().Is4() {
conn, err = d.lc.ListenPacket(context.Background(), "udp", "0.0.0.0:0")
} else {
conn, err = d.lc.ListenPacket(context.Background(), "udp", "[::]:0")
}
if err != nil {
return nil, err
}
return &internet.PacketConnWrapper{
PacketConn: conn,
Dest: &net.UDPAddr{
IP: raddr.Addr().AsSlice(),
Port: int(raddr.Port()),
},
}, nil
}
func (d *deviceNet) Close() (err error) {
var errs []error
for _, rule := range d.rules {
if err = d.handle.RuleDel(rule); err != nil {
errs = append(errs, fmt.Errorf("failed to delete rule: %w", err))
}
}
for _, route := range d.routes {
if err = d.handle.RouteDel(route); err != nil {
errs = append(errs, fmt.Errorf("failed to delete route: %w", err))
}
}
if err = d.tunnel.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close tunnel: %w", err))
}
if d.handle != nil {
d.handle.Close()
d.handle = nil
}
if len(errs) == 0 {
return nil
}
return goerrors.Join(errs...)
}
func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (t Tunnel, err error) {
if handler != nil {
return nil, errors.New("TODO: support promiscuous mode")
}
func createKernelTun(localAddresses, dnsServers []netip.Addr, mtu int) (tdev tun.Device, tnet *Net, err error) {
var v4, v6 *netip.Addr
for _, prefixes := range localAddresses {
if v4 == nil && prefixes.Is4() {
@@ -150,22 +75,22 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
// system configs.
if v4 != nil {
if err = writeSysctlZero("/proc/sys/net/ipv4/conf/all/rp_filter"); err != nil {
return nil, fmt.Errorf("failed to disable ipv4 rp_filter for all: %w", err)
return nil, nil, fmt.Errorf("failed to disable ipv4 rp_filter for all: %w", err)
}
}
if v6 != nil {
if err = writeSysctlZero("/proc/sys/net/ipv6/conf/all/disable_ipv6"); err != nil {
return nil, fmt.Errorf("failed to enable ipv6: %w", err)
return nil, nil, fmt.Errorf("failed to enable ipv6: %w", err)
}
if err = writeSysctlZero("/proc/sys/net/ipv6/conf/all/rp_filter"); err != nil {
return nil, fmt.Errorf("failed to disable ipv6 rp_filter for all: %w", err)
return nil, nil, fmt.Errorf("failed to disable ipv6 rp_filter for all: %w", err)
}
}
n := CalculateInterfaceName("wg")
wgt, err := tun.CreateTUN(n, mtu)
if err != nil {
return nil, err
return nil, nil, err
}
defer func() {
if err != nil {
@@ -177,12 +102,12 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
// the operation require root privilege on container require '--privileged' flag.
if v4 != nil {
if err = writeSysctlZero("/proc/sys/net/ipv4/conf/" + n + "/rp_filter"); err != nil {
return nil, fmt.Errorf("failed to disable ipv4 rp_filter for tunnel: %w", err)
return nil, nil, fmt.Errorf("failed to disable ipv4 rp_filter for tunnel: %w", err)
}
}
if v6 != nil {
if err = writeSysctlZero("/proc/sys/net/ipv6/conf/" + n + "/rp_filter"); err != nil {
return nil, fmt.Errorf("failed to disable ipv6 rp_filter for tunnel: %w", err)
return nil, nil, fmt.Errorf("failed to disable ipv6 rp_filter for tunnel: %w", err)
}
}
@@ -196,25 +121,28 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
}
ipv6TableIndex--
if ipv6TableIndex < 0 {
return nil, fmt.Errorf("failed to find available ipv6 table index")
return nil, nil, fmt.Errorf("failed to find available ipv6 table index")
}
}
}
out := newDeviceNet(n)
out.handle, err = netlink.NewHandle()
t := &kernelTun{
Device: wgt,
}
t.handle, err = netlink.NewHandle()
if err != nil {
return nil, err
return nil, nil, err
}
defer func() {
if err != nil {
_ = out.Close()
t.Close()
}
}()
l, err := netlink.LinkByName(n)
if err != nil {
return nil, err
return nil, nil, err
}
if v4 != nil {
@@ -224,7 +152,7 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
Mask: net.CIDRMask(v4.BitLen(), v4.BitLen()),
},
}
out.linkAddrs = append(out.linkAddrs, addr)
t.linkAddrs = append(t.linkAddrs, addr)
}
if v6 != nil {
addr := netlink.Addr{
@@ -233,7 +161,7 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
Mask: net.CIDRMask(v6.BitLen(), v6.BitLen()),
},
}
out.linkAddrs = append(out.linkAddrs, addr)
t.linkAddrs = append(t.linkAddrs, addr)
rt := &netlink.Route{
LinkIndex: l.Attrs().Index,
@@ -243,40 +171,102 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
},
Table: ipv6TableIndex,
}
out.routes = append(out.routes, rt)
t.routes = append(t.routes, rt)
r := netlink.NewRule()
r.Table, r.Family, r.Src = ipv6TableIndex, unix.AF_INET6, addr.IPNet
out.rules = append(out.rules, r)
t.rules = append(t.rules, r)
r = netlink.NewRule()
r.Table, r.Family, r.OifName = ipv6TableIndex, unix.AF_INET6, n
out.rules = append(out.rules, r)
t.rules = append(t.rules, r)
}
for _, addr := range out.linkAddrs {
if err = out.handle.AddrAdd(l, &addr); err != nil {
return nil, fmt.Errorf("failed to add address %s to %s: %w", addr, n, err)
for _, addr := range t.linkAddrs {
if err = t.handle.AddrAdd(l, &addr); err != nil {
return nil, nil, fmt.Errorf("failed to add address %s to %s: %w", addr, n, err)
}
}
if err = out.handle.LinkSetMTU(l, mtu); err != nil {
return nil, err
if err = t.handle.LinkSetMTU(l, mtu); err != nil {
return nil, nil, err
}
if err = out.handle.LinkSetUp(l); err != nil {
return nil, err
if err = t.handle.LinkSetUp(l); err != nil {
return nil, nil, err
}
for _, route := range out.routes {
if err = out.handle.RouteAdd(route); err != nil {
return nil, fmt.Errorf("failed to add route %s: %w", route, err)
for _, route := range t.routes {
if err = t.handle.RouteAdd(route); err != nil {
return nil, nil, fmt.Errorf("failed to add route %s: %w", route, err)
}
}
for _, rule := range out.rules {
if err = out.handle.RuleAdd(rule); err != nil {
return nil, fmt.Errorf("failed to add rule %s: %w", rule, err)
for _, rule := range t.rules {
if err = t.handle.RuleAdd(rule); err != nil {
return nil, nil, fmt.Errorf("failed to add rule %s: %w", rule, err)
}
}
out.tun = wgt
return out, nil
dialer := &net.Dialer{}
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
if err := syscall.BindToDevice(int(fd), n); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to bind to device")
}
})
}
lc := &net.ListenConfig{}
lc.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
if err := syscall.BindToDevice(int(fd), n); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to bind to device")
}
})
}
t.dialer = dialer
t.lc = lc
tnet = &Net{
DialContextTCPAddrPort: t.DialContextTCPAddrPort,
DialUDPAddrPort: t.DialUDPAddrPort,
dnsServers: dnsServers,
hasV4: v4 != nil,
hasV6: v6 != nil,
}
return t, tnet, nil
}
func (tun *kernelTun) Close() (err error) {
var errs []error
for _, rule := range tun.rules {
if err = tun.handle.RuleDel(rule); err != nil {
errs = append(errs, fmt.Errorf("failed to delete rule: %w", err))
}
}
for _, route := range tun.routes {
if err = tun.handle.RouteDel(route); err != nil {
errs = append(errs, fmt.Errorf("failed to delete route: %w", err))
}
}
if err = tun.Device.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close device: %w", err))
}
tun.handle.Close()
errs = append(errs, tun.Device.Close())
return goerrors.Join(errs...)
}
func (tun *kernelTun) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) {
return tun.dialer.DialContext(ctx, "tcp", addr.String())
}
func (tun *kernelTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) {
conn, err := tun.lc.ListenPacket(context.Background(), "udp", ":0")
if err != nil {
return nil, err
}
return &internet.PacketConnWrapper{
PacketConn: conn,
Dest: net.UDPAddrFromAddrPort(raddr),
}, nil
}
func KernelTunSupported() (bool, error) {
+1 -76
View File
@@ -2,10 +2,6 @@ package wireguard
import (
"context"
"errors"
"fmt"
"net/netip"
"strings"
"github.com/xtls/xray-core/common"
)
@@ -14,80 +10,9 @@ func init() {
common.Must(common.RegisterConfig((*DeviceConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
deviceConfig := config.(*DeviceConfig)
if deviceConfig.IsClient {
return New(ctx, deviceConfig)
return NewClient(ctx, deviceConfig)
} else {
return NewServer(ctx, deviceConfig)
}
}))
}
// convert endpoint string to netip.Addr
func parseEndpoints(conf *DeviceConfig) ([]netip.Addr, bool, bool, error) {
var hasIPv4, hasIPv6 bool
endpoints := make([]netip.Addr, len(conf.Endpoint))
for i, str := range conf.Endpoint {
var addr netip.Addr
if strings.Contains(str, "/") {
prefix, err := netip.ParsePrefix(str)
if err != nil {
return nil, false, false, err
}
addr = prefix.Addr()
if prefix.Bits() != addr.BitLen() {
return nil, false, false, errors.New("interface address subnet should be /32 for IPv4 and /128 for IPv6")
}
} else {
var err error
addr, err = netip.ParseAddr(str)
if err != nil {
return nil, false, false, err
}
}
endpoints[i] = addr
if addr.Is4() {
hasIPv4 = true
} else if addr.Is6() {
hasIPv6 = true
}
}
return endpoints, hasIPv4, hasIPv6, nil
}
// serialize the config into an IPC request
func createIPCRequest(conf *DeviceConfig) string {
var request strings.Builder
request.WriteString(fmt.Sprintf("private_key=%s\n", conf.SecretKey))
if !conf.IsClient {
// placeholder, we'll handle actual port listening on Xray
request.WriteString("listen_port=1337\n")
}
for _, peer := range conf.Peers {
if peer.PublicKey != "" {
request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey))
}
if peer.PreSharedKey != "" {
request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey))
}
if peer.Endpoint != "" {
request.WriteString(fmt.Sprintf("endpoint=%s\n", peer.Endpoint))
}
for _, ip := range peer.AllowedIps {
request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip))
}
if peer.KeepAlive != 0 {
request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive))
}
}
return request.String()[:request.Len()]
}
-85
View File
@@ -390,88 +390,3 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) {
t.Error(err)
}
}
func TestShadowsocksNone(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
account := serial.ToTypedMessage(&shadowsocks.Account{
Password: "shadowsocks-password",
CipherType: shadowsocks.CipherType_NONE,
})
serverPort := tcp.PickPort()
serverConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{
Users: []*protocol.User{{
Account: account,
Level: 1,
}},
Network: []net.Network{net.Network_TCP},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
}
clientPort := tcp.PickPort()
clientConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{net.Network_TCP},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
Account: account,
},
},
}),
},
},
}
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
common.Must(err)
defer CloseAllServers(servers)
var errGroup errgroup.Group
for range 3 {
errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20))
}
if err := errGroup.Wait(); err != nil {
t.Fatal(err)
}
}
-194
View File
@@ -423,103 +423,6 @@ func TestVMessChacha20(t *testing.T) {
}
}
func TestVMessNone(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
userID := protocol.NewID(uuid.New())
serverPort := tcp.PickPort()
serverConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
}),
},
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&inbound.Config{
User: []*protocol.User{
{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
}),
},
},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
}
clientPort := tcp.PickPort()
clientConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
}),
},
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{net.Network_TCP},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&outbound.Config{
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
SecuritySettings: &protocol.SecurityConfig{
Type: protocol.SecurityType_NONE,
},
}),
},
},
}),
},
},
}
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
common.Must(err)
defer CloseAllServers(servers)
var errg errgroup.Group
for range 3 {
errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30))
}
if err := errg.Wait(); err != nil {
t.Error(err)
}
}
func TestVMessKCP(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
@@ -970,103 +873,6 @@ func TestVMessGCMMuxUDP(t *testing.T) {
}()
}
func TestVMessZero(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
userID := protocol.NewID(uuid.New())
serverPort := tcp.PickPort()
serverConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
}),
},
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&inbound.Config{
User: []*protocol.User{
{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
}),
},
},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
}
clientPort := tcp.PickPort()
clientConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
}),
},
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{net.Network_TCP},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&outbound.Config{
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
SecuritySettings: &protocol.SecurityConfig{
Type: protocol.SecurityType_ZERO,
},
}),
},
},
}),
},
},
}
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
common.Must(err)
defer CloseAllServers(servers)
var errg errgroup.Group
for range 3 {
errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30))
}
if err := errg.Wait(); err != nil {
t.Error(err)
}
}
func TestVMessGCMLengthAuth(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
+11 -10
View File
@@ -3,6 +3,7 @@ package finalmask
import (
"context"
"net"
"slices"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -28,7 +29,7 @@ func NewUdpmaskManager(udpmasks []Udpmask) *UdpmaskManager {
func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) {
var sizes []int
var conns []net.PacketConn
for i, mask := range m.udpmasks {
for i, mask := range slices.Backward(m.udpmasks) {
if _, ok := mask.(headerConn); ok {
conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1)
if err != nil {
@@ -61,7 +62,7 @@ func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketCon
func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) {
var sizes []int
var conns []net.PacketConn
for i, mask := range m.udpmasks {
for i, mask := range slices.Backward(m.udpmasks) {
if _, ok := mask.(headerConn); ok {
conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1)
if err != nil {
@@ -124,7 +125,7 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
if err != nil {
return n, addr, err
}
b = b[:n]
buf := b[:n]
sum := 0
for _, size := range c.sizes {
@@ -132,24 +133,24 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
}
if n < sum {
errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", len(b))
errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", n)
continue
}
for i := range c.conns {
n, _, err = c.conns[i].ReadFrom(b)
n, _, err = c.conns[i].ReadFrom(buf)
if err != nil {
errors.LogErrorInner(context.Background(), err, "[mask] drop packet from ", addr, " with size ", len(b))
errors.LogErrorInner(context.Background(), err, "[mask] drop packet from ", addr, " with size ", n)
break
}
b = b[c.sizes[i] : n+c.sizes[i]]
buf = buf[c.sizes[i] : n+c.sizes[i]]
}
if err != nil {
continue
}
return copy(p, b), addr, nil
return copy(p, buf), addr, nil
}
}
@@ -212,7 +213,7 @@ func NewTcpmaskManager(tcpmasks []Tcpmask) *TcpmaskManager {
func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) {
var err error
for _, mask := range m.tcpmasks {
for _, mask := range slices.Backward(m.tcpmasks) {
raw, err = mask.WrapConnClient(raw)
if err != nil {
return nil, err
@@ -223,7 +224,7 @@ func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) {
func (m *TcpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) {
var err error
for _, mask := range m.tcpmasks {
for _, mask := range slices.Backward(m.tcpmasks) {
raw, err = mask.WrapConnServer(raw)
if err != nil {
return nil, err

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