Compare commits

..
64 Commits
Author SHA1 Message Date
Fangliding 6534bbf4b2 Fix test 2026-05-16 02:41:59 +08:00
Fangliding c7672d004b Add weak for geo file resue 2026-05-16 02:27:12 +08:00
RPRX 1bdb488c9e Xray-core v26.5.9
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

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

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-05-09 17:36:29 +00:00
fanymagnet d38ba9d507 XDNS finalmask: Allow "domain+udp://0.0.0.0:0" for using dialerProxy (#6086)
Fixes https://github.com/XTLS/Xray-core/issues/6046#issuecomment-4364873593
2026-05-09 17:30:08 +00:00
风扇滑翔翼 9d9eaf399f XHTTP client: Fix potential memory leak in stream-up/one (#6095)
Fixes https://github.com/XTLS/Xray-core/pull/6087#issuecomment-4402474552
2026-05-09 16:54:57 +00:00
Meow eb32d166af Docker: Update confdir's default files (#6089)
https://github.com/XTLS/Xray-core/pull/5101#issuecomment-4321731035
2026-05-09 12:35:16 +00:00
Meow 1dbafe629a Config: Rename network/address/port in Tunnel inbound and DNS outbound (#6084)
https://github.com/XTLS/Xray-core/pull/6083#issuecomment-4387702965
https://github.com/XTLS/Xray-core/pull/6084#issuecomment-4395333530
2026-05-07 14:13:25 +00:00
Meow c42deab55c Config: Rename inbounds' clients/accounts to users (#6083)
https://github.com/XTLS/Xray-core/pull/6055#issuecomment-4363896372
https://github.com/XTLS/Xray-core/pull/6082#issuecomment-4384523482
2026-05-07 14:13:05 +00:00
Meow 906d49a271 Direct/Freedom outbound: Prefer IPv4 for finalRules' "AsIs" (#6075)
https://github.com/XTLS/Xray-core/pull/6075#issuecomment-4391684629
https://github.com/XTLS/Xray-core/pull/6075#issuecomment-4392629645
https://github.com/XTLS/Xray-core/pull/6075#issuecomment-4395258425
2026-05-07 14:12:39 +00:00
dependabot[bot] 4192ca0827 Bump google.golang.org/grpc from 1.80.0 to 1.81.0 (#6077)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.80.0 to 1.81.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.81.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.81.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-05-07 10:43:21 +00:00
RPRX 228f1e13aa Xray-core v26.5.3
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-05-03 11:53:13 +00:00
LjhAUMEM 15968585f3 quicParams config: Better unmarshaling udpHop (#6068)
Fixes https://github.com/XTLS/Xray-core/issues/6067#issuecomment-4365869178
2026-05-03 11:04:54 +00:00
RelsaandRPRX 4951994ebe README.md: Remove NetProxy-Magisk from Magisk & Android Clients (#6066)
https://github.com/XTLS/Xray-core/pull/6066#issuecomment-4365784320

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-05-03 08:51:14 +00:00
LjhAUMEM 756a2d1327 Hysteria client: Fix sendThrough (#6063)
And fixes https://github.com/XTLS/Xray-core/issues/6046
2026-05-03 07:18:23 +00:00
RPRX b279076ba1 Browser Dialer: Fix WSS with no early data
Fixes https://github.com/2dust/v2rayNG/issues/5519#issuecomment-4357741914
2026-05-02 21:55:12 +00:00
Kosta e61eeae258 Tunnel inbound: Fix panic when listening on UDS for Xray's internal services (e.g. API) (#6062)
And API supports listening on UNIX domain socket directly

Completes https://github.com/XTLS/Xray-core/pull/5693
2026-05-02 20:56:47 +00:00
Meow 958eb9ea8f Direct/Freedom outbound: Add blockDelay to finalRules (30~90s by default) (#6060)
Document: https://xtls.github.io/config/outbounds/freedom.html#finalruleobject

https://github.com/XTLS/Xray-core/pull/6058#issuecomment-4363489838
https://github.com/XTLS/Xray-core/pull/6058#issuecomment-4363522836
https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4363675060
https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4364587508
2026-05-02 20:40:46 +00:00
8381a5a8a6 Block/Blackhole outbound: Better blocking UDP (#6057)
https://github.com/XTLS/Xray-core/issues/6051#issuecomment-4364008008

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

---------

Co-authored-by: Meo597 <197331664+Meo597@users.noreply.github.com>
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-05-02 15:27:30 +00:00
Yury Kastov 1ead940a71 Config: Parallel for for inbounds' clients (#6055)
https://github.com/XTLS/Xray-core/pull/6055#issuecomment-4360958652
2026-05-02 13:32:59 +00:00
Yury KastovandRPRX bdff2fa72e Config: Support env XRAY_JSON_STRICT=true (#6053)
https://github.com/XTLS/Xray-core/pull/6053#issuecomment-4363840170

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-05-02 13:11:40 +00:00
LjhAUMEM 1d62941bd2 Hysteria: Upgrade to official v2.8.2 (#6041)
https://github.com/XTLS/Xray-core/pull/6041#issuecomment-4357417742

And fixes https://github.com/XTLS/Xray-core/issues/6039
2026-05-02 12:27:27 +00:00
LjhAUMEM 52cf9ef5d6 TUN inbound: Better "autoOutboundsInterface": "auto" (#6035)
https://github.com/XTLS/Xray-core/pull/6035#issuecomment-4336755860

Fixes https://github.com/XTLS/Xray-core/issues/6030
2026-05-02 12:18:25 +00:00
风扇滑翔翼 16568314d8 TLS for WSS/HUS: Allow outer "alpn": ["h2", "http/1.1"] for camouflage (#6034)
https://github.com/XTLS/Xray-core/pull/6034#issuecomment-4363639160

Closes https://github.com/XTLS/Xray-core/issues/6024#issuecomment-4328306231
2026-05-02 11:07:12 +00:00
风扇滑翔翼 1fc6850dc4 TLS ECH: Remove echForceQuery (ECH is forced now if configured) (#6032)
https://github.com/XTLS/Xray-core/pull/5887#issuecomment-4184701517
2026-05-02 10:33:43 +00:00
MeowandRPRX 4e87f59628 Direct/Freedom outbound: Add finalRules (matches network, port and ip, then action) with default safe policies (#6027)
Document: https://xtls.github.io/config/outbounds/freedom.html#finalruleobject

https://github.com/XTLS/Xray-core/pull/6027#issuecomment-4335790980
https://github.com/XTLS/Xray-core/pull/6027#issuecomment-4336309055
https://github.com/XTLS/Xray-core/pull/6027#issuecomment-4338269638

Closes https://github.com/XTLS/Xray-core/issues/6018#issuecomment-4329273637

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-05-02 01:58:43 +00:00
风扇滑翔翼 7ab0a3ccb7 FakeDNS: Little fix (#6022)
https://github.com/XTLS/Xray-core/pull/6021#issuecomment-4344638838
2026-05-01 22:57:51 +00:00
yiguodev 2fff03720d TUN inbound: Reply fake pong to ICMP ping (#6015)
https://github.com/XTLS/Xray-core/pull/6015#issuecomment-4321525342
2026-05-01 22:51:42 +00:00
Zero 7f7fc5a829 README.md: Add XrayUI-dev to Windows in GUI Clients (#6013)
https://github.com/XTLS/Xray-core/issues/6011#issuecomment-4320674957
2026-05-01 22:46:05 +00:00
yiguodev ff6c060168 README.md Add OneXray to more platforms in GUI Clients (#5990)
https://t.me/projectXtls/2214
2026-05-01 22:38:26 +00:00
hiDandelion 5b552db781 README.md: Add Anywhere to Others/iOS (#5762)
https://t.me/projectXtls/2237
2026-05-01 22:34:53 +00:00
dependabot[bot] 108bf7ff82 Bump github.com/robfig/cron/v3 from 3.0.0 to 3.0.1 (#6020)
Bumps [github.com/robfig/cron/v3](https://github.com/robfig/cron) from 3.0.0 to 3.0.1.
- [Commits](https://github.com/robfig/cron/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: github.com/robfig/cron/v3
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 22:31:59 +00:00
RPRX 1836b1c6e4 Browser Dialer: Potential optimized IP and non-standard port
Fixes https://github.com/2dust/v2rayNG/issues/5519#issuecomment-4335112057
2026-04-28 20:57:20 +00:00
RPRX b4f08981be v26.4.25
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-04-25 23:16:35 +00:00
RPRX cd4d0baacd Xray-core: Mark "legacy reverse" as removed to avoid confusions
https://github.com/XTLS/Xray-core/issues/5973#issuecomment-4273582111

https://github.com/XTLS/Xray-core/pull/5947#issuecomment-4273415252
2026-04-25 23:03:19 +00:00
Meow 3bc24a3d5d Geodata: Support automatically updating .dat files and hot reloading (#5992)
https://github.com/XTLS/Xray-core/pull/5992#issuecomment-4320551920

Usage: https://github.com/XTLS/Xray-core/pull/5992#issuecomment-4291168039
2026-04-25 21:20:42 +00:00
LjhAUMEM fa07b34956 XDNS finalmask: Use single UDP socket for multiple resolvers for now (#5982)
https://github.com/XTLS/Xray-core/pull/5982#issuecomment-4302271929

Closes https://github.com/XTLS/Xray-core/pull/5976#issuecomment-4320460288
2026-04-25 20:26:15 +00:00
fish4terrisa-MSDSM 85a8bf5f39 Browser Dialer: Allow being switched on runtime when Xray is used as a lib (#5978)
https://github.com/XTLS/Xray-core/pull/5978#issuecomment-4279520473

https://github.com/XTLS/Xray-core/pull/5978#issuecomment-4320401635
2026-04-25 19:48:49 +00:00
Exclude0122and风扇滑翔翼 d0f533f94a app/router/condition.go: Retrieve original target in net.FindProcess() when "IPIfNonMatch" is enabled (#5979)
Fixes https://github.com/XTLS/Xray-core/issues/5980

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-04-25 17:45:03 +00:00
Meow d1db1d6a27 DNS outbound: Add rules (matches qtype and domain, then action) (#5981)
https://github.com/XTLS/Xray-core/pull/5981#issuecomment-4279809648

Example: https://github.com/XTLS/Xray-core/pull/5981#issuecomment-4283200236

Closes https://github.com/XTLS/Xray-core/issues/5218
2026-04-25 17:27:39 +00:00
7. SunandMeow 1a14ffcec6 Geodata strmatcher: Restore lenient Type.New(Domain) behavior (#5989)
https://github.com/XTLS/Xray-core/pull/5989#issuecomment-4288238360

Fixes https://github.com/XTLS/Xray-core/issues/5986#issuecomment-4288010800

---------

Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-04-25 17:08:23 +00:00
风扇滑翔翼 454c930d13 Loopback outbound: Use DispatchLink() (#6005)
https://github.com/XTLS/Xray-core/pull/6000

Fixes https://github.com/XTLS/Xray-core/issues/5917
2026-04-25 16:48:49 +00:00
Meow bc590bcb56 Geodata: Reduce memory usage again (#5975)
https://github.com/XTLS/Xray-core/pull/5975#issuecomment-4274779560
2026-04-25 16:15:37 +00:00
Meow 7cf25970de IPMatcher: Fix full CIDR issue (#5971)
Fixes https://github.com/XTLS/Xray-core/issues/5977
2026-04-25 16:07:04 +00:00
dependabot[bot] d837687368 Bump golang.zx2c4.com/wireguard/windows from 0.6.1 to 1.0.1 (#5985)
Bumps golang.zx2c4.com/wireguard/windows from 0.6.1 to 1.0.1.

---
updated-dependencies:
- dependency-name: golang.zx2c4.com/wireguard/windows
  dependency-version: 1.0.1
  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-04-25 15:09:05 +00:00
RPRX b4650360d6 v26.4.17
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-04-17 23:04:05 +00:00
MeowandRPRX d52f15060b Direct/Freedom outbound: Block UDP responses that are come from ipsBlocked as well (#5952)
https://github.com/XTLS/Xray-core/pull/5947#issuecomment-4258980670

https://github.com/XTLS/Xray-core/pull/5952#issuecomment-4259324234

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-17 22:56:27 +00:00
Meow 31ab22c33d Geodata: Support reversed CIDR rules in IP rules (#5951)
https://github.com/XTLS/Xray-core/pull/5947#issuecomment-4258063215

https://github.com/XTLS/Xray-core/pull/5951#issuecomment-4260093653
2026-04-17 22:13:35 +00:00
Meow d42c981f9c DomainMatcher: Fix Match() result slice aliasing race (#5959)
Fixes https://github.com/XTLS/Xray-core/pull/5814
2026-04-17 22:07:58 +00:00
Иван cb1106c2fb header-custom finalmask: Extend expression primitives for 1:1 handshakes (#5949)
https://github.com/XTLS/Xray-core/pull/5945
https://github.com/XTLS/Xray-core/pull/5920
2026-04-17 22:01:54 +00:00
风扇滑翔翼 df4b97097c Loopback outbound: Avoid directly modifying potential shared ctx (#5960)
Fixes https://github.com/XTLS/Xray-core/issues/5958
2026-04-17 21:41:10 +00:00
dependabot[bot] a9cec25b8d Bump github.com/pires/go-proxyproto from 0.11.0 to 0.12.0 (#5948)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.11.0...v0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 21:33:38 +00:00
RPRX c5edc122b7 v26.4.15
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-04-15 23:56:34 +00:00
RPRX 9dd17d55fb XUDP GetGlobalID(): Remove inbound.Name == "wireguard" for now
https://github.com/XTLS/Xray-core/pull/5947#issuecomment-4256423483
2026-04-15 23:47:21 +00:00
Meowand风扇滑翔翼 310b764811 Direct/Freedom outbound: Add ipsBlocked (supports IP, CIDR, "geoip:", "ext:") and apply a default safe policy (#5947)
https://github.com/XTLS/Xray-core/pull/5892#issuecomment-4254056911

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-04-15 23:41:11 +00:00
风扇滑翔翼 3691741440 Chore: Use buf.New() instead of buf.NewWithSize() (#5946)
https://github.com/XTLS/Xray-core/pull/5941#issuecomment-4252905907

https://github.com/XTLS/Xray-core/pull/5946#issuecomment-4253919073
2026-04-15 16:57:51 +00:00
Иван 05e259c8e4 header-custom finalmask: Add UDP standalone handshake mode (#5945)
https://github.com/XTLS/Xray-core/commit/175502d8079aa5a151242ed911d01a1b90b98b28
2026-04-15 16:21:23 +00:00
Иван 175502d807 header-custom finalmask: Add programmable handshake templates and runtime core (#5920)
https://github.com/XTLS/Xray-core/pull/5920#issuecomment-4252579201
https://github.com/XTLS/Xray-core/pull/5920#issuecomment-4231698135

https://t.me/projectXtls/1829
https://t.me/projectXtls/1640
2026-04-15 14:17:51 +00:00
Boris KorzunandLjhAUMEM 6780045550 TUN inbound: Add FreeBSD support (#5891)
And reverts "refactor `mtu` to support setting IPv4/v6 separately" https://github.com/XTLS/Xray-core/pull/5891#issuecomment-4245677624

And fixes `autoOutboundsInterface` on Windows https://github.com/XTLS/Xray-core/pull/5887#issuecomment-4251719900

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-04-15 12:40:19 +00:00
LjhAUMEM ff6126463b Hysteria inbound: Use transport's authentication when there are no clients (#5942) 2026-04-15 12:13:15 +00:00
LjhAUMEM 5c3d639c09 Chore: Use buf.NewWithSize() (#5941) 2026-04-15 12:11:51 +00:00
Meow 7c56b7beea DNS: Log rule matches and client order when finalQuery returns early (#5936) 2026-04-15 12:06:13 +00:00
Meow 5b91b152bb DomainMatcher: Reduce startup time on Android as well (#5935)
https://github.com/XTLS/Xray-core/pull/5924#issuecomment-4242355080
2026-04-15 12:00:41 +00:00
Meow ef77a42063 DomainMatcher: Fix CompactDomainMatcher rule indices (#5934)
Fixes https://github.com/XTLS/Xray-core/pull/5924
2026-04-15 11:56:31 +00:00
dependabot[bot] dab99614dc Bump golang.zx2c4.com/wireguard/windows from 0.5.3 to 0.6.1 (#5932)
Bumps golang.zx2c4.com/wireguard/windows from 0.5.3 to 0.6.1.

---
updated-dependencies:
- dependency-name: golang.zx2c4.com/wireguard/windows
  dependency-version: 0.6.1
  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-04-15 11:54:49 +00:00
184 changed files with 10403 additions and 2975 deletions
+8 -5
View File
@@ -32,9 +32,12 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/03_routing.json
RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json
RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json RUN echo '{}' >/tmp/usr/local/etc/xray/07_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files # Create log files
RUN mkdir -p /tmp/var/log/xray && touch \ RUN mkdir -p /tmp/var/log/xray && touch \
@@ -45,8 +48,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+8 -5
View File
@@ -32,9 +32,12 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/03_routing.json
RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json
RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json RUN echo '{}' >/tmp/usr/local/etc/xray/07_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files # Create log files
RUN mkdir -p /tmp/var/log/xray && touch \ RUN mkdir -p /tmp/var/log/xray && touch \
@@ -54,8 +57,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+5 -2
View File
@@ -73,7 +73,6 @@
- [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless) - [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) - [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- Magisk - Magisk
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk) - [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk)
- [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk) - [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
- Homebrew - Homebrew
@@ -111,6 +110,8 @@
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient) - [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [GenyConnect](https://github.com/genyleap/GenyConnect) - [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- [XrayUI-dev](https://github.com/PhoenixNil/XrayUI-dev)
- Android - Android
- [v2rayNG](https://github.com/2dust/v2rayNG) - [v2rayNG](https://github.com/2dust/v2rayNG)
- [X-flutter](https://github.com/XTLS/X-flutter) - [X-flutter](https://github.com/XTLS/X-flutter)
@@ -118,7 +119,7 @@
- [SimpleXray](https://github.com/lhear/SimpleXray) - [SimpleXray](https://github.com/lhear/SimpleXray)
- [XrayFA](https://github.com/Q7DF1/XrayFA) - [XrayFA](https://github.com/Q7DF1/XrayFA)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk) - [OneXray](https://github.com/OneXray/OneXray)
- iOS & macOS arm64 & tvOS - iOS & macOS arm64 & tvOS
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274) - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064) - [Streisand](https://apps.apple.com/app/streisand/id6450534064)
@@ -143,10 +144,12 @@
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [v2rayN](https://github.com/2dust/v2rayN) - [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect) - [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX... ## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
- iOS & macOS arm64 & tvOS - iOS & macOS arm64 & tvOS
- [Anywhere](https://github.com/NodePassProject/Anywhere)
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118) - [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
- [Loon](https://apps.apple.com/us/app/loon/id1373567447) - [Loon](https://apps.apple.com/us/app/loon/id1373567447)
- [Egern](https://apps.apple.com/us/app/egern/id1616105820) - [Egern](https://apps.apple.com/us/app/egern/id1616105820)
+19 -4
View File
@@ -4,12 +4,14 @@ import (
"context" "context"
"net" "net"
"sync" "sync"
"strings"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/signal/done" "github.com/xtls/xray-core/common/signal/done"
core "github.com/xtls/xray-core/core" core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/transport/internet"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@@ -73,14 +75,27 @@ func (c *Commander) Start() error {
} }
} }
if len(c.listen) > 0 { if len(c.listen) > 0 {
if l, err := net.Listen("tcp", c.listen); err != nil { var addr net.Addr
if strings.HasPrefix(c.listen, "/") || strings.HasPrefix(c.listen, "@") {
addr = &net.UnixAddr{Name: c.listen, Net: "unix"}
} else {
tcpAddr, err := net.ResolveTCPAddr("tcp", c.listen)
if err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to parse listen address ", c.listen)
return err
}
addr = tcpAddr
}
l, err := internet.ListenSystem(context.Background(), addr, nil)
if err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen) errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen)
return err return err
} else {
errors.LogInfo(context.Background(), "API server listening on ", l.Addr())
go listen(l)
} }
errors.LogInfo(context.Background(), "API server listening on ", l.Addr())
go listen(l)
return nil return nil
} }
+40 -95
View File
@@ -5,18 +5,16 @@ import (
"context" "context"
go_errors "errors" go_errors "errors"
"fmt" "fmt"
"os"
"runtime"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
@@ -158,9 +156,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, client) clients = append(clients, client)
} }
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules) var domainMatcher geodata.DomainMatcher
if err != nil { if len(effectiveRules) > 0 {
return nil, err domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
} }
// If there is no DNS client in config, add a `localhost` DNS client // If there is no DNS client in config, add a `localhost` DNS client
@@ -220,7 +221,7 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
} }
if s.checkSystem { if s.checkSystem {
supportIPv4, supportIPv6 := checkRoutes() supportIPv4, supportIPv6 := utils.CheckRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4 option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6 option.IPv6Enable = option.IPv6Enable && supportIPv6
} else { } else {
@@ -271,24 +272,27 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching // Priority domain matching
hasMatch := false hasMatch := false
MatchSlice := s.domainMatcher.Match(strings.ToLower(domain)) if s.domainMatcher != nil {
sort.Slice(MatchSlice, func(i, j int) bool { matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
return MatchSlice[i] < MatchSlice[j] sort.Slice(matchSlice, func(i, j int) bool {
}) return matchSlice[i] < matchSlice[j]
for _, match := range MatchSlice { })
info := s.matcherInfos[match] for _, match := range matchSlice {
client := s.clients[info.clientIdx] info := s.matcherInfos[match]
domainRule := info.domainRule client := s.clients[info.clientIdx]
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx)) domainRule := info.domainRule
if clientUsed[info.clientIdx] { domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
continue if clientUsed[info.clientIdx] {
} continue
clientUsed[info.clientIdx] = true }
clients = append(clients, client) clientUsed[info.clientIdx] = true
clientNames = append(clientNames, client.Name()) clients = append(clients, client)
hasMatch = true clientNames = append(clientNames, client.Name())
if client.finalQuery { hasMatch = true
return clients if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
} }
} }
@@ -302,17 +306,13 @@ func (s *DNS) sortClients(domain string) []*Client {
clients = append(clients, client) clients = append(clients, client)
clientNames = append(clientNames, client.Name()) clientNames = append(clientNames, client.Name())
if client.finalQuery { if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients return clients
} }
} }
} }
if len(domainRules) > 0 { logDecision(s.ctx, domain, domainRules, clientNames)
errors.LogDebug(s.ctx, "domain ", domain, " matches following rules: ", domainRules)
}
if len(clientNames) > 0 {
errors.LogDebug(s.ctx, "domain ", domain, " will use DNS in order: ", clientNames)
}
if len(clients) == 0 { if len(clients) == 0 {
if len(s.clients) > 0 { if len(s.clients) > 0 {
@@ -327,6 +327,15 @@ func (s *DNS) sortClients(domain string) []*Client {
return clients return clients
} }
func logDecision(ctx context.Context, domain string, domainRules []string, clientNames []string) {
if len(domainRules) > 0 {
errors.LogDebug(ctx, "domain ", domain, " matches following rules: ", domainRules)
}
if len(clientNames) > 0 {
errors.LogDebug(ctx, "domain ", domain, " will use DNS in order: ", clientNames)
}
}
func mergeQueryErrors(domain string, errs []error) error { func mergeQueryErrors(domain string, errs []error) error {
if len(errs) == 0 { if len(errs) == 0 {
return dns.ErrEmptyResponse return dns.ErrEmptyResponse
@@ -528,67 +537,3 @@ func init() {
return New(ctx, config.(*Config)) return New(ctx, config.(*Config))
})) }))
} }
func probeRoutes() (ipv4 bool, ipv6 bool) {
if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil {
ipv4 = true
conn.Close()
}
if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil {
ipv6 = true
conn.Close()
}
return
}
var routeCache struct {
sync.Once
sync.RWMutex
expire time.Time
ipv4, ipv6 bool
}
func checkRoutes() (bool, bool) {
if !isGUIPlatform {
routeCache.Once.Do(func() {
routeCache.ipv4, routeCache.ipv6 = probeRoutes()
})
return routeCache.ipv4, routeCache.ipv6
}
routeCache.RWMutex.RLock()
now := time.Now()
if routeCache.expire.After(now) {
routeCache.RWMutex.RUnlock()
return routeCache.ipv4, routeCache.ipv6
}
routeCache.RWMutex.RUnlock()
routeCache.RWMutex.Lock()
defer routeCache.RWMutex.Unlock()
now = time.Now()
if routeCache.expire.After(now) { // double-check
return routeCache.ipv4, routeCache.ipv6
}
routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms
routeCache.expire = now.Add(100 * time.Millisecond) // ttl
return routeCache.ipv4, routeCache.ipv6
}
var isGUIPlatform = detectGUIPlatform()
func detectGUIPlatform() bool {
switch runtime.GOOS {
case "android", "ios", "windows", "darwin":
return true
case "linux", "freebsd", "openbsd":
if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" {
return true
}
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
return true
}
}
return false
}
+36 -76
View File
@@ -147,7 +147,9 @@ func TestUDPServerSubnet(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -207,7 +209,9 @@ func TestUDPServer(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -345,7 +349,9 @@ func TestPrioritizedDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -414,7 +420,9 @@ func TestUDPServerIPv6(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -481,7 +489,9 @@ func TestStaticHostDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -538,15 +548,8 @@ func TestIPMatch(t *testing.T) {
Port: uint32(port), Port: uint32(port),
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
{ // inner ip, will not match
Value: &geodata.IPRule_Custom{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
Custom: &geodata.CIDR{
// inner ip, will not match
Ip: []byte{192, 168, 11, 1},
Prefix: 32,
},
},
},
}, },
}, },
// second dns, match ip // second dns, match ip
@@ -561,22 +564,8 @@ func TestIPMatch(t *testing.T) {
Port: uint32(port), Port: uint32(port),
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
Value: &geodata.IPRule_Custom{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 4},
Prefix: 32,
},
},
},
}, },
}, },
}, },
@@ -587,7 +576,9 @@ func TestIPMatch(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -664,9 +655,9 @@ func TestLocalDomain(t *testing.T) {
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
// Will match localhost, localhost-a and localhost-b, // Will match localhost, localhost-a and localhost-b,
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}, {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}, {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}, {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}},
}, },
}, },
{ {
@@ -704,7 +695,9 @@ func TestLocalDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -887,22 +880,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.8.8 and 8.8.4.4 // Will only match 8.8.8.8 and 8.8.4.4
{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
Value: &geodata.IPRule_Custom{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}},
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 4, 4},
Prefix: 32,
},
},
},
}, },
}, },
{ {
@@ -922,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
// Will match 8.8.8.8 and 8.8.8.7, etc // Will match 8.8.8.8 and 8.8.8.7, etc
{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 7},
Prefix: 24,
},
},
},
}, },
}, },
{ {
@@ -949,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.7.7 (api.google.com) // Will only match 8.8.7.7 (api.google.com)
{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 7, 7},
Prefix: 32,
},
},
},
}, },
}, },
{ {
@@ -976,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.7.8 (v2.api.google.com) // Will only match 8.8.7.8 (v2.api.google.com)
{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 7, 8},
Prefix: 32,
},
},
},
}, },
}, },
}, },
@@ -994,7 +952,9 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
+8 -12
View File
@@ -17,7 +17,7 @@ import (
type Holder struct { type Holder struct {
domainToIP cache.Lru domainToIP cache.Lru
ipRange *net.IPNet ipRange *net.IPNet
mu *sync.Mutex mu sync.Mutex
config *FakeDnsPool config *FakeDnsPool
} }
@@ -49,9 +49,7 @@ func (fkdns *Holder) Start() error {
} }
func (fkdns *Holder) Close() error { func (fkdns *Holder) Close() error {
fkdns.domainToIP = nil // nothing to do for now, just wait GC
fkdns.ipRange = nil
fkdns.mu = nil
return nil return nil
} }
@@ -70,7 +68,7 @@ func NewFakeDNSHolder() (*Holder, error) {
} }
func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) { func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) {
return &Holder{nil, nil, nil, conf}, nil return &Holder{config: conf}, nil
} }
func (fkdns *Holder) initializeFromConfig() error { func (fkdns *Holder) initializeFromConfig() error {
@@ -92,7 +90,6 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
} }
fkdns.domainToIP = cache.NewLru(lruSize) fkdns.domainToIP = cache.NewLru(lruSize)
fkdns.ipRange = ipRange fkdns.ipRange = ipRange
fkdns.mu = new(sync.Mutex)
return nil return nil
} }
@@ -103,7 +100,7 @@ func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
if v, ok := fkdns.domainToIP.Get(domain); ok { if v, ok := fkdns.domainToIP.Get(domain); ok {
return []net.Address{v.(net.Address)} return []net.Address{v.(net.Address)}
} }
currentTimeMillis := uint64(time.Now().UnixNano() / 1e6) currentTimeMillis := uint64(time.Now().UnixMilli())
ones, bits := fkdns.ipRange.Mask.Size() ones, bits := fkdns.ipRange.Mask.Size()
rooms := bits - ones rooms := bits - ones
if rooms < 64 { if rooms < 64 {
@@ -202,12 +199,11 @@ func (h *HolderMulti) Start() error {
} }
func (h *HolderMulti) Close() error { func (h *HolderMulti) Close() error {
var errs []error
for _, v := range h.holders { for _, v := range h.holders {
if err := v.Close(); err != nil { errs = append(errs, v.Close())
return errors.New("Cannot close all fake dns pools").Base(err)
}
} }
return nil return errors.Combine(errs...)
} }
func (h *HolderMulti) createHolderGroups() error { func (h *HolderMulti) createHolderGroups() error {
@@ -222,7 +218,7 @@ func (h *HolderMulti) createHolderGroups() error {
} }
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) { func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
holderMulti := &HolderMulti{nil, conf} holderMulti := &HolderMulti{config: conf}
if err := holderMulti.createHolderGroups(); err != nil { if err := holderMulti.createHolderGroups(); err != nil {
return nil, err return nil, err
} }
+14 -11
View File
@@ -13,8 +13,8 @@ import (
// StaticHosts represents static domain-ip mapping in DNS server. // StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct { type StaticHosts struct {
reps [][]net.Address responses [][]net.Address
matcher geodata.DomainMatcher matcher geodata.DomainMatcher
} }
// NewStaticHosts creates a new StaticHosts instance. // NewStaticHosts creates a new StaticHosts instance.
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
rep = append(rep, addr) rep = append(rep, addr)
} }
} }
// if len(rep) == 0 {
// errors.LogError(context.Background(), "empty value in static hosts, ignore this rule: ", mapping.Domain)
// continue
// }
reps = append(reps, rep) reps = append(reps, rep)
rules = append(rules, mapping.Domain) rules = append(rules, mapping.Domain)
} }
if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules) matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &StaticHosts{ return &StaticHosts{
reps: reps, responses: reps,
matcher: matcher, matcher: matcher,
}, nil }, nil
} }
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) { func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0) ips := make([]net.Address, 0)
found := false found := false
for _, ruleIdx := range h.matcher.Match(domain) { for _, idx := range h.matcher.Match(domain) {
for _, rep := range h.reps[ruleIdx] { for _, rep := range h.responses[idx] {
if err, ok := rep.(dns.RCodeError); ok { if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 { if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
return nil, err return nil, err
} }
} }
ips = append(ips, h.reps[ruleIdx]...) ips = append(ips, h.responses[idx]...)
found = true found = true
} }
if !found { if !found {
@@ -122,5 +122,8 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts. // Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) { func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5) return h.lookup(domain, option, 5)
} }
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
@@ -166,7 +167,7 @@ func (c *Client) Name() string {
// QueryIP sends DNS query to the name server with the client's IP. // QueryIP sends DNS query to the name server with the client's IP.
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) { func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) {
if c.checkSystem { if c.checkSystem {
supportIPv4, supportIPv6 := checkRoutes() supportIPv4, supportIPv6 := utils.CheckRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4 option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6 option.IPv6Enable = option.IPv6Enable && supportIPv6
} else { } else {
+198
View File
@@ -0,0 +1,198 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: app/geodata/config.proto
package geodata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Asset struct {
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
File string `protobuf:"bytes,2,opt,name=file,proto3" json:"file,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Asset) Reset() {
*x = Asset{}
mi := &file_app_geodata_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Asset) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Asset) ProtoMessage() {}
func (x *Asset) ProtoReflect() protoreflect.Message {
mi := &file_app_geodata_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Asset.ProtoReflect.Descriptor instead.
func (*Asset) Descriptor() ([]byte, []int) {
return file_app_geodata_config_proto_rawDescGZIP(), []int{0}
}
func (x *Asset) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Asset) GetFile() string {
if x != nil {
return x.File
}
return ""
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cron string `protobuf:"bytes,1,opt,name=cron,proto3" json:"cron,omitempty"`
Outbound string `protobuf:"bytes,2,opt,name=outbound,proto3" json:"outbound,omitempty"`
Assets []*Asset `protobuf:"bytes,3,rep,name=assets,proto3" json:"assets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_app_geodata_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_geodata_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_app_geodata_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetCron() string {
if x != nil {
return x.Cron
}
return ""
}
func (x *Config) GetOutbound() string {
if x != nil {
return x.Outbound
}
return ""
}
func (x *Config) GetAssets() []*Asset {
if x != nil {
return x.Assets
}
return nil
}
var File_app_geodata_config_proto protoreflect.FileDescriptor
const file_app_geodata_config_proto_rawDesc = "" +
"\n" +
"\x18app/geodata/config.proto\x12\x10xray.app.geodata\"-\n" +
"\x05Asset\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
"\x04file\x18\x02 \x01(\tR\x04file\"i\n" +
"\x06Config\x12\x12\n" +
"\x04cron\x18\x01 \x01(\tR\x04cron\x12\x1a\n" +
"\boutbound\x18\x02 \x01(\tR\boutbound\x12/\n" +
"\x06assets\x18\x03 \x03(\v2\x17.xray.app.geodata.AssetR\x06assetsBR\n" +
"\x14com.xray.app.geodataP\x01Z%github.com/xtls/xray-core/app/geodata\xaa\x02\x10Xray.App.Geodatab\x06proto3"
var (
file_app_geodata_config_proto_rawDescOnce sync.Once
file_app_geodata_config_proto_rawDescData []byte
)
func file_app_geodata_config_proto_rawDescGZIP() []byte {
file_app_geodata_config_proto_rawDescOnce.Do(func() {
file_app_geodata_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_geodata_config_proto_rawDesc), len(file_app_geodata_config_proto_rawDesc)))
})
return file_app_geodata_config_proto_rawDescData
}
var file_app_geodata_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_geodata_config_proto_goTypes = []any{
(*Asset)(nil), // 0: xray.app.geodata.Asset
(*Config)(nil), // 1: xray.app.geodata.Config
}
var file_app_geodata_config_proto_depIdxs = []int32{
0, // 0: xray.app.geodata.Config.assets:type_name -> xray.app.geodata.Asset
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_app_geodata_config_proto_init() }
func file_app_geodata_config_proto_init() {
if File_app_geodata_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_geodata_config_proto_rawDesc), len(file_app_geodata_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_geodata_config_proto_goTypes,
DependencyIndexes: file_app_geodata_config_proto_depIdxs,
MessageInfos: file_app_geodata_config_proto_msgTypes,
}.Build()
File_app_geodata_config_proto = out.File
file_app_geodata_config_proto_goTypes = nil
file_app_geodata_config_proto_depIdxs = nil
}
+21
View File
@@ -0,0 +1,21 @@
syntax = "proto3";
package xray.app.geodata;
option csharp_namespace = "Xray.App.Geodata";
option go_package = "github.com/xtls/xray-core/app/geodata";
option java_package = "com.xray.app.geodata";
option java_multiple_files = true;
message Asset {
string url = 1;
string file = 2;
}
message Config {
string cron = 1;
string outbound = 2;
repeated Asset assets = 3;
}
+304
View File
@@ -0,0 +1,304 @@
package geodata
import (
"context"
go_errors "errors"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged"
)
const idleTimeout = 30 * time.Second
type stage struct {
target string
temp string
}
type downloader struct {
ctx context.Context
client *http.Client
}
type idleConn struct {
net.Conn
}
func (c *idleConn) Read(b []byte) (int, error) {
t := time.AfterFunc(idleTimeout, func() {
_ = c.Close()
})
n, err := c.Conn.Read(b)
if !t.Stop() {
_ = c.Close()
return n, errors.New("connection idle timeout")
}
return n, err
}
func (c *idleConn) Write(b []byte) (int, error) {
return c.Conn.Write(b)
}
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
client: newClient(ctx, dispatcher, outbound),
}
}
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())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
func (d *downloader) download(assets []*Asset) ([]stage, error) {
staged := make([]stage, 0, len(assets))
for _, asset := range assets {
stage, err := d.downloadOne(asset)
if err != nil {
clean(staged)
return nil, err
}
staged = append(staged, stage)
}
return staged, nil
}
func (d *downloader) downloadOne(asset *Asset) (stage, error) {
target, err := filesystem.ResolveAsset(asset.File)
if err != nil {
return stage{}, err
}
errors.LogInfo(d.ctx, "downloading geodata asset from ", asset.Url, " to ", target)
temp, err := tempFile(target, ".tmp")
if err != nil {
return stage{}, err
}
tempName := temp.Name()
keepTemp := false
defer func() {
if !keepTemp {
os.Remove(tempName)
}
}()
if err := d.fetch(asset.Url, temp); err != nil {
temp.Close()
return stage{}, err
}
if err := temp.Chmod(0o644); err != nil {
temp.Close()
return stage{}, err
}
if err := temp.Close(); err != nil {
return stage{}, err
}
keepTemp = true
return stage{
target: target,
temp: tempName,
}, nil
}
func (d *downloader) fetch(rawURL string, writer io.Writer) error {
req, err := http.NewRequestWithContext(d.ctx, http.MethodGet, rawURL, nil)
if err != nil {
return err
}
utils.TryDefaultHeadersWith(req.Header, "nav")
resp, err := d.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
io.Copy(io.Discard, resp.Body)
return errors.New("unexpected status code: ", resp.StatusCode)
}
n, err := io.Copy(writer, resp.Body)
if err != nil {
return err
}
if n == 0 {
return errors.New("empty response body")
}
return nil
}
func clean(assets []stage) {
for _, asset := range assets {
if asset.temp != "" {
os.Remove(asset.temp)
}
}
}
type tx struct {
swaps []swap
}
type swap struct {
target string
backup string
hadOriginal bool
}
func swapAll(assets []stage) (*tx, error) {
t := &tx{}
for _, asset := range assets {
s, err := swapOne(asset)
if err != nil {
return nil, errors.Combine(err, t.rollback())
}
t.swaps = append(t.swaps, s)
}
return t, nil
}
func swapOne(asset stage) (swap, error) {
backup, err := backupFile(asset.target)
if err != nil {
return swap{}, err
}
s := swap{
target: asset.target,
backup: backup,
}
if err := os.Rename(asset.target, backup); err != nil {
if !go_errors.Is(err, os.ErrNotExist) {
return swap{}, err
}
if err := os.Remove(backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
return swap{}, err
}
} else {
s.hadOriginal = true
}
if err := os.Rename(asset.temp, asset.target); err != nil {
if s.hadOriginal {
if restoreErr := os.Rename(backup, asset.target); restoreErr != nil {
return swap{}, errors.Combine(err, restoreErr)
}
}
return swap{}, err
}
return s, nil
}
func (t *tx) rollback() error {
var errs []error
for i := len(t.swaps) - 1; i >= 0; i-- {
if err := t.swaps[i].rollback(); err != nil {
errs = append(errs, err)
}
}
return errors.Combine(errs...)
}
func (s swap) rollback() error {
var errs []error
if err := os.Remove(s.target); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
if s.hadOriginal {
if err := os.Rename(s.backup, s.target); err != nil {
errs = append(errs, err)
}
} else if err := os.Remove(s.backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
return errors.Combine(errs...)
}
func (t *tx) commit() error {
var errs []error
for _, swap := range t.swaps {
if err := os.Remove(swap.backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
}
return errors.Combine(errs...)
}
func tempFile(target string, suffix string) (*os.File, error) {
dir := filepath.Dir(target)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return os.CreateTemp(dir, "."+filepath.Base(target)+".*"+suffix)
}
func backupFile(target string) (string, error) {
file, err := tempFile(target, ".bak")
if err != nil {
return "", err
}
name := file.Name()
if err := file.Close(); err != nil {
os.Remove(name)
return "", err
}
if err := os.Remove(name); err != nil {
return "", err
}
return name, nil
}
+134
View File
@@ -0,0 +1,134 @@
package geodata
import (
"context"
"sync"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
commongeodata "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
)
type Instance struct {
assets []*Asset
downloader *downloader
tasker *cron.Cron
mu sync.Mutex
running bool
}
func New(ctx context.Context, config *Config) (*Instance, error) {
if config.Cron == "" {
return &Instance{}, nil
}
g := &Instance{
assets: config.Assets,
}
if len(g.assets) > 0 {
var dispatcher routing.Dispatcher
if err := core.RequireFeatures(ctx, func(d routing.Dispatcher) {
dispatcher = d
}); err != nil {
return nil, errors.New("failed to get dispatcher for geodata downloader").Base(err)
}
g.downloader = newDownloader(ctx, dispatcher, config.Outbound)
}
g.tasker = cron.New(
cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)),
cron.WithLogger(cron.DiscardLogger),
)
if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron)
return g, nil
}
func (g *Instance) execute() {
var err error
if g.downloader != nil {
err = g.reloadWithUpdate()
} else {
err = reload()
}
if err != nil {
errors.LogErrorInner(context.Background(), err, "scheduled geodata reload failed")
}
}
func (g *Instance) reloadWithUpdate() error {
staged, err := g.downloader.download(g.assets)
if err != nil {
return err
}
defer clean(staged)
tx, err := swapAll(staged)
if err != nil {
return err
}
if err := reload(); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload geodata after downloading assets, rolling back")
rollbackErr := tx.rollback()
return errors.Combine(err, rollbackErr)
}
return tx.commit()
}
func reload() error {
return errors.Combine(commongeodata.IPReg.Reload(), commongeodata.DomainReg.Reload())
}
func (g *Instance) Type() interface{} {
return (*Instance)(nil)
}
func (g *Instance) Start() error {
g.mu.Lock()
defer g.mu.Unlock()
if g.running {
return nil
}
if g.tasker != nil {
g.tasker.Start()
}
g.running = true
return nil
}
func (g *Instance) Close() error {
g.mu.Lock()
defer g.mu.Unlock()
if !g.running {
return nil
}
if g.tasker != nil {
<-g.tasker.Stop().Done()
}
g.running = false
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
return New(ctx, cfg.(*Config))
}))
}
+4 -6
View File
@@ -18,9 +18,9 @@ import (
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/proxy/hysteria/account" hysteria_proxy "github.com/xtls/xray-core/proxy/hysteria"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tcp" "github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/udp" "github.com/xtls/xray-core/transport/internet/udp"
@@ -134,10 +134,8 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
func (w *tcpWorker) Start() error { func (w *tcpWorker) Start() error {
ctx := context.Background() ctx := context.Background()
type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator } if v, ok := w.proxy.(*hysteria_proxy.Server); ok {
if v, ok := w.proxy.(HysteriaInboundValidator); ok { ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator())
ctx = hyCtx.ContextWithRequireDatagram(ctx, true)
ctx = hyCtx.ContextWithValidator(ctx, v.HysteriaInboundValidator())
} }
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) { hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
+3 -3
View File
@@ -48,7 +48,7 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag", Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
}) })
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146)) conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*stat.CounterConnection) _, ok := conn.(*stat.CounterConnection)
@@ -78,7 +78,7 @@ func TestOutboundWithStatCounter(t *testing.T) {
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag", Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
}) })
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146)) conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*stat.CounterConnection) _, ok := conn.(*stat.CounterConnection)
@@ -118,7 +118,7 @@ func TestTagsCache(t *testing.T) {
tag := fmt.Sprintf("%s%d", tags_prefix, idx) tag := fmt.Sprintf("%s%d", tags_prefix, idx)
cfg := &core.OutboundHandlerConfig{ cfg := &core.OutboundHandlerConfig{
Tag: tag, Tag: tag,
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
} }
if h, err := NewHandler(ctx, cfg); err == nil { if h, err := NewHandler(ctx, cfg); err == nil {
if err := ohm.AddHandler(ctx, h); err == nil { if err := ohm.AddHandler(ctx, h); err == nil {
+1 -1
View File
@@ -308,7 +308,7 @@ func TestServiceTestRoute(t *testing.T) {
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}, SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
+8 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
) )
type Condition interface { type Condition interface {
@@ -356,7 +357,13 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
var dstIP string var dstIP string
var dstPort uint16 = 0 var dstPort uint16 = 0
if len(ctx.GetTargetIPs()) > 0 {
// do not use resolved IP because Android process lookup needs original dst ip
resolvableContext, ok := ctx.(*dns.ResolvableContext)
if ok && len(resolvableContext.Context.GetTargetIPs()) > 0 {
dstIP = resolvableContext.Context.GetTargetIPs()[0].String()
dstPort = uint16(resolvableContext.Context.GetTargetPort())
} else if len(ctx.GetTargetIPs()) > 0 {
dstIP = ctx.GetTargetIPs()[0].String() dstIP = ctx.GetTargetIPs()[0].String()
dstPort = uint16(ctx.GetTargetPort()) dstPort = uint16(ctx.GetTargetPort())
} }
+8 -12
View File
@@ -92,25 +92,22 @@ func TestRoutingRule(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{8, 8, 8, 8}, Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
Prefix: 32,
}, },
}, },
}, },
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{8, 8, 8, 8}, Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
Prefix: 32,
}, },
}, },
}, },
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
Prefix: 128,
}, },
}, },
}, },
@@ -140,9 +137,8 @@ func TestRoutingRule(t *testing.T) {
SourceIp: []*geodata.IPRule{ SourceIp: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{192, 168, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
Prefix: 16,
}, },
}, },
}, },
+6 -9
View File
@@ -159,9 +159,8 @@ func TestIPOnDemand(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{192, 168, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
Prefix: 16,
}, },
}, },
}, },
@@ -204,9 +203,8 @@ func TestIPIfNonMatchDomain(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{192, 168, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
Prefix: 16,
}, },
}, },
}, },
@@ -249,9 +247,8 @@ func TestIPIfNonMatchIP(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{127, 0, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
Prefix: 8,
}, },
}, },
}, },
+85 -30
View File
@@ -8,10 +8,15 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata/strmatcher" "github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
) )
type DomainMatcher interface { type DomainMatcher interface {
// Match returns the indices of all rules that match the input domain.
// The returned slice is owned by the caller and may be safely modified.
// Note: the slice may contain duplicates and the order is unspecified.
Match(input string) []uint32 Match(input string) []uint32
MatchAny(input string) bool MatchAny(input string) bool
} }
@@ -19,10 +24,54 @@ type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error) BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
} }
type MphDomainMatcherFactory struct{} type MphDomainMatcherFactory struct {
sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher]
}
func buildDomainRulesKey(rules []*DomainRule) string {
var sb strings.Builder
cache := false
for _, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
sb.WriteString(v.Custom.Type.String())
sb.WriteString(":")
sb.WriteString(v.Custom.Value)
sb.WriteString(",")
case *DomainRule_Geosite:
cache = true
sb.WriteString(v.Geosite.File)
sb.WriteString(":")
sb.WriteString(v.Geosite.Code)
sb.WriteString("@")
sb.WriteString(v.Geosite.Attrs)
sb.WriteString(",")
default:
panic("unknown domain rule type")
}
}
if !cache {
return ""
}
return sb.String()
}
// BuildMatcher implements DomainMatcherFactory. // BuildMatcher implements DomainMatcherFactory.
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) { func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
key := buildDomainRulesKey(rules)
if key != "" {
f.Lock()
defer f.Unlock()
if g, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
return g, nil
}
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
}
g := strmatcher.NewMphValueMatcher() g := strmatcher.NewMphValueMatcher()
for i, r := range rules { for i, r := range rules {
switch v := r.Value.(type) { switch v := r.Value.(type) {
@@ -53,25 +102,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
if err := g.Build(); err != nil { if err := g.Build(); err != nil {
return nil, err return nil, err
} }
if key != "" {
f.shared.Store(key, g)
}
return g, nil return g, nil
} }
type CompactDomainMatcherFactory struct { type CompactDomainMatcherFactory struct {
sync.Mutex sync.Mutex
shared map[string]strmatcher.MatcherGroup // TODO: cleanup shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher]
} }
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, error) { func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
key := rule.File + ":" + rule.Code + "@" + rule.Attrs key := rule.File + ":" + rule.Code + "@" + rule.Attrs
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
if m := f.shared[key]; m != nil { if s, ok := f.shared.Load(key); ok {
return m, nil errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil
} }
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
g := strmatcher.NewLinearValueMatcher() s := strmatcher.NewLinearAnyMatcher()
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs) domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -83,60 +137,58 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err) errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue continue
} }
g.Add(m, 0) s.Add(m)
} }
f.shared[key] = g f.shared.Store(key, s)
return g, err return s, err
} }
// BuildMatcher implements DomainMatcherFactory. // BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) { func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{ compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)), matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)), values: make([]uint32, 0, len(rules)),
} }
custom := strmatcher.NewLinearValueMatcher() for i, r := range rules {
var idx uint32
for _, r := range rules {
switch v := r.Value.(type) { switch v := r.Value.(type) {
case *DomainRule_Custom: case *DomainRule_Custom:
m, err := parseDomain(v.Custom) m, err := parseDomain(v.Custom)
if err != nil { if err != nil {
return nil, err return nil, err
} }
custom.Add(m, 0) if compact.custom == nil {
compact.custom = strmatcher.NewLinearValueMatcher()
}
compact.custom.Add(m, uint32(i))
case *DomainRule_Geosite: case *DomainRule_Geosite:
m, err := f.getOrCreateFrom(v.Geosite) m, err := f.getOrCreateFrom(v.Geosite)
if err != nil { if err != nil {
return nil, err return nil, err
} }
compact.matchers = append(compact.matchers, m) compact.matchers = append(compact.matchers, m)
compact.values = append(compact.values, idx) compact.values = append(compact.values, uint32(i))
idx++
default: default:
panic("unknown domain rule type") panic("unknown domain rule type")
} }
} }
if len(compact.matchers) != len(rules) {
compact.matchers = append(compact.matchers, custom)
compact.values = append(compact.values, idx+1)
}
return compact, nil return compact, nil
} }
type CompactDomainMatcher struct { type CompactDomainMatcher struct {
matchers []strmatcher.MatcherGroup custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherSet
values []uint32 values []uint32
} }
func (c *CompactDomainMatcher) Add(matcher strmatcher.MatcherGroup, value uint32) {
c.matchers = append(c.matchers, matcher)
c.values = append(c.values, value)
}
// Match implements DomainMatcher. // Match implements DomainMatcher.
func (c *CompactDomainMatcher) Match(input string) []uint32 { func (c *CompactDomainMatcher) Match(input string) []uint32 {
result := make([]uint32, 0) var result []uint32
if c.custom != nil {
result = append(result, c.custom.Match(input)...)
}
for i, m := range c.matchers { for i, m := range c.matchers {
if m.MatchAny(input) { if m.MatchAny(input) {
result = append(result, c.values[i]) result = append(result, c.values[i])
@@ -147,6 +199,9 @@ func (c *CompactDomainMatcher) Match(input string) []uint32 {
// MatchAny implements DomainMatcher. // MatchAny implements DomainMatcher.
func (c *CompactDomainMatcher) MatchAny(input string) bool { func (c *CompactDomainMatcher) MatchAny(input string) bool {
if c.custom != nil && c.custom.MatchAny(input) {
return true
}
for _, m := range c.matchers { for _, m := range c.matchers {
if m.MatchAny(input) { if m.MatchAny(input) {
return true return true
@@ -175,9 +230,9 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory { func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS { switch runtime.GOOS {
case "ios": case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)} return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
default: default:
return &MphDomainMatcherFactory{} return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
} }
} }
+74
View File
@@ -0,0 +1,74 @@
package geodata
import (
"path/filepath"
"reflect"
"slices"
"testing"
"github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
)
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("example.com")
slices.Sort(got)
want := []uint32{0, 1}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("163.com")
slices.Sort(got)
want := []uint32{0, 1}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}).
BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("example.com")
if !reflect.DeepEqual(got, []uint32{0, 1}) {
t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1})
}
got[0] = 1
gotAgain := matcher.Match("example.com")
if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) {
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
}
}
+79 -2
View File
@@ -1,11 +1,59 @@
package geodata package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
)
type DomainRegistry struct { type DomainRegistry struct {
factory DomainMatcherFactory mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
} }
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) { func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
return r.factory.BuildMatcher(rules) r.mu.Lock()
defer r.mu.Unlock()
m, err := r.factory.BuildMatcher(rules)
if err != nil {
return nil, err
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
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)")
factory := newDomainMatcherFactory()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
return nil
} }
func newDomainRegistry() *DomainRegistry { func newDomainRegistry() *DomainRegistry {
@@ -15,3 +63,32 @@ func newDomainRegistry() *DomainRegistry {
} }
var DomainReg = newDomainRegistry() var DomainReg = newDomainRegistry()
type domainMatcherState struct {
matcher DomainMatcher
}
type DynamicDomainMatcher struct {
rules []*DomainRule
state atomic.Pointer[domainMatcherState]
}
// Match implements DomainMatcher.
func (d *DynamicDomainMatcher) Match(input string) []uint32 {
return d.state.Load().matcher.Match(input)
}
// MatchAny implements DomainMatcher.
func (d *DynamicDomainMatcher) MatchAny(input string) bool {
return d.state.Load().matcher.MatchAny(input)
}
func (d *DynamicDomainMatcher) Reload(newMatcher DomainMatcher) {
d.state.Store(&domainMatcherState{matcher: newMatcher})
}
func NewDynamicDomainMatcher(rules []*DomainRule, matcher DomainMatcher) *DynamicDomainMatcher {
d := &DynamicDomainMatcher{rules: rules}
d.Reload(matcher)
return d
}
+96 -39
View File
@@ -433,6 +433,58 @@ func (x *CIDR) GetPrefix() uint32 {
return 0 return 0
} }
type CIDRRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cidr *CIDR `protobuf:"bytes,1,opt,name=cidr,proto3" json:"cidr,omitempty"`
ReverseMatch bool `protobuf:"varint,2,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CIDRRule) Reset() {
*x = CIDRRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CIDRRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDRRule) ProtoMessage() {}
func (x *CIDRRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CIDRRule.ProtoReflect.Descriptor instead.
func (*CIDRRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
}
func (x *CIDRRule) GetCidr() *CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *CIDRRule) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type GeoIP struct { type GeoIP struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
@@ -444,7 +496,7 @@ type GeoIP struct {
func (x *GeoIP) Reset() { func (x *GeoIP) Reset() {
*x = GeoIP{} *x = GeoIP{}
mi := &file_common_geodata_geodat_proto_msgTypes[6] mi := &file_common_geodata_geodat_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -456,7 +508,7 @@ func (x *GeoIP) String() string {
func (*GeoIP) ProtoMessage() {} func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message { func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[6] mi := &file_common_geodata_geodat_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -469,7 +521,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. // Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) { func (*GeoIP) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6} return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
} }
func (x *GeoIP) GetCode() string { func (x *GeoIP) GetCode() string {
@@ -502,7 +554,7 @@ type GeoIPList struct {
func (x *GeoIPList) Reset() { func (x *GeoIPList) Reset() {
*x = GeoIPList{} *x = GeoIPList{}
mi := &file_common_geodata_geodat_proto_msgTypes[7] mi := &file_common_geodata_geodat_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -514,7 +566,7 @@ func (x *GeoIPList) String() string {
func (*GeoIPList) ProtoMessage() {} func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message { func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[7] mi := &file_common_geodata_geodat_proto_msgTypes[8]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -527,7 +579,7 @@ func (x *GeoIPList) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. // Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) { func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7} return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
} }
func (x *GeoIPList) GetEntry() []*GeoIP { func (x *GeoIPList) GetEntry() []*GeoIP {
@@ -548,7 +600,7 @@ type GeoIPRule struct {
func (x *GeoIPRule) Reset() { func (x *GeoIPRule) Reset() {
*x = GeoIPRule{} *x = GeoIPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[8] mi := &file_common_geodata_geodat_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -560,7 +612,7 @@ func (x *GeoIPRule) String() string {
func (*GeoIPRule) ProtoMessage() {} func (*GeoIPRule) ProtoMessage() {}
func (x *GeoIPRule) ProtoReflect() protoreflect.Message { func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[8] mi := &file_common_geodata_geodat_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -573,7 +625,7 @@ func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead. // Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
func (*GeoIPRule) Descriptor() ([]byte, []int) { func (*GeoIPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8} return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
} }
func (x *GeoIPRule) GetFile() string { func (x *GeoIPRule) GetFile() string {
@@ -610,7 +662,7 @@ type IPRule struct {
func (x *IPRule) Reset() { func (x *IPRule) Reset() {
*x = IPRule{} *x = IPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[9] mi := &file_common_geodata_geodat_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -622,7 +674,7 @@ func (x *IPRule) String() string {
func (*IPRule) ProtoMessage() {} func (*IPRule) ProtoMessage() {}
func (x *IPRule) ProtoReflect() protoreflect.Message { func (x *IPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[9] mi := &file_common_geodata_geodat_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -635,7 +687,7 @@ func (x *IPRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead. // Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
func (*IPRule) Descriptor() ([]byte, []int) { func (*IPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9} return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10}
} }
func (x *IPRule) GetValue() isIPRule_Value { func (x *IPRule) GetValue() isIPRule_Value {
@@ -654,7 +706,7 @@ func (x *IPRule) GetGeoip() *GeoIPRule {
return nil return nil
} }
func (x *IPRule) GetCustom() *CIDR { func (x *IPRule) GetCustom() *CIDRRule {
if x != nil { if x != nil {
if x, ok := x.Value.(*IPRule_Custom); ok { if x, ok := x.Value.(*IPRule_Custom); ok {
return x.Custom return x.Custom
@@ -672,7 +724,7 @@ type IPRule_Geoip struct {
} }
type IPRule_Custom struct { type IPRule_Custom struct {
Custom *CIDR `protobuf:"bytes,2,opt,name=custom,proto3,oneof"` Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
} }
func (*IPRule_Geoip) isIPRule_Value() {} func (*IPRule_Geoip) isIPRule_Value() {}
@@ -693,7 +745,7 @@ type Domain_Attribute struct {
func (x *Domain_Attribute) Reset() { func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{} *x = Domain_Attribute{}
mi := &file_common_geodata_geodat_proto_msgTypes[10] mi := &file_common_geodata_geodat_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -705,7 +757,7 @@ func (x *Domain_Attribute) String() string {
func (*Domain_Attribute) ProtoMessage() {} func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[10] mi := &file_common_geodata_geodat_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -807,7 +859,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
"\x05value\".\n" + "\x05value\".\n" +
"\x04CIDR\x12\x0e\n" + "\x04CIDR\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" + "\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"o\n" + "\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" +
"\bCIDRRule\x12-\n" +
"\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" +
"\x05GeoIP\x12\x12\n" + "\x05GeoIP\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" + "\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" + "\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
@@ -817,10 +872,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
"\tGeoIPRule\x12\x12\n" + "\tGeoIPRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" + "\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12#\n" + "\x04code\x18\x02 \x01(\tR\x04code\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"~\n" + "\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" +
"\x06IPRule\x126\n" + "\x06IPRule\x126\n" +
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x123\n" + "\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" +
"\x06custom\x18\x02 \x01(\v2\x19.xray.common.geodata.CIDRH\x00R\x06customB\a\n" + "\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" +
"\x05valueB[\n" + "\x05valueB[\n" +
"\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3" "\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3"
@@ -837,7 +892,7 @@ func file_common_geodata_geodat_proto_rawDescGZIP() []byte {
} }
var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_common_geodata_geodat_proto_goTypes = []any{ var file_common_geodata_geodat_proto_goTypes = []any{
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type (Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
(*Domain)(nil), // 1: xray.common.geodata.Domain (*Domain)(nil), // 1: xray.common.geodata.Domain
@@ -846,28 +901,30 @@ var file_common_geodata_geodat_proto_goTypes = []any{
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule (*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule (*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
(*CIDR)(nil), // 6: xray.common.geodata.CIDR (*CIDR)(nil), // 6: xray.common.geodata.CIDR
(*GeoIP)(nil), // 7: xray.common.geodata.GeoIP (*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule
(*GeoIPList)(nil), // 8: xray.common.geodata.GeoIPList (*GeoIP)(nil), // 8: xray.common.geodata.GeoIP
(*GeoIPRule)(nil), // 9: xray.common.geodata.GeoIPRule (*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList
(*IPRule)(nil), // 10: xray.common.geodata.IPRule (*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule
(*Domain_Attribute)(nil), // 11: xray.common.geodata.Domain.Attribute (*IPRule)(nil), // 11: xray.common.geodata.IPRule
(*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute
} }
var file_common_geodata_geodat_proto_depIdxs = []int32{ var file_common_geodata_geodat_proto_depIdxs = []int32{
0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type 0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type
11, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute 12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain 1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain
2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite 2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite
4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule 4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule
1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain 1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain
6, // 6: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR 6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR
7, // 7: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP 6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
9, // 8: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule 8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
6, // 9: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDR 10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
10, // [10:10] is the sub-list for method output_type 7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule
10, // [10:10] is the sub-list for method input_type 11, // [11:11] is the sub-list for method output_type
10, // [10:10] is the sub-list for extension type_name 11, // [11:11] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension extendee 11, // [11:11] is the sub-list for extension type_name
0, // [0:10] is the sub-list for field type_name 11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
} }
func init() { file_common_geodata_geodat_proto_init() } func init() { file_common_geodata_geodat_proto_init() }
@@ -879,11 +936,11 @@ func file_common_geodata_geodat_proto_init() {
(*DomainRule_Geosite)(nil), (*DomainRule_Geosite)(nil),
(*DomainRule_Custom)(nil), (*DomainRule_Custom)(nil),
} }
file_common_geodata_geodat_proto_msgTypes[9].OneofWrappers = []any{ file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
(*IPRule_Geoip)(nil), (*IPRule_Geoip)(nil),
(*IPRule_Custom)(nil), (*IPRule_Custom)(nil),
} }
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{ file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{
(*Domain_Attribute_BoolValue)(nil), (*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil), (*Domain_Attribute_IntValue)(nil),
} }
@@ -893,7 +950,7 @@ func file_common_geodata_geodat_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
NumEnums: 1, NumEnums: 1,
NumMessages: 11, NumMessages: 12,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+6 -1
View File
@@ -66,6 +66,11 @@ message CIDR {
uint32 prefix = 2; uint32 prefix = 2;
} }
message CIDRRule {
CIDR cidr = 1;
bool reverse_match = 2;
}
message GeoIP { message GeoIP {
string code = 1; string code = 1;
repeated CIDR cidr = 2; repeated CIDR cidr = 2;
@@ -85,6 +90,6 @@ message GeoIPRule {
message IPRule { message IPRule {
oneof value { oneof value {
GeoIPRule geoip = 1; GeoIPRule geoip = 1;
CIDR custom = 2; CIDRRule custom = 2;
} }
} }
+45 -18
View File
@@ -11,6 +11,7 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"go4.org/netipx" "go4.org/netipx"
) )
@@ -806,7 +807,7 @@ func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) {
type IPSetFactory struct { type IPSetFactory struct {
sync.Mutex sync.Mutex
shared map[string]*IPSet // TODO: cleanup shared *utils.WeakCacheMap[string, IPSet]
} }
func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) { func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) {
@@ -815,9 +816,11 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
if ipset := f.shared[key]; ipset != nil { if ipset, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil return ipset, nil
} }
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error { ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules { for _, r := range rules {
@@ -833,7 +836,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
return nil return nil
}) })
if err == nil { if err == nil {
f.shared[key] = ipset f.shared.Store(key, ipset)
} }
return ipset, err return ipset, err
} }
@@ -915,24 +918,31 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
return nil, errors.New("failed to build IPv6 set").Base(err) return nil, errors.New("failed to build IPv6 set").Base(err)
} }
var has4, has6 bool
var max4, max6 int var max4, max6 int
for _, p := range ipv4.Prefixes() { for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 { if b := p.Bits(); b > max4 {
max4 = b max4 = b
} }
} }
for _, p := range ipv6.Prefixes() { for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 { if b := p.Bits(); b > max6 {
max6 = b max6 = b
} }
} }
if max4 == 0 { if !has4 {
max4 = 0xff max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
} }
if max6 == 0 { if !has6 {
max6 = 0xff max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
} }
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
@@ -940,45 +950,58 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) { func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) {
n := len(rules) n := len(rules)
custom := make([]*CIDR, 0, n) posCustom := make([]*CIDR, 0, n)
pos := make([]*GeoIPRule, 0, n) negCustom := make([]*CIDR, 0, n)
neg := make([]*GeoIPRule, 0, n) posGeoip := make([]*GeoIPRule, 0, n)
negGeoip := make([]*GeoIPRule, 0, n)
for _, r := range rules { for _, r := range rules {
switch v := r.Value.(type) { switch v := r.Value.(type) {
case *IPRule_Custom: case *IPRule_Custom:
custom = append(custom, v.Custom) if !v.Custom.ReverseMatch {
posCustom = append(posCustom, v.Custom.Cidr)
} else {
negCustom = append(negCustom, v.Custom.Cidr)
}
case *IPRule_Geoip: case *IPRule_Geoip:
if !v.Geoip.ReverseMatch { if !v.Geoip.ReverseMatch {
pos = append(pos, v.Geoip) posGeoip = append(posGeoip, v.Geoip)
} else { } else {
neg = append(neg, v.Geoip) negGeoip = append(negGeoip, v.Geoip)
} }
default: default:
panic("unknown ip rule type") panic("unknown ip rule type")
} }
} }
subs := make([]*HeuristicIPMatcher, 0, 3) subs := make([]*HeuristicIPMatcher, 0, 4)
if len(custom) > 0 { if len(posCustom) > 0 {
ipset, err := f.CreateFromCIDRs(custom) ipset, err := f.CreateFromCIDRs(posCustom)
if err != nil { if err != nil {
return nil, err return nil, err
} }
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false}) subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
} }
if len(pos) > 0 { if len(negCustom) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(pos) ipset, err := f.CreateFromCIDRs(negCustom)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
}
if len(posGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip)
if err != nil { if err != nil {
return nil, err return nil, err
} }
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false}) subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
} }
if len(neg) > 0 { if len(negGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(neg) ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -994,3 +1017,7 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
return &HeuristicMultiIPMatcher{matchers: subs}, nil return &HeuristicMultiIPMatcher{matchers: subs}, nil
} }
} }
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: utils.NewWeakCacheMap[string, IPSet]()}
}
+112
View File
@@ -97,6 +97,90 @@ func TestIPMatcher(t *testing.T) {
} }
} }
func TestIPMatcherFullCIDR4(t *testing.T) {
matcher := buildIPMatcher(
"0.0.0.0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "0.0.0.0",
Output: true,
},
{
Input: "255.255.255.255",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "::0",
Output: false,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherFullCIDR6(t *testing.T) {
matcher := buildIPMatcher(
"::0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: false,
},
{
Input: "0.0.0.0",
Output: false,
},
{
Input: "255.255.255.255",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "::0",
Output: true,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: true,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherRegression(t *testing.T) { func TestIPMatcherRegression(t *testing.T) {
matcher := buildIPMatcher( matcher := buildIPMatcher(
"98.108.20.0/22", "98.108.20.0/22",
@@ -189,6 +273,34 @@ func TestIPReverseMatcher2(t *testing.T) {
} }
} }
func TestIPCustomReverseMatcher(t *testing.T) {
matcher := buildIPMatcher("!8.8.8.8/32")
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "1.1.1.1",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherAnyMatchAndMatches(t *testing.T) { func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
matcher := buildIPMatcher( matcher := buildIPMatcher(
"8.8.8.8/32", "8.8.8.8/32",
+120 -2
View File
@@ -1,17 +1,135 @@
package geodata package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
type IPRegistry struct { type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
} }
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) { func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
return buildOptimizedIPMatcher(r.ipsetFactory, rules) r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
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)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
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)")
return nil
} }
func newIPRegistry() *IPRegistry { func newIPRegistry() *IPRegistry {
return &IPRegistry{ return &IPRegistry{
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)}, ipsetFactory: newIPSetFactory(),
} }
} }
var IPReg = newIPRegistry() var IPReg = newIPRegistry()
type ipMatcherState struct {
matcher IPMatcher
}
type DynamicIPMatcher struct {
rules []*IPRule
state atomic.Pointer[ipMatcherState]
mu sync.Mutex
reverse bool
reverseSet bool
}
// Match implements IPMatcher.
func (d *DynamicIPMatcher) Match(ip net.IP) bool {
return d.state.Load().matcher.Match(ip)
}
// AnyMatch implements IPMatcher.
func (d *DynamicIPMatcher) AnyMatch(ips []net.IP) bool {
return d.state.Load().matcher.AnyMatch(ips)
}
// Matches implements IPMatcher.
func (d *DynamicIPMatcher) Matches(ips []net.IP) bool {
return d.state.Load().matcher.Matches(ips)
}
// FilterIPs implements IPMatcher.
func (d *DynamicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
return d.state.Load().matcher.FilterIPs(ips)
}
// ToggleReverse implements IPMatcher.
func (d *DynamicIPMatcher) ToggleReverse() {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = !d.reverse
d.state.Load().matcher.ToggleReverse()
}
// SetReverse implements IPMatcher.
func (d *DynamicIPMatcher) SetReverse(reverse bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = reverse
d.reverseSet = true
d.state.Load().matcher.SetReverse(reverse)
}
func (d *DynamicIPMatcher) Reload(newMatcher IPMatcher) {
d.mu.Lock()
defer d.mu.Unlock()
if d.reverseSet {
newMatcher.SetReverse(d.reverse)
} else if d.reverse {
newMatcher.ToggleReverse()
}
d.state.Store(&ipMatcherState{matcher: newMatcher})
}
func NewDynamicIPMatcher(rules []*IPRule, matcher IPMatcher) *DynamicIPMatcher {
d := &DynamicIPMatcher{rules: rules}
d.Reload(matcher)
return d
}
+21 -10
View File
@@ -17,6 +17,8 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
var ipRules []*IPRule var ipRules []*IPRule
for i, r := range rules { for i, r := range rules {
r, reverse := cutReversePrefix(r)
if strings.HasPrefix(r, "geoip:") { if strings.HasPrefix(r, "geoip:") {
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):] r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
} }
@@ -32,9 +34,9 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
var rule isIPRule_Value var rule isIPRule_Value
var err error var err error
if prefix > 0 { if prefix > 0 {
rule, err = parseGeoIPRule(r[prefix:]) rule, err = parseGeoIPRule(r[prefix:], reverse)
} else { } else {
rule, err = parseCustomIPRule(r) rule, err = parseCustomIPRule(r, reverse)
} }
if err != nil { if err != nil {
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err) return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
@@ -45,7 +47,16 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
return ipRules, nil return ipRules, nil
} }
func parseGeoIPRule(rule string) (*IPRule_Geoip, error) { func cutReversePrefix(s string) (string, bool) {
reverse := false
for strings.HasPrefix(s, "!") {
s = s[1:]
reverse = !reverse
}
return s, reverse
}
func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
file, code, ok := strings.Cut(rule, ":") file, code, ok := strings.Cut(rule, ":")
if !ok { if !ok {
return nil, errors.New("syntax error") return nil, errors.New("syntax error")
@@ -55,11 +66,8 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
return nil, errors.New("empty file") return nil, errors.New("empty file")
} }
reverse := false code, codeReverse := cutReversePrefix(code)
if strings.HasPrefix(code, "!") { reverse = reverse != codeReverse
code = code[1:]
reverse = true
}
if code == "" { if code == "" {
return nil, errors.New("empty code") return nil, errors.New("empty code")
} }
@@ -78,13 +86,16 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
}, nil }, nil
} }
func parseCustomIPRule(rule string) (*IPRule_Custom, error) { func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
cidr, err := parseCIDR(rule) cidr, err := parseCIDR(rule)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &IPRule_Custom{ return &IPRule_Custom{
Custom: cidr, Custom: &CIDRRule{
Cidr: cidr,
ReverseMatch: reverse,
},
}, nil }, nil
} }
+55
View File
@@ -13,12 +13,20 @@ func TestParseIPRules(t *testing.T) {
rules := []string{ rules := []string{
"geoip:us", "geoip:us",
"geoip:cn", "geoip:cn",
"!geoip:cn",
"!!geoip:cn",
"geoip:!cn", "geoip:!cn",
"geoip:!!cn",
"!geoip:!cn",
"ext:geoip.dat:!cn", "ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn",
"ext:geoip.dat:ca", "ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn", "ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ca", "ext-ip:geoip.dat:!ca",
"192.168.0.0/24", "192.168.0.0/24",
"!192.168.0.0/24",
"!!192.168.0.0/24",
"!!!192.168.0.0/24",
"192.168.0.1", "192.168.0.1",
"fe80::/64", "fe80::/64",
"fe80::", "fe80::",
@@ -30,6 +38,53 @@ func TestParseIPRules(t *testing.T) {
} }
} }
func TestParseIPRuleReverse(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, tt := range []struct {
rule string
reverse bool
}{
{rule: "!192.168.0.0/24", reverse: true},
{rule: "!!192.168.0.0/24", reverse: false},
{rule: "!!!192.168.0.0/24", reverse: true},
{rule: "!!!!192.168.0.0/24", reverse: false},
{rule: "geoip:cn", reverse: false},
{rule: "!geoip:cn", reverse: true},
{rule: "!!geoip:cn", reverse: false},
{rule: "geoip:!cn", reverse: true},
{rule: "geoip:!!cn", reverse: false},
{rule: "!geoip:!cn", reverse: false},
{rule: "!!geoip:!cn", reverse: true},
{rule: "!geoip:!!cn", reverse: true},
{rule: "ext:geoip.dat:!!!cn", reverse: true},
} {
t.Run(tt.rule, func(t *testing.T) {
rules, err := geodata.ParseIPRules([]string{tt.rule})
if err != nil {
t.Fatalf("Failed to parse ip rules, got %s", err)
}
if len(rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(rules))
}
switch rule := rules[0]; {
case rule.GetGeoip() != nil:
if rule.GetGeoip().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected geoip reverse match to be %t", tt.reverse)
}
case rule.GetCustom() != nil:
if rule.GetCustom().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected custom reverse match to be %t", tt.reverse)
}
default:
t.Fatal("Expected ip rule")
}
})
}
}
func TestParseDomainRules(t *testing.T) { func TestParseDomainRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
@@ -0,0 +1,53 @@
package strmatcher
// LinearAnyMatcher is an implementation of AnyMatcher.
type LinearAnyMatcher struct {
full *FullMatcherSet
domain *DomainMatcherSet
substr *SubstrMatcherSet
regex *SimpleMatcherSet
}
func NewLinearAnyMatcher() *LinearAnyMatcher {
return new(LinearAnyMatcher)
}
// Add implements AnyMatcher.Add.
func (s *LinearAnyMatcher) Add(matcher Matcher) {
switch matcher := matcher.(type) {
case FullMatcher:
if s.full == nil {
s.full = NewFullMatcherSet()
}
s.full.AddFullMatcher(matcher)
case DomainMatcher:
if s.domain == nil {
s.domain = NewDomainMatcherSet()
}
s.domain.AddDomainMatcher(matcher)
case SubstrMatcher:
if s.substr == nil {
s.substr = new(SubstrMatcherSet)
}
s.substr.AddSubstrMatcher(matcher)
default:
if s.regex == nil {
s.regex = new(SimpleMatcherSet)
}
s.regex.AddMatcher(matcher)
}
}
// MatchAny implements AnyMatcher.MatchAny.
func (s *LinearAnyMatcher) MatchAny(input string) bool {
if s.full != nil && s.full.MatchAny(input) {
return true
}
if s.domain != nil && s.domain.MatchAny(input) {
return true
}
if s.substr != nil && s.substr.MatchAny(input) {
return true
}
return s.regex != nil && s.regex.MatchAny(input)
}
+64 -6
View File
@@ -3,6 +3,7 @@ package strmatcher
import ( import (
"errors" "errors"
"regexp" "regexp"
"slices"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -99,10 +100,6 @@ func (t Type) New(pattern string) (Matcher, error) {
case Substr: case Substr:
return SubstrMatcher(pattern), nil return SubstrMatcher(pattern), nil
case Domain: case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern) regex, err := regexp.Compile(pattern)
@@ -253,13 +250,12 @@ func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error {
} }
// CompositeMatches flattens the matches slice to produce a single matched indices slice. // CompositeMatches flattens the matches slice to produce a single matched indices slice.
// It is designed to avoid new memory allocation as possible.
func CompositeMatches(matches [][]uint32) []uint32 { func CompositeMatches(matches [][]uint32) []uint32 {
switch len(matches) { switch len(matches) {
case 0: case 0:
return nil return nil
case 1: case 1:
return matches[0] return slices.Clone(matches[0])
default: default:
result := make([]uint32, 0, 5) result := make([]uint32, 0, 5)
for i := 0; i < len(matches); i++ { for i := 0; i < len(matches); i++ {
@@ -288,3 +284,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
return result return result
} }
} }
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
type MatcherSetForAll interface {
AddMatcher(matcher Matcher)
}
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
type MatcherSetForFull interface {
AddFullMatcher(matcher FullMatcher)
}
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
type MatcherSetForDomain interface {
AddDomainMatcher(matcher DomainMatcher)
}
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
type MatcherSetForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher)
}
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
type MatcherSetForRegex interface {
AddRegexMatcher(matcher *RegexMatcher)
}
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
// It returns error if the MatcherSet does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherSet.
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
if s, ok := s.(IndexMatcher); ok {
s.Add(matcher)
return nil
}
if s, ok := s.(MatcherSetForAll); ok {
s.AddMatcher(matcher)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if s, ok := s.(MatcherSetForFull); ok {
s.AddFullMatcher(matcher)
return nil
}
case DomainMatcher:
if s, ok := s.(MatcherSetForDomain); ok {
s.AddDomainMatcher(matcher)
return nil
}
case SubstrMatcher:
if s, ok := s.(MatcherSetForSubstr); ok {
s.AddSubstrMatcher(matcher)
return nil
}
case *RegexMatcher:
if s, ok := s.(MatcherSetForRegex); ok {
s.AddRegexMatcher(matcher)
return nil
}
}
return errors.New("cannot add matcher to matcher set")
}
@@ -0,0 +1,79 @@
package strmatcher
type trieNode2 struct {
matched bool
children map[string]*trieNode2
}
// DomainMatcherSet is an implementation of MatcherSet.
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
type DomainMatcherSet struct {
root *trieNode2
}
func NewDomainMatcherSet() *DomainMatcherSet {
return &DomainMatcherSet{
root: new(trieNode2),
}
}
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
node := s.root
pattern := matcher.Pattern()
for i := len(pattern); i > 0; {
var part string
for j := i - 1; ; j-- {
if pattern[j] == '.' {
part = pattern[j+1 : i]
i = j
break
}
if j == 0 {
part = pattern[j:i]
i = j
break
}
}
if node.children == nil {
node.children = make(map[string]*trieNode2)
}
next := node.children[part]
if next == nil {
next = new(trieNode2)
node.children[part] = next
}
node = next
}
node.matched = true
}
// MatchAny implements MatcherSet.MatchAny.
func (s *DomainMatcherSet) MatchAny(input string) bool {
node := s.root
for i := len(input); i > 0; {
for j := i - 1; ; j-- {
if input[j] == '.' {
node = node.children[input[j+1:i]]
i = j
break
}
if j == 0 {
node = node.children[input[j:i]]
i = j
break
}
}
if node == nil {
return false
}
if node.matched {
return true
}
if node.children == nil {
return false
}
}
return false
}
@@ -0,0 +1,95 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestDomainMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "a.b.com",
},
{
Pattern: "c.a.b.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "x.example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "a.b.com",
Result: true,
},
{
Domain: "c.a.b.com",
Result: true,
},
{
Domain: "c.a..b.com",
Result: false,
},
{
Domain: ".com",
Result: false,
},
{
Domain: "com",
Result: false,
},
{
Domain: "",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewDomainMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyDomainMatcherSet(t *testing.T) {
s := NewDomainMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -0,0 +1,24 @@
package strmatcher
// FullMatcherSet is an implementation of MatcherSet.
// It uses a hash table to facilitate exact match lookup.
type FullMatcherSet struct {
matchers map[string]struct{}
}
func NewFullMatcherSet() *FullMatcherSet {
return &FullMatcherSet{
matchers: make(map[string]struct{}),
}
}
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
s.matchers[matcher.Pattern()] = struct{}{}
}
// MatchAny implements MatcherSet.Any.
func (s *FullMatcherSet) MatchAny(input string) bool {
_, found := s.matchers[input]
return found
}
@@ -0,0 +1,65 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestFullMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewFullMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyFullMatcherSet(t *testing.T) {
s := NewFullMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -0,0 +1,22 @@
package strmatcher
// SimpleMatcherSet is an implementation of MatcherSet.
// It simply stores all matchers in an array and sequentially matches them.
type SimpleMatcherSet struct {
matchers []Matcher
}
// AddMatcher implements MatcherSetForAll.AddMatcher.
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
s.matchers = append(s.matchers, matcher)
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SimpleMatcherSet) MatchAny(input string) bool {
for _, m := range s.matchers {
if m.Match(input) {
return true
}
}
return false
}
@@ -0,0 +1,69 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSimpleMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "example.com",
mType: Domain,
},
{
pattern: "example.com",
mType: Full,
},
{
pattern: "example.com",
mType: Regex,
},
}
cases := []struct {
input string
output bool
}{
{
input: "www.example.com",
output: true,
},
{
input: "example.com",
output: true,
},
{
input: "www.e3ample.com",
output: false,
},
{
input: "xample.com",
output: false,
},
{
input: "xexample.com",
output: true,
},
{
input: "examplexcom",
output: true,
},
}
matcherSet := &SimpleMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
@@ -0,0 +1,24 @@
package strmatcher
import "strings"
// SubstrMatcherSet is implementation of MatcherSet,
// It is simply implmeneted to comply with the priority specification of Substr matchers.
type SubstrMatcherSet struct {
patterns []string
}
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
s.patterns = append(s.patterns, matcher.Pattern())
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SubstrMatcherSet) MatchAny(input string) bool {
for _, pattern := range s.patterns {
if strings.Contains(input, pattern) {
return true
}
}
return false
}
@@ -0,0 +1,77 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSubstrMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "apis",
mType: Substr,
},
{
pattern: "google",
mType: Substr,
},
{
pattern: "apis",
mType: Substr,
},
}
cases := []struct {
input string
output bool
}{
{
input: "google.com",
output: true,
},
{
input: "apis.com",
output: true,
},
{
input: "googleapis.com",
output: true,
},
{
input: "fonts.googleapis.com",
output: true,
},
{
input: "apis.googleapis.com",
output: true,
},
{
input: "baidu.com",
output: false,
},
{
input: "goog",
output: false,
},
{
input: "api",
output: false,
},
}
matcherSet := &SubstrMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
+21 -1
View File
@@ -15,7 +15,7 @@ const (
) )
// Matcher is the interface to determine a string matches a pattern. // Matcher is the interface to determine a string matches a pattern.
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex). // - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
type Matcher interface { type Matcher interface {
// Type returns the matcher's type. // Type returns the matcher's type.
Type() Type Type() Type
@@ -62,6 +62,7 @@ type IndexMatcher interface {
// Match returns the indices of all matchers that matches the input. // Match returns the indices of all matchers that matches the input.
// * Empty array is returned if no such matcher exists. // * Empty array is returned if no such matcher exists.
// * The order of returned matchers should follow priority specification. // * The order of returned matchers should follow priority specification.
// * The returned slice is owned by the caller and may be safely modified.
// Priority specification: // Priority specification:
// 1. Priority between matcher types: full > domain > substr > regex. // 1. Priority between matcher types: full > domain > substr > regex.
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence. // 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
@@ -89,6 +90,7 @@ type ValueMatcher interface {
// * Empty array is returned if no such matcher exists. // * Empty array is returned if no such matcher exists.
// * The order of returned values should follow priority specification. // * The order of returned values should follow priority specification.
// * Same value may appear multiple times if multiple matched matchers were added with that value. // * Same value may appear multiple times if multiple matched matchers were added with that value.
// * The returned slice is owned by the caller and may be safely modified.
// Priority specification: // Priority specification:
// 1. Priority between matcher types: full > domain > substr > regex. // 1. Priority between matcher types: full > domain > substr > regex.
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence. // 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
@@ -99,3 +101,21 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found. // MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool MatchAny(input string) bool
} }
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherSet interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
type AnyMatcher interface {
// Add adds a new Matcher to AnyMatcher.
Add(matcher Matcher)
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
+40 -2
View File
@@ -1,6 +1,7 @@
package filesystem package filesystem
import ( import (
"errors"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -26,11 +27,48 @@ func ReadFile(path string) ([]byte, error) {
} }
func ReadAsset(file string) ([]byte, error) { func ReadAsset(file string) ([]byte, error) {
return ReadFile(platform.GetAssetLocation(file)) path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
} }
func OpenAsset(file string) (io.ReadCloser, error) { func OpenAsset(file string) (io.ReadCloser, error) {
return NewFileReader(platform.GetAssetLocation(file)) path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file)
return path, err
}
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
if !filepath.IsLocal(file) || file == "." {
return "", nil, errors.New("asset path must stay in asset directory")
}
local, err := filepath.Localize(file)
if err != nil {
return "", nil, err
}
path := platform.GetAssetLocation(local)
info, err := os.Stat(path)
if err != nil {
return "", nil, err
}
if !info.Mode().IsRegular() {
return "", nil, errors.New("asset is not a regular file")
}
return path, info, nil
} }
func ReadCert(file string) ([]byte, error) { func ReadCert(file string) ([]byte, error) {
+32
View File
@@ -0,0 +1,32 @@
package filesystem_test
import (
"path/filepath"
"testing"
. "github.com/xtls/xray-core/common/platform/filesystem"
)
func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{
"",
".",
"..",
"../geoip.dat",
"nested/..",
"nested/../geoip.dat",
"nested//geoip.dat",
"/geoip.dat",
"/tmp/geoip.dat",
`C:\geoip.dat`,
`C:geoip.dat`,
`\\server\share\geoip.dat`,
`nested\geoip.dat`,
`nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"),
} {
if _, err := StatAsset(file); err == nil {
t.Fatalf("expected error for %q", file)
}
}
}
+1
View File
@@ -17,6 +17,7 @@ const (
UseFreedomSplice = "xray.buf.splice" UseFreedomSplice = "xray.buf.splice"
UseVmessPadding = "xray.vmess.padding" UseVmessPadding = "xray.vmess.padding"
UseCone = "xray.cone.disabled" UseCone = "xray.cone.disabled"
UseStrictJSON = "xray.json.strict"
BufferSize = "xray.ray.buffer.size" BufferSize = "xray.ray.buffer.size"
BrowserDialerAddress = "xray.browser.dialer" BrowserDialerAddress = "xray.browser.dialer"
+29
View File
@@ -4,6 +4,7 @@ import (
"testing" "testing"
. "github.com/xtls/xray-core/common/serial" . "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
) )
func TestGetInstance(t *testing.T) { func TestGetInstance(t *testing.T) {
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
t.Error("expect nil, but actually not") t.Error("expect nil, but actually not")
} }
} }
func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) {
msg := &custom.UDPConfig{
Mode: "standalone",
Client: []*custom.UDPItem{
{Rand: 12, Save: "txid"},
},
}
tm := ToTypedMessage(msg)
if tm == nil {
t.Fatal("expected typed message")
}
roundTrip, err := tm.GetInstance()
if err != nil {
t.Fatalf("GetInstance() failed: %v", err)
}
udp, ok := roundTrip.(*custom.UDPConfig)
if !ok {
t.Fatalf("unexpected round-trip type: %T", roundTrip)
}
if udp.GetMode() != "standalone" {
t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode())
}
}
+43
View File
@@ -0,0 +1,43 @@
package task
import (
"runtime"
"golang.org/x/sync/errgroup"
)
// ParallelForN runs fn(0..n-1) in parallel across runtime.GOMAXPROCS(0) worker
// goroutines. Indices are partitioned into contiguous chunks so the number of
// spawned goroutines stays bounded regardless of n.
//
// fn must be safe to call concurrently from different goroutines (each call
// receives its own unique index). Output collected by writing to indexed slots
// in a pre-allocated slice is a common safe pattern.
//
// Returns the first non-nil error reported by fn; other workers may still be
// finishing briefly afterwards.
func ParallelForN(n int, fn func(i int) error) error {
if n <= 0 {
return nil
}
workers := max(runtime.GOMAXPROCS(0), 1)
workers = min(workers, n)
chunk := (n + workers - 1) / workers
var eg errgroup.Group
for w := range workers {
start := w * chunk
end := min(start+chunk, n)
if start >= end {
break
}
eg.Go(func() error {
for i := start; i < end; i++ {
if err := fn(i); err != nil {
return err
}
}
return nil
})
}
return eg.Wait()
}
+50
View File
@@ -0,0 +1,50 @@
package task_test
import (
"errors"
"sync/atomic"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/task"
)
func TestParallelForN_Empty(t *testing.T) {
called := false
err := ParallelForN(0, func(i int) error {
called = true
return nil
})
common.Must(err)
if called {
t.Fatal("fn should not be called when n=0")
}
}
func TestParallelForN_AllIndicesCovered(t *testing.T) {
const N = 10000
var seen [N]int32
err := ParallelForN(N, func(i int) error {
atomic.AddInt32(&seen[i], 1)
return nil
})
common.Must(err)
for i := 0; i < N; i++ {
if seen[i] != 1 {
t.Fatalf("index %d called %d times, expected 1", i, seen[i])
}
}
}
func TestParallelForN_Error(t *testing.T) {
boom := errors.New("boom")
err := ParallelForN(1000, func(i int) error {
if i == 42 {
return boom
}
return nil
})
if err != boom {
t.Fatalf("expected %v, got %v", boom, err)
}
}
+73
View File
@@ -0,0 +1,73 @@
package utils
import (
"net"
"os"
"runtime"
"sync"
"time"
)
func probeRoutes() (ipv4 bool, ipv6 bool) {
if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil {
ipv4 = true
conn.Close()
}
if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil {
ipv6 = true
conn.Close()
}
return
}
var routeCache struct {
sync.Once
sync.RWMutex
expire time.Time
ipv4, ipv6 bool
}
func CheckRoutes() (bool, bool) {
if !isGUIPlatform {
routeCache.Once.Do(func() {
routeCache.ipv4, routeCache.ipv6 = probeRoutes()
})
return routeCache.ipv4, routeCache.ipv6
}
routeCache.RWMutex.RLock()
now := time.Now()
if routeCache.expire.After(now) {
routeCache.RWMutex.RUnlock()
return routeCache.ipv4, routeCache.ipv6
}
routeCache.RWMutex.RUnlock()
routeCache.RWMutex.Lock()
defer routeCache.RWMutex.Unlock()
now = time.Now()
if routeCache.expire.After(now) { // double-check
return routeCache.ipv4, routeCache.ipv6
}
routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms
routeCache.expire = now.Add(100 * time.Millisecond) // ttl
return routeCache.ipv4, routeCache.ipv6
}
var isGUIPlatform = detectGUIPlatform()
func detectGUIPlatform() bool {
switch runtime.GOOS {
case "android", "ios", "windows", "darwin":
return true
case "linux", "freebsd", "openbsd":
if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" {
return true
}
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
return true
}
}
return false
}
+45
View File
@@ -0,0 +1,45 @@
package utils
import (
"runtime"
"sync"
"weak"
)
// WeakCacheMap is a map that holds weak references to values.
// Use for shared expensive objects and automatic cleanup when no longer used.
// This object can be GC and no goroutine is used for cleanup.
type WeakCacheMap[K comparable, V any] struct {
mu sync.Mutex
m map[K]weak.Pointer[V]
}
func NewWeakCacheMap[K comparable, V any]() *WeakCacheMap[K, V] {
return &WeakCacheMap[K, V]{
m: make(map[K]weak.Pointer[V]),
}
}
func (c *WeakCacheMap[K, V]) Load(key K) (value *V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
weakPtr := c.m[key].Value()
if weakPtr != nil {
return weakPtr, true
}
return nil, false
}
func (c *WeakCacheMap[K, V]) Store(key K, value *V) {
c.mu.Lock()
defer c.mu.Unlock()
weakPtr := weak.Make(value)
c.m[key] = weakPtr
runtime.AddCleanup(value, func(struct{}) {
c.mu.Lock()
defer c.mu.Unlock()
if c.m[key] == weakPtr {
delete(c.m, key)
}
}, struct{}{})
}
+1 -1
View File
@@ -53,7 +53,7 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
return return
} }
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP && if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun" || inbound.Name == "wireguard") { (inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
h := blake3.New(8, BaseKey) h := blake3.New(8, BaseKey)
h.Write([]byte(inbound.Source.String())) h.Write([]byte(inbound.Source.String()))
copy(globalID[:], h.Sum(nil)) copy(globalID[:], h.Sum(nil))
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 4 Version_y byte = 5
Version_z byte = 13 Version_z byte = 9
) )
var ( var (
+9 -3
View File
@@ -52,7 +52,9 @@ func TestXrayDial(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -102,7 +104,9 @@ func TestXrayDialUDPConn(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -169,7 +173,9 @@ func TestXrayDialUDP(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
+4 -4
View File
@@ -54,9 +54,9 @@ func TestXrayClose(t *testing.T) {
Listen: net.NewIPOrDomain(net.LocalHostIP), Listen: net.NewIPOrDomain(net.LocalHostIP),
}), }),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP), RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(0), RewritePort: uint32(0),
Networks: []net.Network{net.Network_TCP}, AllowedNetworks: []net.Network{net.Network_TCP},
}), }),
}, },
}, },
@@ -66,7 +66,7 @@ func TestXrayClose(t *testing.T) {
Receiver: &protocol.ServerEndpoint{ Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP), Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(0), Port: uint32(0),
User: &protocol.User{ User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{ Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(), Id: userID.String(),
}), }),
+7 -3
View File
@@ -80,13 +80,17 @@ func New() *Client {
d := &net.Dialer{ d := &net.Dialer{
Timeout: time.Second * 16, Timeout: time.Second * 16,
Control: func(network, address string, c syscall.RawConn) error { Control: func(network, address string, c syscall.RawConn) error {
var errs []error
for _, ctl := range internet.Controllers { for _, ctl := range internet.Controllers {
if err := ctl(network, address, c); err != nil { if err := ctl(network, address, c); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller") errs = append(errs, err)
return err
} }
} }
return nil err := errors.Combine(errs...)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
}
return err
}, },
} }
+6 -5
View File
@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
go 1.26 go 1.26
require ( require (
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.3 github.com/cloudflare/circl v1.6.3
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
@@ -12,8 +12,9 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 github.com/klauspost/cpuid/v2 v2.3.0
github.com/miekg/dns v1.1.72 github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.11.0 github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1 github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
@@ -27,8 +28,8 @@ require (
golang.org/x/sys v0.43.0 golang.org/x/sys v0.43.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v0.5.3 golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.80.0 google.golang.org/grpc v1.81.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3 h12.io/socks v1.0.3
@@ -49,7 +50,7 @@ require (
golang.org/x/text v0.36.0 // indirect golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.43.0 // indirect golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+22 -20
View File
@@ -1,7 +1,7 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY= github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= 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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
@@ -45,14 +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/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
@@ -70,16 +72,16 @@ github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt7
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
@@ -131,14 +133,14 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+146 -17
View File
@@ -1,38 +1,167 @@
package conf package conf
import ( import (
"strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/proxy/dns" "github.com/xtls/xray-core/proxy/dns"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
type DNSOutboundRuleConfig struct {
Action string `json:"action"`
QType *PortList `json:"qtype"`
Domain *StringList `json:"domain"`
}
func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
rule := &dns.DNSRuleConfig{}
switch strings.ToLower(c.Action) {
case "direct":
rule.Action = dns.RuleAction_Direct
case "drop":
rule.Action = dns.RuleAction_Drop
case "reject":
rule.Action = dns.RuleAction_Reject
case "hijack":
rule.Action = dns.RuleAction_Hijack
default:
return nil, errors.New("unknown action: ", c.Action)
}
if c.QType != nil {
for _, r := range c.QType.Range {
if r.From > r.To {
return nil, errors.New("invalid qtype range: ", r.String())
}
if r.To > 65535 {
return nil, errors.New("dns rule qtype out of range: ", r.String())
}
for qtype := r.From; qtype <= r.To; qtype++ {
rule.Qtype = append(rule.Qtype, int32(qtype))
}
}
}
if c.Domain != nil {
rules, err := geodata.ParseDomainRules(*c.Domain, geodata.Domain_Substr)
if err != nil {
return nil, err
}
rule.Domain = rules
}
return rule, nil
}
type DNSOutboundConfig struct { type DNSOutboundConfig struct {
Network Network `json:"network"` RewriteNetwork Network `json:"rewriteNetwork"`
Address *Address `json:"address"` RewriteAddress *Address `json:"rewriteAddress"`
Port uint16 `json:"port"` RewritePort uint16 `json:"rewritePort"`
UserLevel uint32 `json:"userLevel"` Network Network `json:"network"`
NonIPQuery string `json:"nonIPQuery"` Address *Address `json:"address"`
BlockTypes []int32 `json:"blockTypes"` Port uint16 `json:"port"`
UserLevel uint32 `json:"userLevel"`
Rules []*DNSOutboundRuleConfig `json:"rules"`
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
} }
func (c *DNSOutboundConfig) Build() (proto.Message, error) { func (c *DNSOutboundConfig) Build() (proto.Message, error) {
if len(c.Network) > 0 {
c.RewriteNetwork = c.Network
}
if c.Address != nil {
c.RewriteAddress = c.Address
}
if c.Port != 0 {
c.RewritePort = c.Port
}
config := &dns.Config{ config := &dns.Config{
Server: &net.Endpoint{ RewriteServer: &net.Endpoint{
Network: c.Network.Build(), Network: c.RewriteNetwork.Build(),
Port: uint32(c.Port), Port: uint32(c.RewritePort),
}, },
UserLevel: c.UserLevel, UserLevel: c.UserLevel,
} }
if c.Address != nil { if c.RewriteAddress != nil {
config.Server.Address = c.Address.Build() config.RewriteServer.Address = c.RewriteAddress.Build()
} }
switch c.NonIPQuery {
case "", "reject", "drop", "skip": // todo: remove legacy
default: if c.NonIPQuery != nil || c.BlockTypes != nil {
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery) if c.Rules != nil {
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
}
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes"`, `"rules"`)
rules, err := c.buildLegacyDNSPolicy()
if err != nil {
return nil, err
}
config.Rule = rules
return config, nil
} }
config.Non_IPQuery = c.NonIPQuery
config.BlockTypes = c.BlockTypes for _, r := range c.Rules {
rule, err := r.Build()
if err != nil {
return nil, err
}
config.Rule = append(config.Rule, rule)
}
return config, nil return config, nil
} }
// todo: remove legacy
func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) {
rules := make([]*dns.DNSRuleConfig, 0, 3)
mode := "reject"
if c.NonIPQuery != nil && *c.NonIPQuery != "" {
mode = *c.NonIPQuery
}
switch mode {
case "", "reject", "drop", "skip":
default:
return nil, errors.New("unknown nonIPQuery: ", mode)
}
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
if mode == "reject" {
rule.Action = dns.RuleAction_Reject
}
for _, qtype := range *c.BlockTypes {
if qtype < 0 || qtype > 65535 {
return nil, errors.New("legacy blockTypes qtype out of range: ", qtype)
}
rule.Qtype = append(rule.Qtype, qtype)
}
rules = append(rules, rule)
}
{
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack}
rule.Qtype = append(rule.Qtype, 1)
rule.Qtype = append(rule.Qtype, 28)
rules = append(rules, rule)
}
{
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject}
if mode == "reject" {
rule.Action = dns.RuleAction_Reject
} else if mode == "drop" {
rule.Action = dns.RuleAction_Drop
} else if mode == "skip" {
rule.Action = dns.RuleAction_Direct
}
rules = append(rules, rule)
}
return rules, nil
}
+206 -1
View File
@@ -1,8 +1,10 @@
package conf_test package conf_test
import ( import (
"strings"
"testing" "testing"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/dns" "github.com/xtls/xray-core/proxy/dns"
@@ -22,12 +24,215 @@ func TestDnsProxyConfig(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
Server: &net.Endpoint{ RewriteServer: &net.Endpoint{
Network: net.Network_TCP, Network: net.Network_TCP,
Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})), Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})),
Port: 53, Port: 53,
}, },
}, },
}, },
{
Input: `{
"rules": [{
"action": "direct",
"qtype": "1,3,23-24"
}, {
"action": "drop",
"qtype": 28,
"domain": ["domain:example.com", "full:example.com"]
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Direct,
Qtype: []int32{1, 3, 23, 24},
},
{
Action: dns.RuleAction_Drop,
Qtype: []int32{28},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Domain,
Value: "example.com",
},
},
},
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Full,
Value: "example.com",
},
},
},
},
},
},
},
},
{
Input: `{
"rules": [{
"action": "reject",
"domain": "keyword:example"
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Reject,
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Substr,
Value: "example",
},
},
},
},
},
},
},
},
{
Input: `{
"rules": [{
"action": "drop",
"qtype": 257
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Drop,
Qtype: []int32{257},
},
},
},
},
}) })
} }
// todo: remove legacy
func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
creator := func() Buildable {
return new(DNSOutboundConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"blockTypes": []
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Reject,
},
},
},
},
{
Input: `{
"blockTypes": [1, 65]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Reject,
Qtype: []int32{1, 65},
},
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Reject,
},
},
},
},
{
Input: `{
"nonIPQuery": "drop",
"blockTypes": [1]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Drop,
Qtype: []int32{1},
},
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Drop,
},
},
},
},
{
Input: `{
"nonIPQuery": "skip",
"blockTypes": [65, 28]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Drop,
Qtype: []int32{65, 28},
},
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Direct,
},
},
},
},
})
}
// todo: remove legacy
func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) {
creator := func() Buildable {
return new(DNSOutboundConfig)
}
_, err := loadJSON(creator)(`{
"rules": [{
"action": "direct",
"qtype": 65
}],
"blockTypes": [65]
}`)
if err == nil || !strings.Contains(err.Error(), `legacy nonIPQuery and blockTypes cannot be mixed with rules`) {
t.Fatal("expected mixed legacy/new config error, but got ", err)
}
}
+18 -6
View File
@@ -8,27 +8,39 @@ import (
) )
type DokodemoConfig struct { type DokodemoConfig struct {
AllowedNetwork *NetworkList `json:"allowedNetwork"`
RewriteAddress *Address `json:"rewriteAddress"`
RewritePort uint16 `json:"rewritePort"`
Network *NetworkList `json:"network"`
Address *Address `json:"address"` Address *Address `json:"address"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
PortMap map[string]string `json:"portMap"` PortMap map[string]string `json:"portMap"`
Network *NetworkList `json:"network"`
FollowRedirect bool `json:"followRedirect"` FollowRedirect bool `json:"followRedirect"`
UserLevel uint32 `json:"userLevel"` UserLevel uint32 `json:"userLevel"`
} }
func (v *DokodemoConfig) Build() (proto.Message, error) { func (v *DokodemoConfig) Build() (proto.Message, error) {
config := new(dokodemo.Config) if v.Network != nil {
if v.Address != nil { v.AllowedNetwork = v.Network
config.Address = v.Address.Build()
} }
config.Port = uint32(v.Port) if v.Address != nil {
v.RewriteAddress = v.Address
}
if v.Port != 0 {
v.RewritePort = v.Port
}
config := new(dokodemo.Config)
config.AllowedNetworks = v.AllowedNetwork.Build()
if v.RewriteAddress != nil {
config.RewriteAddress = v.RewriteAddress.Build()
}
config.RewritePort = uint32(v.RewritePort)
config.PortMap = v.PortMap config.PortMap = v.PortMap
for _, v := range config.PortMap { for _, v := range config.PortMap {
if _, _, err := net.SplitHostPort(v); err != nil { if _, _, err := net.SplitHostPort(v); err != nil {
return nil, errors.New("invalid portMap: ", v).Base(err) return nil, errors.New("invalid portMap: ", v).Base(err)
} }
} }
config.Networks = v.Network.Build()
config.FollowRedirect = v.FollowRedirect config.FollowRedirect = v.FollowRedirect
config.UserLevel = v.UserLevel config.UserLevel = v.UserLevel
return config, nil return config, nil
+5 -5
View File
@@ -24,15 +24,15 @@ func TestDokodemoConfig(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dokodemo.Config{ Output: &dokodemo.Config{
Address: &net.IPOrDomain{ RewriteAddress: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{ Address: &net.IPOrDomain_Ip{
Ip: []byte{8, 8, 8, 8}, Ip: []byte{8, 8, 8, 8},
}, },
}, },
Port: 53, RewritePort: 53,
Networks: []net.Network{net.Network_TCP}, AllowedNetworks: []net.Network{net.Network_TCP},
FollowRedirect: true, FollowRedirect: true,
UserLevel: 1, UserLevel: 1,
}, },
}, },
}) })
+77 -11
View File
@@ -1,13 +1,15 @@
package conf package conf
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"net" "net"
"strings" "strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
v2net "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/geodata"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/proxy/freedom" "github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
@@ -15,14 +17,16 @@ import (
) )
type FreedomConfig struct { type FreedomConfig struct {
TargetStrategy string `json:"targetStrategy"` TargetStrategy string `json:"targetStrategy"`
DomainStrategy string `json:"domainStrategy"` DomainStrategy string `json:"domainStrategy"`
Redirect string `json:"redirect"` Redirect string `json:"redirect"`
UserLevel uint32 `json:"userLevel"` UserLevel uint32 `json:"userLevel"`
Fragment *Fragment `json:"fragment"` Fragment *Fragment `json:"fragment"`
Noise *Noise `json:"noise"` Noise *Noise `json:"noise"`
Noises []*Noise `json:"noises"` Noises []*Noise `json:"noises"`
ProxyProtocol uint32 `json:"proxyProtocol"` ProxyProtocol uint32 `json:"proxyProtocol"`
IPsBlocked *StringList `json:"ipsBlocked"`
FinalRules []*FreedomFinalRuleConfig `json:"finalRules"`
} }
type Fragment struct { type Fragment struct {
@@ -39,8 +43,21 @@ type Noise struct {
ApplyTo string `json:"applyTo"` ApplyTo string `json:"applyTo"`
} }
type FreedomFinalRuleConfig struct {
Action string `json:"action"`
Network *NetworkList `json:"network"`
Port *PortList `json:"port"`
IP *StringList `json:"ip"`
BlockDelay *Int32Range `json:"blockDelay"`
}
// Build implements Buildable // Build implements Buildable
func (c *FreedomConfig) Build() (proto.Message, error) { func (c *FreedomConfig) Build() (proto.Message, error) {
if c.IPsBlocked != nil {
// todo: remove legacy
errors.LogWarning(context.Background(), `The feature "ipsBlocked" has been removed and migrated to "finalRules". Please update your config(s) according to release note and documentation.`)
}
config := new(freedom.Config) config := new(freedom.Config)
targetStrategy := c.TargetStrategy targetStrategy := c.TargetStrategy
if targetStrategy == "" { if targetStrategy == "" {
@@ -140,12 +157,13 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
} }
config.UserLevel = c.UserLevel config.UserLevel = c.UserLevel
if len(c.Redirect) > 0 { if len(c.Redirect) > 0 {
host, portStr, err := net.SplitHostPort(c.Redirect) host, portStr, err := net.SplitHostPort(c.Redirect)
if err != nil { if err != nil {
return nil, errors.New("invalid redirect address: ", c.Redirect, ": ", err).Base(err) return nil, errors.New("invalid redirect address: ", c.Redirect, ": ", err).Base(err)
} }
port, err := v2net.PortFromString(portStr) port, err := xnet.PortFromString(portStr)
if err != nil { if err != nil {
return nil, errors.New("invalid redirect port: ", c.Redirect, ": ", err).Base(err) return nil, errors.New("invalid redirect port: ", c.Redirect, ": ", err).Base(err)
} }
@@ -156,12 +174,22 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
} }
if len(host) > 0 { if len(host) > 0 {
config.DestinationOverride.Server.Address = v2net.NewIPOrDomain(v2net.ParseAddress(host)) config.DestinationOverride.Server.Address = xnet.NewIPOrDomain(xnet.ParseAddress(host))
} }
} }
if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 { if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 {
config.ProxyProtocol = c.ProxyProtocol config.ProxyProtocol = c.ProxyProtocol
} }
for _, r := range c.FinalRules {
rule, err := r.Build()
if err != nil {
return nil, err
}
config.FinalRules = append(config.FinalRules, rule)
}
return config, nil return config, nil
} }
@@ -220,3 +248,41 @@ func ParseNoise(noise *Noise) (*freedom.Noise, error) {
} }
return NConfig, nil return NConfig, nil
} }
func (c *FreedomFinalRuleConfig) Build() (*freedom.FinalRuleConfig, error) {
rule := &freedom.FinalRuleConfig{}
switch strings.ToLower(c.Action) {
case "allow":
rule.Action = freedom.RuleAction_Allow
case "block":
rule.Action = freedom.RuleAction_Block
default:
return nil, errors.New("unknown action: ", c.Action)
}
if c.Network != nil {
rule.Networks = c.Network.Build()
}
if c.Port != nil {
rule.PortList = c.Port.Build()
}
if c.IP != nil {
rules, err := geodata.ParseIPRules(*c.IP)
if err != nil {
return nil, err
}
rule.Ip = rules
}
if c.BlockDelay != nil {
rule.BlockDelay = &freedom.Range{
Min: uint64(c.BlockDelay.From),
Max: uint64(c.BlockDelay.To),
}
}
return rule, nil
}
+60
View File
@@ -3,6 +3,7 @@ package conf_test
import ( import (
"testing" "testing"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
@@ -38,5 +39,64 @@ func TestFreedomConfig(t *testing.T) {
UserLevel: 1, UserLevel: 1,
}, },
}, },
{
Input: `{
"finalRules": [{
"action": "block",
"network": "tcp,udp",
"port": "53,443",
"ip": ["10.0.0.0/8", "2001:db8::/32"],
"blockDelay": "30-60"
}, {
"action": "allow",
"network": ["udp"]
}]
}`,
Parser: loadJSON(creator),
Output: &freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{
{
Action: freedom.RuleAction_Block,
Networks: []net.Network{net.Network_TCP, net.Network_UDP},
PortList: &net.PortList{
Range: []*net.PortRange{
{From: 53, To: 53},
{From: 443, To: 443},
},
},
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{
Ip: []byte{10, 0, 0, 0},
Prefix: 8,
},
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{
Ip: net.ParseAddress("2001:db8::").IP(),
Prefix: 32,
},
},
},
},
},
BlockDelay: &freedom.Range{
Min: 30,
Max: 60,
},
},
{
Action: freedom.RuleAction_Allow,
Networks: []net.Network{net.Network_UDP},
},
},
},
},
}) })
} }
+71
View File
@@ -0,0 +1,71 @@
package conf
import (
"net/url"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
type GeodataAssetConfig struct {
URL string `json:"url"`
File string `json:"file"`
}
func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
if err := validateHTTPS(c.URL); err != nil {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
}
if _, err := filesystem.StatAsset(c.File); err != nil {
return nil, errors.New("invalid geodata asset file: ", c.File).Base(err)
}
return &geodata.Asset{
Url: c.URL,
File: c.File,
}, nil
}
func validateHTTPS(s string) error {
u, err := url.ParseRequestURI(s)
if err != nil {
return err
}
if u.Scheme != "https" || u.Host == "" {
return errors.New("scheme must be https")
}
return nil
}
type GeodataConfig struct {
Cron *string `json:"cron"`
Outbound string `json:"outbound"`
Assets []*GeodataAssetConfig `json:"assets"`
}
func (c *GeodataConfig) Build() (proto.Message, error) {
config := &geodata.Config{}
if c.Cron != nil {
if _, err := cron.ParseStandard(*c.Cron); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
config.Cron = *c.Cron
}
config.Outbound = c.Outbound
assets := make([]*geodata.Asset, 0, len(c.Assets))
for _, asset := range c.Assets {
built, err := asset.Build()
if err != nil {
return nil, err
}
assets = append(assets, built)
}
config.Assets = assets
return config, nil
}
+75
View File
@@ -0,0 +1,75 @@
package conf_test
import (
"path/filepath"
"testing"
"github.com/xtls/xray-core/app/geodata"
. "github.com/xtls/xray-core/infra/conf"
)
func TestGeodataConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
creator := func() Buildable {
return new(GeodataConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"cron": "0 4 * * *",
"outbound": "proxy",
"assets": [
{"url": "https://example.com/geoip.dat", "file": "geoip.dat"},
{"url": "https://example.com/geosite.dat", "file": "geosite.dat"}
]
}`,
Parser: loadJSON(creator),
Output: &geodata.Config{
Cron: "0 4 * * *",
Outbound: "proxy",
Assets: []*geodata.Asset{
{Url: "https://example.com/geoip.dat", File: "geoip.dat"},
{Url: "https://example.com/geosite.dat", File: "geosite.dat"},
},
},
},
})
}
func TestGeodataAssetConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "geoip.dat",
}).Build(); err != nil {
t.Fatal(err)
}
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "missing.dat",
}).Build(); err == nil {
t.Fatal("expected error")
}
}
func TestGeodataAssetConfigInvalidURL(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, rawURL := range []string{
"",
"http://example.com/geoip.dat",
"ftp://example.com/geoip.dat",
"https:///geoip.dat",
} {
if _, err := (&GeodataAssetConfig{
URL: rawURL,
File: "geoip.dat",
}).Build(); err == nil {
t.Fatalf("expected error for %q", rawURL)
}
}
}
+9 -4
View File
@@ -23,6 +23,7 @@ func (v *HTTPAccount) Build() *http.Account {
} }
type HTTPServerConfig struct { type HTTPServerConfig struct {
Users []*HTTPAccount `json:"users"`
Accounts []*HTTPAccount `json:"accounts"` Accounts []*HTTPAccount `json:"accounts"`
Transparent bool `json:"allowTransparent"` Transparent bool `json:"allowTransparent"`
UserLevel uint32 `json:"userLevel"` UserLevel uint32 `json:"userLevel"`
@@ -34,9 +35,13 @@ func (c *HTTPServerConfig) Build() (proto.Message, error) {
UserLevel: c.UserLevel, UserLevel: c.UserLevel,
} }
if len(c.Accounts) > 0 { if c.Accounts != nil {
c.Users = c.Accounts
}
// TODO: PB
if len(c.Users) > 0 {
config.Accounts = make(map[string]string) config.Accounts = make(map[string]string)
for _, account := range c.Accounts { for _, account := range c.Users {
config.Accounts[account.Username] = account.Password config.Accounts[account.Username] = account.Password
} }
} }
@@ -51,8 +56,8 @@ type HTTPRemoteConfig struct {
} }
type HTTPClientConfig struct { type HTTPClientConfig struct {
Address *Address `json:"address"` Address *Address `json:"address"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
Level uint32 `json:"level"` Level uint32 `json:"level"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"user"` Username string `json:"user"`
+18 -7
View File
@@ -4,6 +4,7 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/hysteria" "github.com/xtls/xray-core/proxy/hysteria"
"github.com/xtls/xray-core/proxy/hysteria/account" "github.com/xtls/xray-core/proxy/hysteria/account"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@@ -38,22 +39,32 @@ type HysteriaUserConfig struct {
type HysteriaServerConfig struct { type HysteriaServerConfig struct {
Version int32 `json:"version"` Version int32 `json:"version"`
Users []*HysteriaUserConfig `json:"clients"` Users []*HysteriaUserConfig `json:"users"`
Clients []*HysteriaUserConfig `json:"clients"`
} }
func (c *HysteriaServerConfig) Build() (proto.Message, error) { func (c *HysteriaServerConfig) Build() (proto.Message, error) {
config := new(hysteria.ServerConfig) config := new(hysteria.ServerConfig)
if c.Users != nil { if c.Clients != nil {
for _, user := range c.Users { c.Users = c.Clients
account := &account.Account{ }
if len(c.Users) > 0 {
config.Users = make([]*protocol.User, len(c.Users))
processUser := func(idx int) error {
user := c.Users[idx]
acc := &account.Account{
Auth: user.Auth, Auth: user.Auth,
} }
config.Users = append(config.Users, &protocol.User{ config.Users[idx] = &protocol.User{
Email: user.Email, Email: user.Email,
Level: user.Level, Level: user.Level,
Account: serial.ToTypedMessage(account), Account: serial.ToTypedMessage(acc),
}) }
return nil
}
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
return nil, err
} }
} }
+8 -12
View File
@@ -135,17 +135,15 @@ func TestRouterConfig(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{10, 0, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
Prefix: 8,
}, },
}, },
}, },
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
Prefix: 128,
}, },
}, },
}, },
@@ -216,17 +214,15 @@ func TestRouterConfig(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{10, 0, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
Prefix: 8,
}, },
}, },
}, },
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
Prefix: 128,
}, },
}, },
}, },
+15 -1
View File
@@ -5,12 +5,22 @@ import (
"io" "io"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
creflect "github.com/xtls/xray-core/common/reflect" creflect "github.com/xtls/xray-core/common/reflect"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/main/confloader" "github.com/xtls/xray-core/main/confloader"
) )
// UseStrictJSON, when true, makes JSON config decoders skip the custom
// comment-stripping reader and parse input as strict RFC 8259 JSON.
//
// Enabled by setting the env variable xray.json.strict=true (or its normalized
// form XRAY_JSON_STRICT=true). Default false preserves backward-compatible
// behavior for human-edited configs that may contain comments or other
// JSON5/JSONC syntax.
var UseStrictJSON = platform.NewEnvFlag(platform.UseStrictJSON).GetValue(func() string { return "" }) == "true"
func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) { func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) {
c, err := mergeConfigs(files) c, err := mergeConfigs(files)
if err != nil { if err != nil {
@@ -31,7 +41,11 @@ func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
if err != nil { if err != nil {
return nil, errors.New("failed to read config: ", file).Base(err) return nil, errors.New("failed to read config: ", file).Base(err)
} }
c, err := ReaderDecoderByFormat[file.Format](r) decoder := ReaderDecoderByFormat[file.Format]
if file.Format == "json" && UseStrictJSON {
decoder = DecodeJSONConfigStrict
}
c, err := decoder(r)
if err != nil { if err != nil {
return nil, errors.New("failed to decode config: ", file).Base(err) return nil, errors.New("failed to decode config: ", file).Base(err)
} }
+21
View File
@@ -42,6 +42,9 @@ func findOffset(b []byte, o int) *offset {
// DecodeJSONConfig reads from reader and decode the config into *conf.Config // DecodeJSONConfig reads from reader and decode the config into *conf.Config
// syntax error could be detected. // syntax error could be detected.
//
// Permissive: accepts JSON with Java/Python-style comments via json_reader.Reader.
// Used for local files and stdin where the config is human-edited.
func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) { func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
jsonConfig := &conf.Config{} jsonConfig := &conf.Config{}
@@ -69,6 +72,24 @@ func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
return jsonConfig, nil return jsonConfig, nil
} }
// DecodeJSONConfigStrict reads standard RFC 8259 JSON without comment-stripping.
// Used for remote sources (http/https/http+unix) where the payload is produced by
// automated systems and cannot contain JSON5/JSONC extensions. Avoids the
// byte-by-byte comment stripper and TeeReader, which are significant overhead on
// large configs.
func DecodeJSONConfigStrict(reader io.Reader) (*conf.Config, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, errors.New("failed to read config file").Base(err)
}
jsonConfig := &conf.Config{}
if err := json.Unmarshal(data, jsonConfig); err != nil {
return nil, errors.New("failed to parse remote JSON config").Base(err)
}
return jsonConfig, nil
}
func LoadJSONConfig(reader io.Reader) (*core.Config, error) { func LoadJSONConfig(reader io.Reader) (*core.Config, error) {
jsonConfig, err := DecodeJSONConfig(reader) jsonConfig, err := DecodeJSONConfig(reader)
if err != nil { if err != nil {
+40 -20
View File
@@ -8,6 +8,7 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/shadowsocks" "github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/shadowsocks_2022" "github.com/xtls/xray-core/proxy/shadowsocks_2022"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@@ -44,13 +45,18 @@ type ShadowsocksServerConfig struct {
Password string `json:"password"` Password string `json:"password"`
Level byte `json:"level"` Level byte `json:"level"`
Email string `json:"email"` Email string `json:"email"`
Users []*ShadowsocksUserConfig `json:"clients"` Users []*ShadowsocksUserConfig `json:"users"`
Clients []*ShadowsocksUserConfig `json:"clients"`
NetworkList *NetworkList `json:"network"` NetworkList *NetworkList `json:"network"`
} }
func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption") errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption")
if v.Clients != nil {
v.Users = v.Clients
}
if C.Contains(shadowaead_2022.List, v.Cipher) { if C.Contains(shadowaead_2022.List, v.Cipher) {
return buildShadowsocks2022(v) return buildShadowsocks2022(v)
} }
@@ -59,23 +65,31 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
config.Network = v.NetworkList.Build() config.Network = v.NetworkList.Build()
if v.Users != nil { if v.Users != nil {
for _, user := range v.Users { if len(v.Users) > 0 {
account := &shadowsocks.Account{ config.Users = make([]*protocol.User, len(v.Users))
Password: user.Password, processUser := func(idx int) error {
CipherType: cipherFromString(user.Cipher), user := v.Users[idx]
account := &shadowsocks.Account{
Password: user.Password,
CipherType: cipherFromString(user.Cipher),
}
if account.Password == "" {
return errors.New("Shadowsocks password is not specified.")
}
if account.CipherType < shadowsocks.CipherType_AES_128_GCM ||
account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 {
return errors.New("unsupported cipher method: ", user.Cipher)
}
config.Users[idx] = &protocol.User{
Email: user.Email,
Level: uint32(user.Level),
Account: serial.ToTypedMessage(account),
}
return nil
} }
if account.Password == "" { if err := task.ParallelForN(len(v.Users), processUser); err != nil {
return nil, errors.New("Shadowsocks password is not specified.") return nil, err
} }
if account.CipherType < shadowsocks.CipherType_AES_128_GCM ||
account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 {
return nil, errors.New("unsupported cipher method: ", user.Cipher)
}
config.Users = append(config.Users, &protocol.User{
Email: user.Email,
Level: uint32(user.Level),
Account: serial.ToTypedMessage(account),
})
} }
} else { } else {
account := &shadowsocks.Account{ account := &shadowsocks.Account{
@@ -121,18 +135,24 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
config.Key = v.Password config.Key = v.Password
config.Network = v.NetworkList.Build() config.Network = v.NetworkList.Build()
for _, user := range v.Users { config.Users = make([]*protocol.User, len(v.Users))
processUser := func(idx int) error {
user := v.Users[idx]
if user.Cipher != "" { if user.Cipher != "" {
return nil, errors.New("shadowsocks 2022 (multi-user): users must have empty method") return errors.New("shadowsocks 2022 (multi-user): users must have empty method")
} }
account := &shadowsocks_2022.Account{ account := &shadowsocks_2022.Account{
Key: user.Password, Key: user.Password,
} }
config.Users = append(config.Users, &protocol.User{ config.Users[idx] = &protocol.User{
Email: user.Email, Email: user.Email,
Level: uint32(user.Level), Level: uint32(user.Level),
Account: serial.ToTypedMessage(account), Account: serial.ToTypedMessage(account),
}) }
return nil
}
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
return nil, err
} }
return config, nil return config, nil
} }
+8 -3
View File
@@ -29,6 +29,7 @@ const (
type SocksServerConfig struct { type SocksServerConfig struct {
AuthMethod string `json:"auth"` AuthMethod string `json:"auth"`
Users []*SocksAccount `json:"users"`
Accounts []*SocksAccount `json:"accounts"` Accounts []*SocksAccount `json:"accounts"`
UDP bool `json:"udp"` UDP bool `json:"udp"`
Host *Address `json:"ip"` Host *Address `json:"ip"`
@@ -47,9 +48,13 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
config.AuthType = socks.AuthType_NO_AUTH config.AuthType = socks.AuthType_NO_AUTH
} }
if len(v.Accounts) > 0 { if v.Accounts != nil {
config.Accounts = make(map[string]string, len(v.Accounts)) v.Users = v.Accounts
for _, account := range v.Accounts { }
// TODO: PB
if len(v.Users) > 0 {
config.Accounts = make(map[string]string, len(v.Users))
for _, account := range v.Users {
config.Accounts[account.Username] = account.Password config.Accounts[account.Username] = account.Password
} }
} }
+226 -36
View File
@@ -8,6 +8,7 @@ import (
"math" "math"
"net/url" "net/url"
"os" "os"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -496,8 +497,8 @@ func (b Bandwidth) Bps() (uint64, error) {
} }
type UdpHop struct { type UdpHop struct {
PortList json.RawMessage `json:"ports"` PortList PortList `json:"ports"`
Interval *Int32Range `json:"interval"` Interval Int32Range `json:"interval"`
} }
type Masquerade struct { type Masquerade struct {
@@ -1231,6 +1232,8 @@ func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
} }
var ( var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"header-custom": func() interface{} { return new(HeaderCustomTCP) }, "header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) }, "fragment": func() interface{} { return new(FragmentMask) },
@@ -1256,11 +1259,14 @@ var (
) )
type TCPItem struct { type TCPItem struct {
Delay Int32Range `json:"delay"` Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"` Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"` RandRange *Int32Range `json:"randRange"`
Type string `json:"type"` Capture string `json:"capture"`
Packet json.RawMessage `json:"packet"` Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
} }
type HeaderCustomTCP struct { type HeaderCustomTCP struct {
@@ -1272,22 +1278,22 @@ type HeaderCustomTCP struct {
func (c *HeaderCustomTCP) Build() (proto.Message, error) { func (c *HeaderCustomTCP) Build() (proto.Message, error) {
for _, value := range c.Clients { for _, value := range c.Clients {
for _, item := range value { for _, item := range value {
if len(item.Packet) > 0 && item.Rand > 0 { if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") return nil, err
} }
} }
} }
for _, value := range c.Servers { for _, value := range c.Servers {
for _, item := range value { for _, item := range value {
if len(item.Packet) > 0 && item.Rand > 0 { if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") return nil, err
} }
} }
} }
for _, value := range c.Errors { for _, value := range c.Errors {
for _, item := range value { for _, item := range value {
if len(item.Packet) > 0 && item.Rand > 0 { if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") return nil, err
} }
} }
} }
@@ -1308,6 +1314,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err return nil, err
} }
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{ clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From), DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To), DelayMax: int64(item.Delay.To),
@@ -1315,6 +1325,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
RandMin: item.RandRange.From, RandMin: item.RandRange.From,
RandMax: item.RandRange.To, RandMax: item.RandRange.To,
Packet: item.Packet, Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
}) })
} }
} }
@@ -1333,6 +1346,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err return nil, err
} }
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{ servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From), DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To), DelayMax: int64(item.Delay.To),
@@ -1340,6 +1357,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
RandMin: item.RandRange.From, RandMin: item.RandRange.From,
RandMax: item.RandRange.To, RandMax: item.RandRange.To,
Packet: item.Packet, Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
}) })
} }
} }
@@ -1358,6 +1378,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err return nil, err
} }
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{ errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From), DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To), DelayMax: int64(item.Delay.To),
@@ -1365,6 +1389,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
RandMin: item.RandRange.From, RandMin: item.RandRange.From,
RandMax: item.RandRange.To, RandMax: item.RandRange.To,
Packet: item.Packet, Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
}) })
} }
} }
@@ -1471,26 +1498,185 @@ func (c *NoiseMask) Build() (proto.Message, error) {
} }
type UDPItem struct { type UDPItem struct {
Rand int32 `json:"rand"` Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"` RandRange *Int32Range `json:"randRange"`
Type string `json:"type"` Capture string `json:"capture"`
Packet json.RawMessage `json:"packet"` 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 { type HeaderCustomUDP struct {
Mode string `json:"mode"`
Client []UDPItem `json:"client"` Client []UDPItem `json:"client"`
Server []UDPItem `json:"server"` Server []UDPItem `json:"server"`
} }
func (c *HeaderCustomUDP) Build() (proto.Message, error) { 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 { for _, item := range c.Client {
if len(item.Packet) > 0 && item.Rand > 0 { if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") return nil, err
} }
} }
for _, item := range c.Server { for _, item := range c.Server {
if len(item.Packet) > 0 && item.Rand > 0 { if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") return nil, err
} }
} }
@@ -1506,11 +1692,18 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err return nil, err
} }
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
client = append(client, &custom.UDPItem{ client = append(client, &custom.UDPItem{
Rand: item.Rand, Rand: item.Rand,
RandMin: item.RandRange.From, RandMin: item.RandRange.From,
RandMax: item.RandRange.To, RandMax: item.RandRange.To,
Packet: item.Packet, Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
}) })
} }
@@ -1526,17 +1719,25 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err return nil, err
} }
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
server = append(server, &custom.UDPItem{ server = append(server, &custom.UDPItem{
Rand: item.Rand, Rand: item.Rand,
RandMin: item.RandRange.From, RandMin: item.RandRange.From,
RandMax: item.RandRange.To, RandMax: item.RandRange.To,
Packet: item.Packet, Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
}) })
} }
return &custom.UDPConfig{ return &custom.UDPConfig{
Client: client, Client: client,
Server: server, Server: server,
Mode: c.Mode,
}, nil }, nil
} }
@@ -1941,18 +2142,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal") return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
} }
var hop *PortList if (c.FinalMask.QuicParams.UdpHop.Interval.From != 0 && c.FinalMask.QuicParams.UdpHop.Interval.From < 5) || (c.FinalMask.QuicParams.UdpHop.Interval.To != 0 && c.FinalMask.QuicParams.UdpHop.Interval.To < 5) {
if err := json.Unmarshal(c.FinalMask.QuicParams.UdpHop.PortList, &hop); err != nil {
hop = &PortList{}
}
var inertvalMin, inertvalMax int64
if c.FinalMask.QuicParams.UdpHop.Interval != nil {
inertvalMin = int64(c.FinalMask.QuicParams.UdpHop.Interval.From)
inertvalMax = int64(c.FinalMask.QuicParams.UdpHop.Interval.To)
}
if (inertvalMin != 0 && inertvalMin < 5) || (inertvalMax != 0 && inertvalMax < 5) {
return nil, errors.New("Interval must be at least 5") return nil, errors.New("Interval must be at least 5")
} }
@@ -1989,9 +2179,9 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
BrutalUp: up, BrutalUp: up,
BrutalDown: down, BrutalDown: down,
UdpHop: &internet.UdpHop{ UdpHop: &internet.UdpHop{
Ports: hop.Build().Ports(), Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
IntervalMin: inertvalMin, IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
IntervalMax: inertvalMax, IntervalMax: int64(c.FinalMask.QuicParams.UdpHop.Interval.To),
}, },
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow, InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow, MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
+136
View File
@@ -2,10 +2,12 @@ package conf_test
import ( import (
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -156,3 +158,137 @@ func TestSocketConfig(t *testing.T) {
t.Fatalf("unexpected parsed TFO value, which should be -1") t.Fatalf("unexpected parsed TFO value, which should be -1")
} }
} }
func TestHeaderCustomUDPBuild(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
runMultiTestCase(t, []TestCase{
{
Input: `{
"client": [
{
"type": "hex",
"packet": "aabb"
},
{
"rand": 2,
"capture": "seed",
"randRange": "16-32"
}
],
"server": [
{
"capture": "txid",
"transform": {
"op": "concat",
"args": [
{"reuse": "seed"},
{"u64": 258},
{"type": "hex", "bytes": "c0de"}
]
}
},
{
"reuse": "txid"
}
],
"mode": "standalone"
}`,
Parser: parser,
Output: &finalmaskcustom.UDPConfig{
Mode: "standalone",
Client: []*finalmaskcustom.UDPItem{
{
RandMax: 255,
Packet: []byte{0xAA, 0xBB},
},
{
Rand: 2,
RandMin: 16,
RandMax: 32,
Save: "seed",
},
},
Server: []*finalmaskcustom.UDPItem{
{
RandMax: 255,
Save: "txid",
Expr: &finalmaskcustom.Expr{
Op: "concat",
Args: []*finalmaskcustom.ExprArg{
{
Value: &finalmaskcustom.ExprArg_Var{
Var: "seed",
},
},
{
Value: &finalmaskcustom.ExprArg_U64{
U64: 258,
},
},
{
Value: &finalmaskcustom.ExprArg_Bytes{
Bytes: []byte{0xC0, 0xDE},
},
},
},
},
},
{
RandMax: 255,
Var: "txid",
},
},
},
},
})
}
func TestHeaderCustomTCPBuildRejectsMixedItemKinds(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomTCP) })
_, err := parser(`{
"clients": [[
{
"packet": [1, 2],
"reuse": "txid"
}
]]
}`)
if err == nil || !strings.Contains(err.Error(), "exactly one item kind") {
t.Fatalf("expected mixed item kind rejection, got %v", err)
}
}
func TestHeaderCustomUDPBuildRejectsInvalidVariableNames(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
_, err := parser(`{
"client": [
{
"capture": "bad-name",
"rand": 4
}
]
}`)
if err == nil || !strings.Contains(err.Error(), "invalid variable name") {
t.Fatalf("expected invalid variable name rejection, got %v", err)
}
}
func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
_, err := parser(`{
"client": [
{
"transform": {
"op": "concat"
}
}
]
}`)
if err == nil || !strings.Contains(err.Error(), "transform args") {
t.Fatalf("expected transform arg rejection, got %v", err)
}
}
+15 -4
View File
@@ -12,6 +12,7 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/trojan" "github.com/xtls/xray-core/proxy/trojan"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -111,6 +112,7 @@ type TrojanUserConfig struct {
// TrojanServerConfig is Inbound configuration // TrojanServerConfig is Inbound configuration
type TrojanServerConfig struct { type TrojanServerConfig struct {
Users []*TrojanUserConfig `json:"users"`
Clients []*TrojanUserConfig `json:"clients"` Clients []*TrojanUserConfig `json:"clients"`
Fallbacks []*TrojanInboundFallback `json:"fallbacks"` Fallbacks []*TrojanInboundFallback `json:"fallbacks"`
} }
@@ -119,13 +121,18 @@ type TrojanServerConfig struct {
func (c *TrojanServerConfig) Build() (proto.Message, error) { func (c *TrojanServerConfig) Build() (proto.Message, error) {
errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed") errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed")
config := &trojan.ServerConfig{ if c.Clients != nil {
Users: make([]*protocol.User, len(c.Clients)), c.Users = c.Clients
} }
for idx, rawUser := range c.Clients { config := &trojan.ServerConfig{
Users: make([]*protocol.User, len(c.Users)),
}
processClient := func(idx int) error {
rawUser := c.Users[idx]
if rawUser.Flow != "" { if rawUser.Flow != "" {
return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``) return errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
} }
config.Users[idx] = &protocol.User{ config.Users[idx] = &protocol.User{
@@ -135,6 +142,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
Password: rawUser.Password, Password: rawUser.Password,
}), }),
} }
return nil
}
if err := task.ParallelForN(len(c.Users), processClient); err != nil {
return nil, err
} }
for _, fb := range c.Fallbacks { for _, fb := range c.Fallbacks {
+3 -6
View File
@@ -7,7 +7,7 @@ import (
type TunConfig struct { type TunConfig struct {
Name string `json:"name"` Name string `json:"name"`
MTU []uint32 `json:"mtu"` MTU uint32 `json:"mtu"`
Gateway []string `json:"gateway"` Gateway []string `json:"gateway"`
DNS []string `json:"dns"` DNS []string `json:"dns"`
UserLevel uint32 `json:"userLevel"` UserLevel uint32 `json:"userLevel"`
@@ -34,11 +34,8 @@ func (v *TunConfig) Build() (proto.Message, error) {
if config.Name == "" { if config.Name == "" {
config.Name = "xray0" config.Name = "xray0"
} }
if len(config.MTU) == 0 { if config.MTU == 0 {
config.MTU = []uint32{1500, 1280} config.MTU = 1500
}
if len(config.MTU) == 1 {
config.MTU = append(config.MTU, config.MTU[0])
} }
return config, nil return config, nil
} }
+22 -10
View File
@@ -13,6 +13,7 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vless/inbound" "github.com/xtls/xray-core/proxy/vless/inbound"
@@ -30,6 +31,7 @@ type VLessInboundFallback struct {
} }
type VLessInboundConfig struct { type VLessInboundConfig struct {
Users []json.RawMessage `json:"users"`
Clients []json.RawMessage `json:"clients"` Clients []json.RawMessage `json:"clients"`
Decryption string `json:"decryption"` Decryption string `json:"decryption"`
Fallbacks []*VLessInboundFallback `json:"fallbacks"` Fallbacks []*VLessInboundFallback `json:"fallbacks"`
@@ -40,25 +42,30 @@ type VLessInboundConfig struct {
// Build implements Buildable // Build implements Buildable
func (c *VLessInboundConfig) Build() (proto.Message, error) { func (c *VLessInboundConfig) Build() (proto.Message, error) {
config := new(inbound.Config) config := new(inbound.Config)
config.Clients = make([]*protocol.User, len(c.Clients))
if c.Clients != nil {
c.Users = c.Clients
}
config.Users = make([]*protocol.User, len(c.Users))
switch c.Flow { switch c.Flow {
case vless.XRV, "": case vless.XRV, "":
default: default:
return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`) return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`)
} }
for idx, rawUser := range c.Clients { processClient := func(idx int) error {
rawUser := c.Users[idx]
user := new(protocol.User) user := new(protocol.User)
if err := json.Unmarshal(rawUser, user); err != nil { if err := json.Unmarshal(rawUser, user); err != nil {
return nil, errors.New(`VLESS clients: invalid user`).Base(err) return errors.New(`VLESS users: invalid user`).Base(err)
} }
account := new(vless.Account) account := new(vless.Account)
if err := json.Unmarshal(rawUser, account); err != nil { if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New(`VLESS clients: invalid user`).Base(err) return errors.New(`VLESS users: invalid user`).Base(err)
} }
u, err := uuid.ParseString(account.Id) u, err := uuid.ParseString(account.Id)
if err != nil { if err != nil {
return nil, err return err
} }
account.Id = u.String() account.Id = u.String()
@@ -67,7 +74,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
account.Flow = c.Flow account.Flow = c.Flow
case vless.XRV: case vless.XRV:
default: default:
return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) return errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`)
} }
if len(account.Testseed) < 4 { if len(account.Testseed) < 4 {
@@ -75,20 +82,25 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
} }
if account.Encryption != "" { if account.Encryption != "" {
return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`) return errors.New(`VLESS users: "encryption" should not be in inbound settings`)
} }
if account.Reverse != nil { if account.Reverse != nil {
if account.Reverse.Tag == "" { if account.Reverse.Tag == "" {
return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`) return errors.New(`VLESS users: "tag" can't be empty for "reverse"`)
} }
if account.Reverse.Sniffing != nil { // may not be reached: error json unmarshal if account.Reverse.Sniffing != nil { // may not be reached: error json unmarshal
return nil, errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`) return errors.New(`VLESS users: inbound's "reverse" can't have "sniffing"`)
} }
} }
user.Account = serial.ToTypedMessage(account) user.Account = serial.ToTypedMessage(account)
config.Clients[idx] = user config.Users[idx] = user
return nil
}
if err := task.ParallelForN(len(c.Users), processClient); err != nil {
return nil, err
} }
config.Decryption = c.Decryption config.Decryption = c.Decryption
+1 -1
View File
@@ -119,7 +119,7 @@ func TestVLessInbound(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &inbound.Config{ Output: &inbound.Config{
Clients: []*protocol.User{ Users: []*protocol.User{
{ {
Account: serial.ToTypedMessage(&vless.Account{ Account: serial.ToTypedMessage(&vless.Account{
Id: "27848739-7e62-4138-9fd3-098a63964b6b", Id: "27848739-7e62-4138-9fd3-098a63964b6b",
+15 -5
View File
@@ -7,6 +7,7 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/proxy/vmess" "github.com/xtls/xray-core/proxy/vmess"
"github.com/xtls/xray-core/proxy/vmess/inbound" "github.com/xtls/xray-core/proxy/vmess/inbound"
@@ -58,7 +59,8 @@ func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig {
} }
type VMessInboundConfig struct { type VMessInboundConfig struct {
Users []json.RawMessage `json:"clients"` Users []json.RawMessage `json:"users"`
Clients []json.RawMessage `json:"clients"`
Defaults *VMessDefaultConfig `json:"default"` Defaults *VMessDefaultConfig `json:"default"`
} }
@@ -72,25 +74,33 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) {
config.Default = c.Defaults.Build() config.Default = c.Defaults.Build()
} }
if c.Clients != nil {
c.Users = c.Clients
}
config.User = make([]*protocol.User, len(c.Users)) config.User = make([]*protocol.User, len(c.Users))
for idx, rawData := range c.Users { processUser := func(idx int) error {
rawData := c.Users[idx]
user := new(protocol.User) user := new(protocol.User)
if err := json.Unmarshal(rawData, user); err != nil { if err := json.Unmarshal(rawData, user); err != nil {
return nil, errors.New("invalid VMess user").Base(err) return errors.New("invalid VMess user").Base(err)
} }
account := new(VMessAccount) account := new(VMessAccount)
if err := json.Unmarshal(rawData, account); err != nil { if err := json.Unmarshal(rawData, account); err != nil {
return nil, errors.New("invalid VMess user").Base(err) return errors.New("invalid VMess user").Base(err)
} }
u, err := uuid.ParseString(account.ID) u, err := uuid.ParseString(account.ID)
if err != nil { if err != nil {
return nil, err return err
} }
account.ID = u.String() account.ID = u.String()
user.Account = serial.ToTypedMessage(account.Build()) user.Account = serial.ToTypedMessage(account.Build())
config.User[idx] = user config.User[idx] = user
return nil
}
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
return nil, err
} }
return config, nil return config, nil
+14
View File
@@ -361,6 +361,7 @@ type Config struct {
Observatory *ObservatoryConfig `json:"observatory"` Observatory *ObservatoryConfig `json:"observatory"`
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"` BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
Version *VersionConfig `json:"version"` Version *VersionConfig `json:"version"`
Geodata *GeodataConfig `json:"geodata"`
} }
func (c *Config) findInboundTag(tag string) int { func (c *Config) findInboundTag(tag string) int {
@@ -433,6 +434,10 @@ func (c *Config) Override(o *Config, fn string) {
c.Version = o.Version c.Version = o.Version
} }
if o.Geodata != nil {
c.Geodata = o.Geodata
}
// update the Inbound in slice if the only one in override config has same tag // update the Inbound in slice if the only one in override config has same tag
if len(o.InboundConfigs) > 0 { if len(o.InboundConfigs) > 0 {
for i := range o.InboundConfigs { for i := range o.InboundConfigs {
@@ -542,6 +547,7 @@ func (c *Config) Build() (*core.Config, error) {
} }
if c.Reverse != nil { if c.Reverse != nil {
return nil, errors.PrintRemovedFeatureError(`"legacy reverse"`, `"VLESS Reverse Proxy"`)
r, err := c.Reverse.Build() r, err := c.Reverse.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build reverse configuration").Base(err) return nil, errors.New("failed to build reverse configuration").Base(err)
@@ -581,6 +587,14 @@ func (c *Config) Build() (*core.Config, error) {
config.App = append(config.App, serial.ToTypedMessage(r)) config.App = append(config.App, serial.ToTypedMessage(r))
} }
if c.Geodata != nil {
r, err := c.Geodata.Build()
if err != nil {
return nil, errors.New("failed to build geodata configuration").Base(err)
}
config.App = append(config.App, serial.ToTypedMessage(r))
}
var inbounds []InboundDetourConfig var inbounds []InboundDetourConfig
if len(c.InboundConfigs) > 0 { if len(c.InboundConfigs) > 0 {
+8 -5
View File
@@ -99,9 +99,8 @@ func TestXrayConfig(t *testing.T) {
Ip: []*geodata.IPRule{ Ip: []*geodata.IPRule{
{ {
Value: &geodata.IPRule_Custom{ Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{ Custom: &geodata.CIDRRule{
Ip: []byte{10, 0, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
Prefix: 8,
}, },
}, },
}, },
@@ -216,8 +215,12 @@ func TestSniffingConfig_Build(t *testing.T) {
if rule == nil { if rule == nil {
t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i) t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i)
} }
if !reflect.DeepEqual(rule.Ip, tc.ip) || rule.Prefix != tc.prefix { cidr := rule.GetCidr()
t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, rule.Ip, rule.Prefix, tc.ip, tc.prefix) if cidr == nil {
t.Fatalf("SniffingConfig.Build() produced a custom ip rule without cidr at index %d", i)
}
if !reflect.DeepEqual(cidr.Ip, tc.ip) || cidr.Prefix != tc.prefix {
t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, cidr.Ip, cidr.Prefix, tc.ip, tc.prefix)
} }
} }
} }
+1 -1
View File
@@ -80,7 +80,7 @@ func extractInboundUsers(inb *core.InboundHandlerConfig) []*protocol.User {
case *vmessin.Config: case *vmessin.Config:
return ty.User return ty.User
case *vlessin.Config: case *vlessin.Config:
return ty.Clients return ty.Users
case *trojan.ServerConfig: case *trojan.ServerConfig:
return ty.Users return ty.Users
case *shadowsocks.ServerConfig: case *shadowsocks.ServerConfig:
+1
View File
@@ -20,6 +20,7 @@ import (
// Other optional features. // Other optional features.
_ "github.com/xtls/xray-core/app/dns" _ "github.com/xtls/xray-core/app/dns"
_ "github.com/xtls/xray-core/app/dns/fakedns" _ "github.com/xtls/xray-core/app/dns/fakedns"
_ "github.com/xtls/xray-core/app/geodata"
_ "github.com/xtls/xray-core/app/log" _ "github.com/xtls/xray-core/app/log"
_ "github.com/xtls/xray-core/app/metrics" _ "github.com/xtls/xray-core/app/metrics"
_ "github.com/xtls/xray-core/app/policy" _ "github.com/xtls/xray-core/app/policy"
+7
View File
@@ -41,6 +41,13 @@ func init() {
} }
return cf.Build() return cf.Build()
case io.Reader: case io.Reader:
if serial.UseStrictJSON {
cfg, err := serial.DecodeJSONConfigStrict(v)
if err != nil {
return nil, err
}
return cfg.Build()
}
return serial.LoadJSONConfig(v) return serial.LoadJSONConfig(v)
default: default:
return nil, errors.New("unknown type") return nil, errors.New("unknown type")
+15 -1
View File
@@ -6,7 +6,11 @@ import (
"time" "time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
) )
@@ -38,7 +42,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
// Sleep a little here to make sure the response is sent to client. // Sleep a little here to make sure the response is sent to client.
time.Sleep(time.Second) time.Sleep(time.Second)
} }
common.Interrupt(link.Writer) defer common.Interrupt(link.Writer)
defer common.Interrupt(link.Reader)
// wait to drain all the possible incoming UDP data
if ob.Target.Network == net.Network_UDP {
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, func() {
cancel()
}, time.Duration(30+dice.Roll(61))*time.Second)
go buf.Copy(link.Reader, buf.Discard, buf.UpdateActivity(timer))
<-ctx.Done()
}
return nil return nil
} }
+162 -41
View File
@@ -7,6 +7,7 @@
package dns package dns
import ( import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net" net "github.com/xtls/xray-core/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -22,21 +23,130 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type RuleAction int32
const (
RuleAction_Direct RuleAction = 0
RuleAction_Drop RuleAction = 1
RuleAction_Reject RuleAction = 2
RuleAction_Hijack RuleAction = 3
)
// Enum value maps for RuleAction.
var (
RuleAction_name = map[int32]string{
0: "Direct",
1: "Drop",
2: "Reject",
3: "Hijack",
}
RuleAction_value = map[string]int32{
"Direct": 0,
"Drop": 1,
"Reject": 2,
"Hijack": 3,
}
)
func (x RuleAction) Enum() *RuleAction {
p := new(RuleAction)
*p = x
return p
}
func (x RuleAction) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (RuleAction) Descriptor() protoreflect.EnumDescriptor {
return file_proxy_dns_config_proto_enumTypes[0].Descriptor()
}
func (RuleAction) Type() protoreflect.EnumType {
return &file_proxy_dns_config_proto_enumTypes[0]
}
func (x RuleAction) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use RuleAction.Descriptor instead.
func (RuleAction) EnumDescriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
type DNSRuleConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.dns.RuleAction" json:"action,omitempty"`
Qtype []int32 `protobuf:"varint,2,rep,packed,name=qtype,proto3" json:"qtype,omitempty"`
Domain []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DNSRuleConfig) Reset() {
*x = DNSRuleConfig{}
mi := &file_proxy_dns_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DNSRuleConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSRuleConfig) ProtoMessage() {}
func (x *DNSRuleConfig) ProtoReflect() protoreflect.Message {
mi := &file_proxy_dns_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSRuleConfig.ProtoReflect.Descriptor instead.
func (*DNSRuleConfig) Descriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
func (x *DNSRuleConfig) GetAction() RuleAction {
if x != nil {
return x.Action
}
return RuleAction_Direct
}
func (x *DNSRuleConfig) GetQtype() []int32 {
if x != nil {
return x.Qtype
}
return nil
}
func (x *DNSRuleConfig) GetDomain() []*geodata.DomainRule {
if x != nil {
return x.Domain
}
return nil
}
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// Server is the DNS server address. If specified, this address overrides the UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
// original one. Rule []*DNSRuleConfig `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` RewriteServer *net.Endpoint `protobuf:"bytes,3,opt,name=rewrite_server,json=rewriteServer,proto3" json:"rewrite_server,omitempty"`
UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
Non_IPQuery string `protobuf:"bytes,3,opt,name=non_IP_query,json=nonIPQuery,proto3" json:"non_IP_query,omitempty"`
BlockTypes []int32 `protobuf:"varint,4,rep,packed,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_proxy_dns_config_proto_msgTypes[0] mi := &file_proxy_dns_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -48,7 +158,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_dns_config_proto_msgTypes[0] mi := &file_proxy_dns_config_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -61,14 +171,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0} return file_proxy_dns_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetServer() *net.Endpoint {
if x != nil {
return x.Server
}
return nil
} }
func (x *Config) GetUserLevel() uint32 { func (x *Config) GetUserLevel() uint32 {
@@ -78,16 +181,16 @@ func (x *Config) GetUserLevel() uint32 {
return 0 return 0
} }
func (x *Config) GetNon_IPQuery() string { func (x *Config) GetRule() []*DNSRuleConfig {
if x != nil { if x != nil {
return x.Non_IPQuery return x.Rule
} }
return "" return nil
} }
func (x *Config) GetBlockTypes() []int32 { func (x *Config) GetRewriteServer() *net.Endpoint {
if x != nil { if x != nil {
return x.BlockTypes return x.RewriteServer
} }
return nil return nil
} }
@@ -96,15 +199,25 @@ var File_proxy_dns_config_proto protoreflect.FileDescriptor
const file_proxy_dns_config_proto_rawDesc = "" + const file_proxy_dns_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" + "\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" +
"\x06Config\x121\n" + "\rDNSRuleConfig\x122\n" +
"\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\n" + "\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" +
"\x05qtype\x18\x02 \x03(\x05R\x05qtype\x127\n" +
"\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\"\x9c\x01\n" +
"\x06Config\x12\x1d\n" +
"\n" + "\n" +
"user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" + "user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" +
"\fnon_IP_query\x18\x03 \x01(\tR\n" + "\x04rule\x18\x02 \x03(\v2\x1d.xray.proxy.dns.DNSRuleConfigR\x04rule\x12@\n" +
"nonIPQuery\x12\x1f\n" + "\x0erewrite_server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\rrewriteServer*:\n" +
"\vblock_types\x18\x04 \x03(\x05R\n" + "\n" +
"blockTypesBL\n" + "RuleAction\x12\n" +
"\n" +
"\x06Direct\x10\x00\x12\b\n" +
"\x04Drop\x10\x01\x12\n" +
"\n" +
"\x06Reject\x10\x02\x12\n" +
"\n" +
"\x06Hijack\x10\x03BL\n" +
"\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3" "\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3"
var ( var (
@@ -119,18 +232,25 @@ func file_proxy_dns_config_proto_rawDescGZIP() []byte {
return file_proxy_dns_config_proto_rawDescData return file_proxy_dns_config_proto_rawDescData
} }
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_proxy_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_dns_config_proto_goTypes = []any{ var file_proxy_dns_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.dns.Config (RuleAction)(0), // 0: xray.proxy.dns.RuleAction
(*net.Endpoint)(nil), // 1: xray.common.net.Endpoint (*DNSRuleConfig)(nil), // 1: xray.proxy.dns.DNSRuleConfig
(*Config)(nil), // 2: xray.proxy.dns.Config
(*geodata.DomainRule)(nil), // 3: xray.common.geodata.DomainRule
(*net.Endpoint)(nil), // 4: xray.common.net.Endpoint
} }
var file_proxy_dns_config_proto_depIdxs = []int32{ var file_proxy_dns_config_proto_depIdxs = []int32{
1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint 0, // 0: xray.proxy.dns.DNSRuleConfig.action:type_name -> xray.proxy.dns.RuleAction
1, // [1:1] is the sub-list for method output_type 3, // 1: xray.proxy.dns.DNSRuleConfig.domain:type_name -> xray.common.geodata.DomainRule
1, // [1:1] is the sub-list for method input_type 1, // 2: xray.proxy.dns.Config.rule:type_name -> xray.proxy.dns.DNSRuleConfig
1, // [1:1] is the sub-list for extension type_name 4, // 3: xray.proxy.dns.Config.rewrite_server:type_name -> xray.common.net.Endpoint
1, // [1:1] is the sub-list for extension extendee 4, // [4:4] is the sub-list for method output_type
0, // [0:1] is the sub-list for field type_name 4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
} }
func init() { file_proxy_dns_config_proto_init() } func init() { file_proxy_dns_config_proto_init() }
@@ -143,13 +263,14 @@ func file_proxy_dns_config_proto_init() {
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)),
NumEnums: 0, NumEnums: 1,
NumMessages: 1, NumMessages: 2,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
GoTypes: file_proxy_dns_config_proto_goTypes, GoTypes: file_proxy_dns_config_proto_goTypes,
DependencyIndexes: file_proxy_dns_config_proto_depIdxs, DependencyIndexes: file_proxy_dns_config_proto_depIdxs,
EnumInfos: file_proxy_dns_config_proto_enumTypes,
MessageInfos: file_proxy_dns_config_proto_msgTypes, MessageInfos: file_proxy_dns_config_proto_msgTypes,
}.Build() }.Build()
File_proxy_dns_config_proto = out.File File_proxy_dns_config_proto = out.File
+17 -6
View File
@@ -7,12 +7,23 @@ option java_package = "com.xray.proxy.dns";
option java_multiple_files = true; option java_multiple_files = true;
import "common/net/destination.proto"; import "common/net/destination.proto";
import "common/geodata/geodat.proto";
enum RuleAction {
Direct = 0;
Drop = 1;
Reject = 2;
Hijack = 3;
}
message DNSRuleConfig {
RuleAction action = 1;
repeated int32 qtype = 2;
repeated xray.common.geodata.DomainRule domain = 3;
}
message Config { message Config {
// Server is the DNS server address. If specified, this address overrides the uint32 user_level = 1;
// original one. repeated DNSRuleConfig rule = 2;
xray.common.net.Endpoint server = 1; xray.common.net.Endpoint rewrite_server = 3;
uint32 user_level = 2;
string non_IP_query = 3;
repeated int32 block_types = 4;
} }
+107 -59
View File
@@ -11,6 +11,7 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
dns_proto "github.com/xtls/xray-core/common/protocol/dns" dns_proto "github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
@@ -40,6 +41,31 @@ func init() {
})) }))
} }
type DNSRule struct {
action RuleAction
qTypes []uint16
domains geodata.DomainMatcher
}
func (r *DNSRule) matchQType(qType uint16) bool {
if len(r.qTypes) == 0 {
return true
}
for _, t := range r.qTypes {
if t == qType {
return true
}
}
return false
}
func (r *DNSRule) Apply(qType uint16, domain string) bool {
if !r.matchQType(qType) {
return false
}
return r.domains == nil || r.domains.MatchAny(strings.TrimSuffix(strings.ToLower(domain), "."))
}
type ownLinkVerifier interface { type ownLinkVerifier interface {
IsOwnLink(ctx context.Context) bool IsOwnLink(ctx context.Context) bool
} }
@@ -48,10 +74,9 @@ type Handler struct {
client dns.Client client dns.Client
fdns dns.FakeDNSEngine fdns dns.FakeDNSEngine
ownLinkVerifier ownLinkVerifier ownLinkVerifier ownLinkVerifier
server net.Destination rewriteServer net.Destination
timeout time.Duration timeout time.Duration
nonIPQuery string rules []*DNSRule
blockTypes []int32
} }
func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error { func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error {
@@ -62,14 +87,29 @@ func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager polic
h.ownLinkVerifier = v h.ownLinkVerifier = v
} }
if config.Server != nil { if config.RewriteServer != nil {
h.server = config.Server.AsDestination() h.rewriteServer = config.RewriteServer.AsDestination()
} }
h.nonIPQuery = config.Non_IPQuery
if h.nonIPQuery == "" { h.rules = make([]*DNSRule, 0, len(config.Rule))
h.nonIPQuery = "reject" for _, r := range config.Rule {
rule := &DNSRule{
action: r.Action,
qTypes: make([]uint16, 0, len(r.Qtype)),
}
for _, t := range r.Qtype {
rule.qTypes = append(rule.qTypes, uint16(t))
}
if len(r.Domain) > 0 {
m, err := geodata.DomainReg.BuildDomainMatcher(r.Domain)
if err != nil {
return err
}
rule.domains = m
}
h.rules = append(h.rules, rule)
} }
h.blockTypes = config.BlockTypes
return nil return nil
} }
@@ -77,30 +117,38 @@ func (h *Handler) isOwnLink(ctx context.Context) bool {
return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx) return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx)
} }
func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) { func parseQuery(b []byte) (id uint16, qType dnsmessage.Type, domain string, ok bool) {
var parser dnsmessage.Parser var parser dnsmessage.Parser
header, err := parser.Start(b) header, err := parser.Start(b)
if err != nil { if err != nil {
errors.LogInfoInner(context.Background(), err, "parser start") errors.LogInfoInner(context.Background(), err, "parser start")
return return
} }
id = header.ID id = header.ID
q, err := parser.Question() q, err := parser.Question()
if err != nil { if err != nil {
errors.LogInfoInner(context.Background(), err, "question") errors.LogInfoInner(context.Background(), err, "question")
return return
} }
domain = q.Name.String()
qType = q.Type qType = q.Type
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA { domain = q.Name.String()
return ok = true
}
r = true
return return
} }
func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction {
qCode := uint16(qType)
for _, r := range h.rules {
if r.Apply(qCode, domain) {
return r.action
}
}
if qType == dnsmessage.TypeA || qType == dnsmessage.TypeAAAA {
return RuleAction_Hijack
}
return RuleAction_Reject
}
// Process implements proxy.Outbound. // Process implements proxy.Outbound.
func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error { func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error {
outbounds := session.OutboundsFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
@@ -113,14 +161,14 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
srcNetwork := ob.Target.Network srcNetwork := ob.Target.Network
dest := ob.Target dest := ob.Target
if h.server.Network != net.Network_Unknown { if h.rewriteServer.Network != net.Network_Unknown {
dest.Network = h.server.Network dest.Network = h.rewriteServer.Network
} }
if h.server.Address != nil { if h.rewriteServer.Address != nil {
dest.Address = h.server.Address dest.Address = h.rewriteServer.Address
} }
if h.server.Port != 0 { if h.rewriteServer.Port != 0 {
dest.Port = h.server.Port dest.Port = h.rewriteServer.Port
} }
errors.LogInfo(ctx, "handling DNS traffic to ", dest) errors.LogInfo(ctx, "handling DNS traffic to ", dest)
@@ -183,51 +231,51 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
if err == io.EOF { if err == io.EOF {
return nil return nil
} }
if err != nil { if err != nil {
return err return err
} }
timer.Update() timer.Update()
if !h.isOwnLink(ctx) { if h.isOwnLink(ctx) {
isIPQuery, domain, id, qType := parseIPQuery(b.Bytes()) if err := connWriter.WriteMessage(b); err != nil {
if len(h.blockTypes) > 0 { return err
for _, blocktype := range h.blockTypes {
if blocktype == int32(qType) {
b.Release()
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
if h.nonIPQuery == "reject" {
err := h.rejectNonIPQuery(id, qType, domain, writer)
if err != nil {
return err
}
}
return nil
}
}
}
if isIPQuery {
b.Release()
go h.handleIPQuery(id, qType, domain, writer, timer)
continue
}
if h.nonIPQuery == "drop" {
b.Release()
continue
}
if h.nonIPQuery == "reject" {
b.Release()
err := h.rejectNonIPQuery(id, qType, domain, writer)
if err != nil {
return err
}
continue
} }
continue
} }
if err := connWriter.WriteMessage(b); err != nil { id, qType, domain, ok := parseQuery(b.Bytes())
return err if !ok {
b.Release()
continue
}
switch h.applyRules(qType, domain) {
case RuleAction_Drop:
b.Release()
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
case RuleAction_Reject:
b.Release()
errors.LogInfo(ctx, "rejected type ", qType, " query for domain ", domain)
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
return err
}
case RuleAction_Hijack:
b.Release()
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
errors.LogError(ctx, "can only hijack A/AAAA records")
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
return err
}
} else {
go h.handleIPQuery(id, qType, domain, writer, timer)
}
case RuleAction_Direct:
if err := connWriter.WriteMessage(b); err != nil {
return err
}
default:
panic("unknown rule action")
} }
} }
} }
+134 -10
View File
@@ -14,6 +14,7 @@ import (
_ "github.com/xtls/xray-core/app/proxyman/inbound" _ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound" _ "github.com/xtls/xray-core/app/proxyman/outbound"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
@@ -113,9 +114,9 @@ func TestUDPDNSTunnel(t *testing.T) {
Inbound: []*core.InboundHandlerConfig{ Inbound: []*core.InboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP), RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(port), RewritePort: uint32(port),
Networks: []net.Network{net.Network_UDP}, AllowedNetworks: []net.Network{net.Network_UDP},
}), }),
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
@@ -232,9 +233,9 @@ func TestTCPDNSTunnel(t *testing.T) {
Inbound: []*core.InboundHandlerConfig{ Inbound: []*core.InboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP), RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(port), RewritePort: uint32(port),
Networks: []net.Network{net.Network_TCP}, AllowedNetworks: []net.Network{net.Network_TCP},
}), }),
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
@@ -318,9 +319,9 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
Inbound: []*core.InboundHandlerConfig{ Inbound: []*core.InboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP), RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(port), RewritePort: uint32(port),
Networks: []net.Network{net.Network_TCP}, AllowedNetworks: []net.Network{net.Network_TCP},
}), }),
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
@@ -331,7 +332,7 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{ ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
Server: &net.Endpoint{ RewriteServer: &net.Endpoint{
Network: net.Network_TCP, Network: net.Network_TCP,
}, },
}), }),
@@ -368,3 +369,126 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
t.Error(r) t.Error(r)
} }
} }
func TestDNSRules(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
}
defer dnsServer.Shutdown()
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
serverPort := udp.PickPort()
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dnsapp.Config{
NameServer: []*dnsapp.NameServer{
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Inbound: []*core.InboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
RewritePort: uint32(port),
AllowedNetworks: []net.Network{net.Network_UDP},
}),
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
Rule: []*dns_proxy.DNSRuleConfig{
{
Qtype: []int32{int32(dns.TypeA)},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Domain,
Value: "facebook.com",
},
},
},
},
Action: dns_proxy.RuleAction_Direct,
},
{
Qtype: []int32{int32(dns.TypeA)},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Full,
Value: "google.com",
},
},
},
},
Action: dns_proxy.RuleAction_Reject,
},
},
}),
},
},
}
v, err := core.New(config)
common.Must(err)
common.Must(v.Start())
defer v.Close()
{
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = []dns.Question{{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
c := new(dns.Client)
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
common.Must(err)
if in.Rcode != dns.RcodeRefused {
t.Fatal("expected Refused, but got ", in.Rcode)
}
}
{
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = []dns.Question{{Name: "facebook.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
c := new(dns.Client)
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
common.Must(err)
if in.Rcode != dns.RcodeSuccess {
t.Fatal("expected Success, but got ", in.Rcode)
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
// GetPredefinedAddress returns the defined address from proto config. Null if address is not valid. // GetPredefinedAddress returns the defined address from proto config. Null if address is not valid.
func (v *Config) GetPredefinedAddress() net.Address { func (v *Config) GetPredefinedAddress() net.Address {
addr := v.Address.AsAddress() addr := v.RewriteAddress.AsAddress()
if addr == nil { if addr == nil {
return nil return nil
} }
+23 -23
View File
@@ -23,16 +23,16 @@ const (
) )
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// List of networks that the Dokodemo accepts. // List of networks that the Dokodemo accepts.
Networks []net.Network `protobuf:"varint,7,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"` FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Config) Reset() { func (x *Config) Reset() {
@@ -65,16 +65,16 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0} return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0}
} }
func (x *Config) GetAddress() *net.IPOrDomain { func (x *Config) GetRewriteAddress() *net.IPOrDomain {
if x != nil { if x != nil {
return x.Address return x.RewriteAddress
} }
return nil return nil
} }
func (x *Config) GetPort() uint32 { func (x *Config) GetRewritePort() uint32 {
if x != nil { if x != nil {
return x.Port return x.RewritePort
} }
return 0 return 0
} }
@@ -86,9 +86,9 @@ func (x *Config) GetPortMap() map[string]string {
return nil return nil
} }
func (x *Config) GetNetworks() []net.Network { func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil { if x != nil {
return x.Networks return x.AllowedNetworks
} }
return nil return nil
} }
@@ -111,12 +111,12 @@ var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor
const file_proxy_dokodemo_config_proto_rawDesc = "" + const file_proxy_dokodemo_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xd2\x02\n" + "\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" +
"\x06Config\x125\n" + "\x06Config\x12D\n" +
"\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + "\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" +
"\x04port\x18\x02 \x01(\rR\x04port\x12C\n" + "\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" +
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x124\n" + "\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\n" +
"\bnetworks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x12'\n" + "\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12'\n" +
"\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" + "\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" +
"\n" + "\n" +
"user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" + "user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" +
@@ -145,9 +145,9 @@ var file_proxy_dokodemo_config_proto_goTypes = []any{
(net.Network)(0), // 3: xray.common.net.Network (net.Network)(0), // 3: xray.common.net.Network
} }
var file_proxy_dokodemo_config_proto_depIdxs = []int32{ var file_proxy_dokodemo_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.dokodemo.Config.address:type_name -> xray.common.net.IPOrDomain 2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry 1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
3, // 2: xray.proxy.dokodemo.Config.networks:type_name -> xray.common.net.Network 3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
3, // [3:3] is the sub-list for method output_type 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 method input_type
3, // [3:3] is the sub-list for extension type_name 3, // [3:3] is the sub-list for extension type_name
+4 -7
View File
@@ -10,14 +10,11 @@ import "common/net/address.proto";
import "common/net/network.proto"; import "common/net/network.proto";
message Config { message Config {
xray.common.net.IPOrDomain address = 1;
uint32 port = 2;
map<string, string> port_map = 3;
// List of networks that the Dokodemo accepts. // List of networks that the Dokodemo accepts.
repeated xray.common.net.Network networks = 7; repeated xray.common.net.Network allowed_networks = 7;
xray.common.net.IPOrDomain rewrite_address = 1;
uint32 rewrite_port = 2;
map<string, string> port_map = 3;
bool follow_redirect = 5; bool follow_redirect = 5;
uint32 user_level = 6; uint32 user_level = 6;
} }
+15 -15
View File
@@ -32,22 +32,22 @@ func init() {
} }
type DokodemoDoor struct { type DokodemoDoor struct {
policyManager policy.Manager policyManager policy.Manager
config *Config config *Config
address net.Address rewriteAddress net.Address
port net.Port rewritePort net.Port
portMap map[string]string portMap map[string]string
sockopt *session.Sockopt sockopt *session.Sockopt
} }
// Init initializes the DokodemoDoor instance with necessary parameters. // Init initializes the DokodemoDoor instance with necessary parameters.
func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.Sockopt) error { func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.Sockopt) error {
if len(config.Networks) == 0 { if len(config.AllowedNetworks) == 0 {
return errors.New("no network specified") return errors.New("no network specified")
} }
d.config = config d.config = config
d.address = config.GetPredefinedAddress() d.rewriteAddress = config.GetPredefinedAddress()
d.port = net.Port(config.Port) d.rewritePort = net.Port(config.RewritePort)
d.portMap = config.PortMap d.portMap = config.PortMap
d.policyManager = pm d.policyManager = pm
d.sockopt = sockopt d.sockopt = sockopt
@@ -57,10 +57,10 @@ func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.
// Network implements proxy.Inbound. // Network implements proxy.Inbound.
func (d *DokodemoDoor) Network() []net.Network { func (d *DokodemoDoor) Network() []net.Network {
if slices.Contains(d.config.Networks, net.Network_TCP) { if slices.Contains(d.config.AllowedNetworks, net.Network_TCP) {
return append(d.config.Networks, net.Network_UNIX) return append(d.config.AllowedNetworks, net.Network_UNIX)
} }
return d.config.Networks return d.config.AllowedNetworks
} }
func (d *DokodemoDoor) policy() policy.Session { func (d *DokodemoDoor) policy() policy.Session {
@@ -78,8 +78,8 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st
} }
dest := net.Destination{ dest := net.Destination{
Network: network, Network: network,
Address: d.address, Address: d.rewriteAddress,
Port: d.port, Port: d.rewritePort,
} }
if !d.config.FollowRedirect { if !d.config.FollowRedirect {
@@ -95,7 +95,7 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st
} }
} }
} }
if dest.Port == 0 { if dest.Port == 0 && port != "" {
dest.Port = net.Port(common.Must2(strconv.Atoi(port))) dest.Port = net.Port(common.Must2(strconv.Atoi(port)))
} }
if d.portMap != nil && d.portMap[port] != "" { if d.portMap != nil && d.portMap[port] != "" {
-1
View File
@@ -1 +0,0 @@
package freedom
+239 -25
View File
@@ -7,6 +7,8 @@
package freedom package freedom
import ( import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net"
protocol "github.com/xtls/xray-core/common/protocol" protocol "github.com/xtls/xray-core/common/protocol"
internet "github.com/xtls/xray-core/transport/internet" internet "github.com/xtls/xray-core/transport/internet"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
@@ -23,6 +25,52 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type RuleAction int32
const (
RuleAction_Allow RuleAction = 0
RuleAction_Block RuleAction = 1
)
// Enum value maps for RuleAction.
var (
RuleAction_name = map[int32]string{
0: "Allow",
1: "Block",
}
RuleAction_value = map[string]int32{
"Allow": 0,
"Block": 1,
}
)
func (x RuleAction) Enum() *RuleAction {
p := new(RuleAction)
*p = x
return p
}
func (x RuleAction) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (RuleAction) Descriptor() protoreflect.EnumDescriptor {
return file_proxy_freedom_config_proto_enumTypes[0].Descriptor()
}
func (RuleAction) Type() protoreflect.EnumType {
return &file_proxy_freedom_config_proto_enumTypes[0]
}
func (x RuleAction) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use RuleAction.Descriptor instead.
func (RuleAction) EnumDescriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{0}
}
type DestinationOverride struct { type DestinationOverride struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
@@ -251,6 +299,134 @@ func (x *Noise) GetApplyTo() string {
return "" return ""
} }
type Range struct {
state protoimpl.MessageState `protogen:"open.v1"`
Min uint64 `protobuf:"varint,1,opt,name=min,proto3" json:"min,omitempty"`
Max uint64 `protobuf:"varint,2,opt,name=max,proto3" json:"max,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Range) Reset() {
*x = Range{}
mi := &file_proxy_freedom_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Range) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Range) ProtoMessage() {}
func (x *Range) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Range.ProtoReflect.Descriptor instead.
func (*Range) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3}
}
func (x *Range) GetMin() uint64 {
if x != nil {
return x.Min
}
return 0
}
func (x *Range) GetMax() uint64 {
if x != nil {
return x.Max
}
return 0
}
type FinalRuleConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.freedom.RuleAction" json:"action,omitempty"`
Networks []net.Network `protobuf:"varint,2,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"`
PortList *net.PortList `protobuf:"bytes,3,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"`
Ip []*geodata.IPRule `protobuf:"bytes,4,rep,name=ip,proto3" json:"ip,omitempty"`
BlockDelay *Range `protobuf:"bytes,5,opt,name=block_delay,json=blockDelay,proto3" json:"block_delay,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FinalRuleConfig) Reset() {
*x = FinalRuleConfig{}
mi := &file_proxy_freedom_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FinalRuleConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FinalRuleConfig) ProtoMessage() {}
func (x *FinalRuleConfig) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FinalRuleConfig.ProtoReflect.Descriptor instead.
func (*FinalRuleConfig) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{4}
}
func (x *FinalRuleConfig) GetAction() RuleAction {
if x != nil {
return x.Action
}
return RuleAction_Allow
}
func (x *FinalRuleConfig) GetNetworks() []net.Network {
if x != nil {
return x.Networks
}
return nil
}
func (x *FinalRuleConfig) GetPortList() *net.PortList {
if x != nil {
return x.PortList
}
return nil
}
func (x *FinalRuleConfig) GetIp() []*geodata.IPRule {
if x != nil {
return x.Ip
}
return nil
}
func (x *FinalRuleConfig) GetBlockDelay() *Range {
if x != nil {
return x.BlockDelay
}
return nil
}
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"` DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"`
@@ -259,13 +435,14 @@ type Config struct {
Fragment *Fragment `protobuf:"bytes,5,opt,name=fragment,proto3" json:"fragment,omitempty"` Fragment *Fragment `protobuf:"bytes,5,opt,name=fragment,proto3" json:"fragment,omitempty"`
ProxyProtocol uint32 `protobuf:"varint,6,opt,name=proxy_protocol,json=proxyProtocol,proto3" json:"proxy_protocol,omitempty"` ProxyProtocol uint32 `protobuf:"varint,6,opt,name=proxy_protocol,json=proxyProtocol,proto3" json:"proxy_protocol,omitempty"`
Noises []*Noise `protobuf:"bytes,7,rep,name=noises,proto3" json:"noises,omitempty"` Noises []*Noise `protobuf:"bytes,7,rep,name=noises,proto3" json:"noises,omitempty"`
FinalRules []*FinalRuleConfig `protobuf:"bytes,8,rep,name=final_rules,json=finalRules,proto3" json:"final_rules,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_proxy_freedom_config_proto_msgTypes[3] mi := &file_proxy_freedom_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -277,7 +454,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[3] mi := &file_proxy_freedom_config_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -290,7 +467,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3} return file_proxy_freedom_config_proto_rawDescGZIP(), []int{5}
} }
func (x *Config) GetDomainStrategy() internet.DomainStrategy { func (x *Config) GetDomainStrategy() internet.DomainStrategy {
@@ -335,11 +512,18 @@ func (x *Config) GetNoises() []*Noise {
return nil return nil
} }
func (x *Config) GetFinalRules() []*FinalRuleConfig {
if x != nil {
return x.FinalRules
}
return nil
}
var File_proxy_freedom_config_proto protoreflect.FileDescriptor var File_proxy_freedom_config_proto protoreflect.FileDescriptor
const file_proxy_freedom_config_proto_rawDesc = "" + const file_proxy_freedom_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\"S\n" + "\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"S\n" +
"\x13DestinationOverride\x12<\n" + "\x13DestinationOverride\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
"\bFragment\x12!\n" + "\bFragment\x12!\n" +
@@ -362,7 +546,17 @@ const file_proxy_freedom_config_proto_rawDesc = "" +
"\tdelay_min\x18\x03 \x01(\x04R\bdelayMin\x12\x1b\n" + "\tdelay_min\x18\x03 \x01(\x04R\bdelayMin\x12\x1b\n" +
"\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" + "\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" +
"\x06packet\x18\x05 \x01(\fR\x06packet\x12\x19\n" + "\x06packet\x18\x05 \x01(\fR\x06packet\x12\x19\n" +
"\bapply_to\x18\x06 \x01(\tR\aapplyTo\"\xe9\x02\n" + "\bapply_to\x18\x06 \x01(\tR\aapplyTo\"+\n" +
"\x05Range\x12\x10\n" +
"\x03min\x18\x01 \x01(\x04R\x03min\x12\x10\n" +
"\x03max\x18\x02 \x01(\x04R\x03max\"\xa0\x02\n" +
"\x0fFinalRuleConfig\x126\n" +
"\x06action\x18\x01 \x01(\x0e2\x1e.xray.proxy.freedom.RuleActionR\x06action\x124\n" +
"\bnetworks\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x126\n" +
"\tport_list\x18\x03 \x01(\v2\x19.xray.common.net.PortListR\bportList\x12+\n" +
"\x02ip\x18\x04 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\x12:\n" +
"\vblock_delay\x18\x05 \x01(\v2\x19.xray.proxy.freedom.RangeR\n" +
"blockDelay\"\xaf\x03\n" +
"\x06Config\x12P\n" + "\x06Config\x12P\n" +
"\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" + "\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" +
"\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" + "\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" +
@@ -370,7 +564,13 @@ const file_proxy_freedom_config_proto_rawDesc = "" +
"user_level\x18\x04 \x01(\rR\tuserLevel\x128\n" + "user_level\x18\x04 \x01(\rR\tuserLevel\x128\n" +
"\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" + "\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" +
"\x0eproxy_protocol\x18\x06 \x01(\rR\rproxyProtocol\x121\n" + "\x0eproxy_protocol\x18\x06 \x01(\rR\rproxyProtocol\x121\n" +
"\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noisesBX\n" + "\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noises\x12D\n" +
"\vfinal_rules\x18\b \x03(\v2#.xray.proxy.freedom.FinalRuleConfigR\n" +
"finalRules*\"\n" +
"\n" +
"RuleAction\x12\t\n" +
"\x05Allow\x10\x00\x12\t\n" +
"\x05Block\x10\x01BX\n" +
"\x16com.xray.proxy.freedomP\x01Z'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3" "\x16com.xray.proxy.freedomP\x01Z'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3"
var ( var (
@@ -385,26 +585,39 @@ func file_proxy_freedom_config_proto_rawDescGZIP() []byte {
return file_proxy_freedom_config_proto_rawDescData return file_proxy_freedom_config_proto_rawDescData
} }
var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_proxy_freedom_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_proxy_freedom_config_proto_goTypes = []any{ var file_proxy_freedom_config_proto_goTypes = []any{
(*DestinationOverride)(nil), // 0: xray.proxy.freedom.DestinationOverride (RuleAction)(0), // 0: xray.proxy.freedom.RuleAction
(*Fragment)(nil), // 1: xray.proxy.freedom.Fragment (*DestinationOverride)(nil), // 1: xray.proxy.freedom.DestinationOverride
(*Noise)(nil), // 2: xray.proxy.freedom.Noise (*Fragment)(nil), // 2: xray.proxy.freedom.Fragment
(*Config)(nil), // 3: xray.proxy.freedom.Config (*Noise)(nil), // 3: xray.proxy.freedom.Noise
(*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint (*Range)(nil), // 4: xray.proxy.freedom.Range
(internet.DomainStrategy)(0), // 5: xray.transport.internet.DomainStrategy (*FinalRuleConfig)(nil), // 5: xray.proxy.freedom.FinalRuleConfig
(*Config)(nil), // 6: xray.proxy.freedom.Config
(*protocol.ServerEndpoint)(nil), // 7: xray.common.protocol.ServerEndpoint
(net.Network)(0), // 8: xray.common.net.Network
(*net.PortList)(nil), // 9: xray.common.net.PortList
(*geodata.IPRule)(nil), // 10: xray.common.geodata.IPRule
(internet.DomainStrategy)(0), // 11: xray.transport.internet.DomainStrategy
} }
var file_proxy_freedom_config_proto_depIdxs = []int32{ var file_proxy_freedom_config_proto_depIdxs = []int32{
4, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint 7, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
5, // 1: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy 0, // 1: xray.proxy.freedom.FinalRuleConfig.action:type_name -> xray.proxy.freedom.RuleAction
0, // 2: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride 8, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network
1, // 3: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment 9, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList
2, // 4: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise 10, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule
5, // [5:5] is the sub-list for method output_type 4, // 5: xray.proxy.freedom.FinalRuleConfig.block_delay:type_name -> xray.proxy.freedom.Range
5, // [5:5] is the sub-list for method input_type 11, // 6: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
5, // [5:5] is the sub-list for extension type_name 1, // 7: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
5, // [5:5] is the sub-list for extension extendee 2, // 8: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
0, // [0:5] is the sub-list for field type_name 3, // 9: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
5, // 10: xray.proxy.freedom.Config.final_rules:type_name -> xray.proxy.freedom.FinalRuleConfig
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
} }
func init() { file_proxy_freedom_config_proto_init() } func init() { file_proxy_freedom_config_proto_init() }
@@ -417,13 +630,14 @@ func file_proxy_freedom_config_proto_init() {
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)),
NumEnums: 0, NumEnums: 1,
NumMessages: 4, NumMessages: 6,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
GoTypes: file_proxy_freedom_config_proto_goTypes, GoTypes: file_proxy_freedom_config_proto_goTypes,
DependencyIndexes: file_proxy_freedom_config_proto_depIdxs, DependencyIndexes: file_proxy_freedom_config_proto_depIdxs,
EnumInfos: file_proxy_freedom_config_proto_enumTypes,
MessageInfos: file_proxy_freedom_config_proto_msgTypes, MessageInfos: file_proxy_freedom_config_proto_msgTypes,
}.Build() }.Build()
File_proxy_freedom_config_proto = out.File File_proxy_freedom_config_proto = out.File
+23
View File
@@ -8,6 +8,9 @@ option java_multiple_files = true;
import "common/protocol/server_spec.proto"; import "common/protocol/server_spec.proto";
import "transport/internet/config.proto"; import "transport/internet/config.proto";
import "common/net/port.proto";
import "common/net/network.proto";
import "common/geodata/geodat.proto";
message DestinationOverride { message DestinationOverride {
xray.common.protocol.ServerEndpoint server = 1; xray.common.protocol.ServerEndpoint server = 1;
@@ -23,6 +26,7 @@ message Fragment {
uint64 max_split_min = 7; uint64 max_split_min = 7;
uint64 max_split_max = 8; uint64 max_split_max = 8;
} }
message Noise { message Noise {
uint64 length_min = 1; uint64 length_min = 1;
uint64 length_max = 2; uint64 length_max = 2;
@@ -32,6 +36,24 @@ message Noise {
string apply_to = 6; string apply_to = 6;
} }
message Range {
uint64 min = 1;
uint64 max = 2;
}
enum RuleAction {
Allow = 0;
Block = 1;
}
message FinalRuleConfig {
RuleAction action = 1;
repeated xray.common.net.Network networks = 2;
xray.common.net.PortList port_list = 3;
repeated xray.common.geodata.IPRule ip = 4;
Range block_delay = 5;
}
message Config { message Config {
xray.transport.internet.DomainStrategy domain_strategy = 1; xray.transport.internet.DomainStrategy domain_strategy = 1;
DestinationOverride destination_override = 3; DestinationOverride destination_override = 3;
@@ -39,4 +61,5 @@ message Config {
Fragment fragment = 5; Fragment fragment = 5;
uint32 proxy_protocol = 6; uint32 proxy_protocol = 6;
repeated Noise noises = 7; repeated Noise noises = 7;
repeated FinalRuleConfig final_rules = 8;
} }
+281 -36
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"io" "io"
"strings"
"time" "time"
"github.com/pires/go-proxyproto" "github.com/pires/go-proxyproto"
@@ -12,6 +13,7 @@ import (
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/retry" "github.com/xtls/xray-core/common/retry"
@@ -29,6 +31,9 @@ import (
) )
var useSplice bool var useSplice bool
var allNetworks [8]bool
var defaultBlockPrivateRule *FinalRule
var defaultBlockAllRule *FinalRule
func init() { func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
@@ -40,24 +45,185 @@ func init() {
} }
return h, nil return h, nil
})) }))
const defaultFlagValue = "NOT_DEFINED_AT_ALL" const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue }) value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
switch value { switch value {
case defaultFlagValue, "auto", "enable": case defaultFlagValue, "auto", "enable":
useSplice = true useSplice = true
} }
for i := range allNetworks {
allNetworks[i] = true
}
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",
})))),
}
defaultBlockAllRule = &FinalRule{
action: RuleAction_Block,
network: allNetworks,
}
}
type FinalRule struct {
action RuleAction
network [8]bool
port net.MemoryPortList
ip geodata.IPMatcher
blockDelay *Range
} }
// Handler handles Freedom connections. // Handler handles Freedom connections.
type Handler struct { type Handler struct {
policyManager policy.Manager policyManager policy.Manager
config *Config config *Config
finalRules []*FinalRule
}
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
rule := &FinalRule{
action: config.GetAction(),
blockDelay: config.GetBlockDelay(),
}
if len(config.Networks) == 0 {
rule.network = allNetworks
} else {
for _, network := range config.Networks {
rule.network[int(network)] = true
}
}
if config.PortList != nil {
rule.port = net.PortListFromProto(config.PortList)
}
if len(config.Ip) > 0 {
matcher, err := geodata.IPReg.BuildIPMatcher(config.Ip)
if err != nil {
return nil, err
}
rule.ip = matcher
}
return rule, nil
}
func (r *FinalRule) matchNetwork(network net.Network) bool {
return r.network[int(network)]
}
func (r *FinalRule) matchPort(port net.Port) bool {
if len(r.port) == 0 {
return true
}
return r.port.Contains(port)
}
func (r *FinalRule) matchIP(addr net.Address) bool {
if r.ip == nil {
return true
}
return addr != nil && addr.Family().IsIP() && r.ip.Match(addr.IP())
}
func (r *FinalRule) Apply(network net.Network, address net.Address, port net.Port) bool {
if !r.matchNetwork(network) {
return false
}
if !r.matchPort(port) {
return false
}
return r.matchIP(address)
}
func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
if inbound == nil {
return nil
}
switch inbound.Name {
case "vless-reverse":
return defaultBlockAllRule
case "vless", "vmess", "trojan", "hysteria", "wireguard":
return defaultBlockPrivateRule
default:
if strings.HasPrefix(inbound.Name, "shadowsocks") {
return defaultBlockPrivateRule
}
}
return nil
}
func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool {
if !dialDest.Address.Family().IsDomain() {
return false
}
if len(h.finalRules) > 0 {
rule := h.finalRules[0]
if rule.action == RuleAction_Allow && rule.network[dialDest.Network] && len(rule.port) == 0 && rule.ip == nil {
return false
}
}
if defaultRule != nil || len(h.finalRules) > 0 {
return true
}
return false
}
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
for _, rule := range h.finalRules {
if rule.Apply(network, address, port) {
return rule
}
}
if defaultRule != nil && defaultRule.Apply(network, address, port) {
return defaultRule
}
return nil
}
func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction {
if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil {
return rule.action
}
return RuleAction_Allow
} }
// Init initializes the Handler with necessary parameters. // Init initializes the Handler with necessary parameters.
func (h *Handler) Init(config *Config, pm policy.Manager) error { func (h *Handler) Init(config *Config, pm policy.Manager) error {
h.config = config h.config = config
h.policyManager = pm h.policyManager = pm
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
for _, rc := range config.FinalRules {
rule, err := buildFinalRule(rc)
if err != nil {
return errors.New("failed to build final rule").Base(err)
}
h.finalRules = append(h.finalRules, rule)
}
return nil return nil
} }
@@ -66,6 +232,20 @@ func (h *Handler) policy() policy.Session {
return p return p
} }
func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
min := uint64(30)
max := uint64(90)
if rule.blockDelay != nil {
min = rule.blockDelay.Min
max = rule.blockDelay.Max
}
span := max - min
if max < min {
span = min - max
}
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
}
func isValidAddress(addr *net.IPOrDomain) bool { func isValidAddress(addr *net.IPOrDomain) bool {
if addr == nil { if addr == nil {
return false return false
@@ -85,6 +265,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.Name = "freedom" ob.Name = "freedom"
ob.CanSpliceCopy = 1 ob.CanSpliceCopy = 1
inbound := session.InboundFromContext(ctx) inbound := session.InboundFromContext(ctx)
defaultRule := getDefaultFinalRule(inbound)
destination := ob.Target destination := ob.Target
origTargetAddr := ob.OriginalTarget.Address origTargetAddr := ob.OriginalTarget.Address
@@ -110,6 +291,9 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
output := link.Writer output := link.Writer
var conn stat.Connection var conn stat.Connection
var blockedDest *net.Destination
var blockedRule *FinalRule
firstResolve := true
err := retry.ExponentialBackoff(5, 100).On(func() error { err := retry.ExponentialBackoff(5, 100).On(func() error {
dialDest := destination dialDest := destination
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() { if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
@@ -120,7 +304,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway) ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
if err != nil { if err != nil {
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain()) errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
if h.config.DomainStrategy.ForceIP() { if h.config.DomainStrategy.ForceIP() || h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) {
return err return err
} }
} else { } else {
@@ -131,6 +315,36 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
} }
errors.LogInfo(ctx, "dialing to ", dialDest) errors.LogInfo(ctx, "dialing to ", dialDest)
} }
} else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules
domain := dialDest.Address.Domain()
var ips []net.IP
if firstResolve {
firstResolve = false
supportIPv4, supportIPv6 := utils.CheckRoutes()
if supportIPv4 {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip4", domain)
}
if len(ips) == 0 && supportIPv6 {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip6", domain)
}
if len(ips) == 0 {
return errors.New("failed to get IP address for domain ", domain)
}
} else {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip", domain)
}
if len(ips) == 0 { // SRV/TXT, lookup failed
return errors.New("failed to get IP address for domain ", domain)
}
if addr := net.IPAddress(ips[dice.Roll(len(ips))]); addr != nil {
dialDest.Address = addr
errors.LogInfo(ctx, "dialing to ", dialDest)
}
}
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &dialDest
blockedRule = rule
return nil
} }
rawConn, err := dialer.Dial(ctx, dialDest) rawConn, err := dialer.Dial(ctx, dialDest)
@@ -138,23 +352,37 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return err return err
} }
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr()
dstAddr := rawConn.RemoteAddr()
header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
if _, err = header.WriteTo(rawConn); err != nil {
rawConn.Close()
return err
}
}
conn = rawConn conn = rawConn
return nil return nil
}) })
if err != nil { if err != nil {
return errors.New("failed to open connection to ", destination).Base(err) return errors.New("failed to open connection to ", destination).Base(err)
} }
if blockedDest != nil {
delay := h.blockDelay(blockedRule)
errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() {
common.Interrupt(input)
common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest)
})
defer timer.Stop()
defer common.Close(output)
if err := buf.Copy(input, buf.Discard); err != nil {
return nil
}
return nil
}
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr()
dstAddr := conn.RemoteAddr()
header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
if _, err = header.WriteTo(conn); err != nil {
conn.Close()
return errors.New("failed to set PROXY protocol v", version).Base(err)
}
}
defer conn.Close() defer conn.Close()
errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr()) errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr())
@@ -189,7 +417,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
writer = buf.NewWriter(conn) writer = buf.NewWriter(conn)
} }
} else { } else {
writer = NewPacketWriter(conn, h, UDPOverride, destination) writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination)
if h.config.Noises != nil { if h.config.Noises != nil {
errors.LogDebug(ctx, "NOISE", h.config.Noises) errors.LogDebug(ctx, "NOISE", h.config.Noises)
writer = &NoisePacketWriter{ writer = &NoisePacketWriter{
@@ -224,7 +452,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
if destination.Network == net.Network_TCP { if destination.Network == net.Network_TCP {
reader = buf.NewReader(conn) reader = buf.NewReader(conn)
} else { } else {
reader = NewPacketReader(conn, UDPOverride, destination) reader = NewPacketReader(conn, h, defaultRule, UDPOverride, destination)
} }
if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil { if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to process response").Base(err) return errors.New("failed to process response").Base(err)
@@ -243,7 +471,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return nil return nil
} }
func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination) buf.Reader { func NewPacketReader(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {
iConn := conn iConn := conn
statConn, ok := iConn.(*stat.CounterConnection) statConn, ok := iConn.(*stat.CounterConnection)
if ok { if ok {
@@ -262,6 +490,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
return &PacketReader{ return &PacketReader{
PacketConnWrapper: c, PacketConnWrapper: c,
Counter: counter, Counter: counter,
Handler: h,
DefaultRule: defaultRule,
IsOverridden: isOverridden, IsOverridden: isOverridden,
InitUnchangedAddr: DialDest.Address, InitUnchangedAddr: DialDest.Address,
InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address, InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address,
@@ -273,6 +503,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
type PacketReader struct { type PacketReader struct {
*internet.PacketConnWrapper *internet.PacketConnWrapper
stats.Counter stats.Counter
Handler *Handler
DefaultRule *FinalRule
IsOverridden bool IsOverridden bool
InitUnchangedAddr net.Address InitUnchangedAddr net.Address
InitChangedAddr net.Address InitChangedAddr net.Address
@@ -281,33 +513,40 @@ type PacketReader struct {
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
b := buf.New() b := buf.New()
b.Resize(0, buf.Size) b.Resize(0, buf.Size)
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes()) for {
if err != nil { n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
b.Release() if err != nil {
return nil, err b.Release()
} return nil, err
b.Resize(0, int32(n))
// if udp dest addr is changed, we are unable to get the correct src addr
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
if !r.IsOverridden {
address := net.IPAddress(d.(*net.UDPAddr).IP)
if r.InitChangedAddr == address {
address = r.InitUnchangedAddr
} }
b.UDP = &net.Destination{ udpAddr := d.(*net.UDPAddr)
Address: address, sourceAddr := net.IPAddress(udpAddr.IP)
Port: net.Port(d.(*net.UDPAddr).Port), if r.Handler.applyFinalRules(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule) == RuleAction_Block {
Network: net.Network_UDP, continue
} }
b.Resize(0, int32(n))
// if udp dest addr is changed, we are unable to get the correct src addr
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
if !r.IsOverridden {
if r.InitChangedAddr == sourceAddr {
sourceAddr = r.InitUnchangedAddr
}
b.UDP = &net.Destination{
Address: sourceAddr,
Port: net.Port(udpAddr.Port),
Network: net.Network_UDP,
}
}
if r.Counter != nil {
r.Counter.Add(int64(n))
}
return buf.MultiBuffer{b}, nil
} }
if r.Counter != nil {
r.Counter.Add(int64(n))
}
return buf.MultiBuffer{b}, nil
} }
// DialDest means the dial target used in the dialer when creating conn // DialDest means the dial target used in the dialer when creating conn
func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, DialDest net.Destination) buf.Writer { func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Writer {
iConn := conn iConn := conn
statConn, ok := iConn.(*stat.CounterConnection) statConn, ok := iConn.(*stat.CounterConnection)
if ok { if ok {
@@ -328,6 +567,7 @@ func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, Dia
PacketConnWrapper: c, PacketConnWrapper: c,
Counter: counter, Counter: counter,
Handler: h, Handler: h,
DefaultRule: defaultRule,
UDPOverride: UDPOverride, UDPOverride: UDPOverride,
ResolvedUDPAddr: resolvedUDPAddr, ResolvedUDPAddr: resolvedUDPAddr,
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address, LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
@@ -341,6 +581,7 @@ type PacketWriter struct {
*internet.PacketConnWrapper *internet.PacketConnWrapper
stats.Counter stats.Counter
*Handler *Handler
DefaultRule *FinalRule
UDPOverride net.Destination UDPOverride net.Destination
// Dest of udp packets might be a domain, we will resolve them to IP // Dest of udp packets might be a domain, we will resolve them to IP
@@ -399,6 +640,10 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
} }
} }
} }
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block {
b.Release()
continue
}
destAddr := b.UDP.RawNetAddr() destAddr := b.UDP.RawNetAddr()
if destAddr == nil { if destAddr == nil {
b.Release() b.Release()
+45 -48
View File
@@ -17,7 +17,6 @@ import (
"github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/features/policy"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria" "github.com/xtls/xray-core/transport/internet/hysteria"
@@ -56,7 +55,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
ob.CanSpliceCopy = 3 ob.CanSpliceCopy = 3
target := ob.Target target := ob.Target
conn, err := dialer.Dial(hyCtx.ContextWithRequireDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination) conn, err := dialer.Dial(hysteria.ContextWithDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination)
if err != nil { if err != nil {
return errors.New("failed to find an available destination").AtWarning().Base(err) return errors.New("failed to find an available destination").AtWarning().Base(err)
} }
@@ -118,7 +117,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
if target.Network == net.Network_UDP { if target.Network == net.Network_UDP {
iConn := stat.TryUnwrapStatsConn(conn) iConn := stat.TryUnwrapStatsConn(conn)
_, ok := iConn.(*hysteria.InterUdpConn) _, ok := iConn.(*hysteria.InterConn)
if !ok { if !ok {
return errors.New("udp requires hysteria udp transport") return errors.New("udp requires hysteria udp transport")
} }
@@ -127,8 +126,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
writer := &UDPWriter{ writer := &UDPWriter{
Writer: conn, writer: conn,
buf: make([]byte, MaxUDPSize),
addr: target.NetAddr(), addr: target.NetAddr(),
} }
@@ -143,8 +141,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
reader := &UDPReader{ reader := &UDPReader{
Reader: conn, reader: conn,
buf: make([]byte, MaxUDPSize),
df: &Defragger{}, df: &Defragger{},
} }
@@ -173,28 +170,22 @@ func init() {
} }
type UDPWriter struct { type UDPWriter struct {
Writer io.Writer writer io.Writer
buf []byte
addr string addr string
buf [buf.Size]byte
} }
func (w *UDPWriter) sendMsg(msg *UDPMessage) error { func (w *UDPWriter) SendMessage(msg *UDPMessage) error {
msgN := msg.Serialize(w.buf) msgN := msg.Serialize(w.buf[:])
if msgN < 0 { if msgN < 0 {
return nil return nil
} }
_, err := w.Writer.Write(w.buf[:msgN]) _, err := w.writer.Write(w.buf[:msgN])
return err return err
} }
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
for { for i, b := range mb {
mb2, b := buf.SplitFirst(mb)
mb = mb2
if b == nil {
break
}
addr := w.addr addr := w.addr
if b.UDP != nil { if b.UDP != nil {
addr = b.UDP.NetAddr() addr = b.UDP.NetAddr()
@@ -209,22 +200,20 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
Data: b.Bytes(), Data: b.Bytes(),
} }
err := w.sendMsg(msg) err := w.SendMessage(msg)
var errTooLarge *quic.DatagramTooLargeError var errTooLarge *quic.DatagramTooLargeError
if go_errors.As(err, &errTooLarge) { if go_errors.As(err, &errTooLarge) {
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize)) fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs { for _, fMsg := range fMsgs {
err := w.sendMsg(&fMsg) err := w.SendMessage(&fMsg)
if err != nil { if err != nil {
b.Release() buf.ReleaseMulti(mb[i:])
buf.ReleaseMulti(mb)
return err return err
} }
} }
} else if err != nil { } else if err != nil {
b.Release() buf.ReleaseMulti(mb[i:])
buf.ReleaseMulti(mb)
return err return err
} }
@@ -235,30 +224,21 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
} }
type UDPReader struct { type UDPReader struct {
Reader io.Reader reader io.Reader
buf []byte df *Defragger
df *Defragger firstBuf *buf.Buffer
firstMsg *UDPMessage
firstDest *net.Destination
} }
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err error) {
if r.firstMsg != nil {
buffer := buf.New()
buffer.Write(r.firstMsg.Data)
buffer.UDP = r.firstDest
r.firstMsg = nil
return buf.MultiBuffer{buffer}, nil
}
for { for {
n, err := r.Reader.Read(r.buf) var buf [hysteria.MaxDatagramFrameSize]byte
n, err := r.reader.Read(buf[:])
if err != nil { if err != nil {
return nil, err return 0, nil, err
} }
msg, err := ParseUDPMessage(r.buf[:n]) msg, err := ParseUDPMessage(buf[:n])
if err != nil { if err != nil {
continue continue
} }
@@ -270,14 +250,31 @@ func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
dest, err := net.ParseDestination("udp:" + dfMsg.Addr) dest, err := net.ParseDestination("udp:" + dfMsg.Addr)
if err != nil { if err != nil {
errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err)
continue continue
} }
buffer := buf.New() if len(p) < len(dfMsg.Data) {
buffer.Write(dfMsg.Data) continue
buffer.UDP = &dest }
return buf.MultiBuffer{buffer}, nil return copy(p, dfMsg.Data), &dest, nil
} }
} }
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if r.firstBuf != nil {
mb := buf.MultiBuffer{r.firstBuf}
r.firstBuf = nil
return mb, nil
}
b := buf.New()
b.Resize(0, buf.Size)
n, addr, err := r.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return nil, err
}
b.Resize(0, int32(n))
b.UDP = addr
return buf.MultiBuffer{b}, nil
}
-9
View File
@@ -1,10 +1 @@
package hysteria package hysteria
import (
"github.com/xtls/xray-core/transport/internet/hysteria/padding"
)
var (
tcpRequestPadding = padding.Padding{Min: 64, Max: 512}
tcpResponsePadding = padding.Padding{Min: 128, Max: 1024}
)
-35
View File
@@ -1,35 +0,0 @@
package ctx
import (
"context"
"github.com/xtls/xray-core/proxy/hysteria/account"
)
type key int
const (
requireDatagram key = iota
validator
)
func ContextWithRequireDatagram(ctx context.Context, udp bool) context.Context {
if !udp {
return ctx
}
return context.WithValue(ctx, requireDatagram, struct{}{})
}
func RequireDatagramFromContext(ctx context.Context) bool {
_, ok := ctx.Value(requireDatagram).(struct{})
return ok
}
func ContextWithValidator(ctx context.Context, v *account.Validator) context.Context {
return context.WithValue(ctx, validator, v)
}
func ValidatorFromContext(ctx context.Context) *account.Validator {
v, _ := ctx.Value(validator).(*account.Validator)
return v
}
-73
View File
@@ -1,73 +0,0 @@
package hysteria
func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage {
if m.Size() <= maxSize {
return []UDPMessage{*m}
}
fullPayload := m.Data
maxPayloadSize := maxSize - m.HeaderSize()
off := 0
fragID := uint8(0)
fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
frags := make([]UDPMessage, fragCount)
for off < len(fullPayload) {
payloadSize := len(fullPayload) - off
if payloadSize > maxPayloadSize {
payloadSize = maxPayloadSize
}
frag := *m
frag.FragID = fragID
frag.FragCount = fragCount
frag.Data = fullPayload[off : off+payloadSize]
frags[fragID] = frag
off += payloadSize
fragID++
}
return frags
}
// Defragger handles the defragmentation of UDP messages.
// The current implementation can only handle one packet ID at a time.
// If another packet arrives before a packet has received all fragments
// in their entirety, any previous state is discarded.
type Defragger struct {
pktID uint16
frags []*UDPMessage
count uint8
size int // data size
}
func (d *Defragger) Feed(m *UDPMessage) *UDPMessage {
if m.FragCount <= 1 {
return m
}
if m.FragID >= m.FragCount {
// wtf is this?
return nil
}
if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
// new message, clear previous state
d.pktID = m.PacketID
d.frags = make([]*UDPMessage, m.FragCount)
d.frags[m.FragID] = m
d.count = 1
d.size = len(m.Data)
} else if d.frags[m.FragID] == nil {
d.frags[m.FragID] = m
d.count++
d.size += len(m.Data)
if int(d.count) == len(d.frags) {
// all fragments received, assemble
data := make([]byte, d.size)
off := 0
for _, frag := range d.frags {
off += copy(data[off:], frag.Data)
}
m.Data = data
m.FragID = 0
m.FragCount = 1
return m
}
}
return nil
}
+75 -4
View File
@@ -8,6 +8,7 @@ import (
"github.com/apernet/quic-go/quicvarint" "github.com/apernet/quic-go/quicvarint"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet/hysteria"
) )
const ( const (
@@ -17,8 +18,6 @@ const (
MaxMessageLength = 2048 MaxMessageLength = 2048
MaxPaddingLength = 4096 MaxPaddingLength = 4096
MaxUDPSize = 4096
maxVarInt1 = 63 maxVarInt1 = 63
maxVarInt2 = 16383 maxVarInt2 = 16383
maxVarInt4 = 1073741823 maxVarInt4 = 1073741823
@@ -62,7 +61,7 @@ func ReadTCPRequest(r io.Reader) (string, error) {
} }
func WriteTCPRequest(w io.Writer, addr string) error { func WriteTCPRequest(w io.Writer, addr string) error {
padding := tcpRequestPadding.String() padding := hysteria.TcpRequestPadding.String()
paddingLen := len(padding) paddingLen := len(padding)
addrLen := len(addr) addrLen := len(addr)
sz := int(quicvarint.Len(uint64(addrLen))) + addrLen + sz := int(quicvarint.Len(uint64(addrLen))) + addrLen +
@@ -122,7 +121,7 @@ func ReadTCPResponse(r io.Reader) (bool, string, error) {
} }
func WriteTCPResponse(w io.Writer, ok bool, msg string) error { func WriteTCPResponse(w io.Writer, ok bool, msg string) error {
padding := tcpResponsePadding.String() padding := hysteria.TcpResponsePadding.String()
paddingLen := len(padding) paddingLen := len(padding)
msgLen := len(msg) msgLen := len(msg)
sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen + sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +
@@ -247,3 +246,75 @@ func varintPut(b []byte, i uint64) int {
} }
panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i))
} }
func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage {
if m.Size() <= maxSize {
return []UDPMessage{*m}
}
fullPayload := m.Data
maxPayloadSize := maxSize - m.HeaderSize()
off := 0
fragID := uint8(0)
fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
frags := make([]UDPMessage, fragCount)
for off < len(fullPayload) {
payloadSize := len(fullPayload) - off
if payloadSize > maxPayloadSize {
payloadSize = maxPayloadSize
}
frag := *m
frag.FragID = fragID
frag.FragCount = fragCount
frag.Data = fullPayload[off : off+payloadSize]
frags[fragID] = frag
off += payloadSize
fragID++
}
return frags
}
// Defragger handles the defragmentation of UDP messages.
// The current implementation can only handle one packet ID at a time.
// If another packet arrives before a packet has received all fragments
// in their entirety, any previous state is discarded.
type Defragger struct {
pktID uint16
frags []*UDPMessage
count uint8
size int // data size
}
func (d *Defragger) Feed(m *UDPMessage) *UDPMessage {
if m.FragCount <= 1 {
return m
}
if m.FragID >= m.FragCount {
// wtf is this?
return nil
}
if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
// new message, clear previous state
d.pktID = m.PacketID
d.frags = make([]*UDPMessage, m.FragCount)
d.frags[m.FragID] = m
d.count = 1
d.size = len(m.Data)
} else if d.frags[m.FragID] == nil {
d.frags[m.FragID] = m
d.count++
d.size += len(m.Data)
if int(d.count) == len(d.frags) {
// all fragments received, assemble
data := make([]byte, d.size)
off := 0
for _, frag := range d.frags {
off += copy(data[off:], frag.Data)
}
m.Data = data
m.FragID = 0
m.FragCount = 1
return m
}
}
return nil
}

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