Compare commits

...
79 Commits
Author SHA1 Message Date
Fangliding 212d1b108b handle localhost 2026-06-05 21:08:34 +08:00
Fangliding 2101c1a99f use netip 2026-06-05 18:36:53 +08:00
Fangliding e1fe8be7a5 typo 2026-06-05 18:30:38 +08:00
Fangliding bbc8103a7d Bypass default interface for loopback 2026-06-05 17:16:53 +08:00
fdb9b616fc README.md: Add Hey to HarmonyOS in GUI Clients (#6244)
https://github.com/popsiclelmlm/Hey/blob/main/entry/src/main/cpp/README.md

---------

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

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

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-02 23:36:42 +00:00
RPRXandGitHub 94ffd50060 Xray-core v26.6.1
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

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

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-06-01 02:11:09 +00:00
风扇滑翔翼andGitHub c4dfcd4c1a Burst observatory: Fix init check (#6221)
https://github.com/XTLS/Xray-core/pull/6106#issuecomment-4587975865
2026-06-01 01:30:40 +00:00
cb8cd048c1 DNS outbound: Replace "reject" with "return" (rCode is 0 by default) (#6214)
https://github.com/XTLS/Xray-core/pull/6214#issuecomment-4587988752

Example: https://github.com/XTLS/Xray-core/pull/6214#issue-4553786283

---------

Co-authored-by: Meo597 <197331664+Meo597@users.noreply.github.com>
2026-06-01 01:25:47 +00:00
DavoyanandGitHub 455f6bc2d5 uTLS: Update ModernFingerprints map and OtherFingerprints map (#6181)
Paying attention to https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533

And a real issue: https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4583018017
2026-05-30 17:29:33 +00:00
LjhAUMEMandGitHub ba538619eb Realm finalmask: Fix client punch peers (#6213)
Fixes https://github.com/XTLS/Xray-core/pull/6137
2026-05-30 09:30:23 +00:00
𐲓𐳛𐳪𐳂𐳐 𐲀𐳢𐳦𐳫𐳢 𐲥𐳔𐳛𐳪𐳌𐳑𐳖𐳇andGitHub d43a808ea5 GitHub Action CI: Add Go source file format check (#6090)
https://github.com/XTLS/Xray-core/pull/6057#issuecomment-4364819830

And https://github.com/XTLS/Xray-core/pull/6149#issuecomment-4546876261
2026-05-29 15:04:59 +00:00
ca4b156b57 Burst observatory: Fix time compare, cancel pending ping on instance close or new schedule started (#6106)
https://github.com/XTLS/Xray-core/pull/6106#issuecomment-4448117773

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-05-29 11:59:24 +00:00
LjhAUMEMandRPRX aba22722a6 Finalmask: Add mkcp-legacy (UDP) to replace mkcp-* and legacy header-* (#6201)
https://github.com/XTLS/Xray-core/pull/6193#issuecomment-4543737699

https://github.com/XTLS/Xray-core/pull/6201#issuecomment-4574213827

Example: https://github.com/XTLS/Xray-core/pull/6201#issue-4525483699
2026-05-29 11:57:32 +00:00
569459c54c Sudoku finalmask: Harden UDP ReadFrom() against invalid packets (#6185)
https://github.com/XTLS/Xray-core/issues/6184#issuecomment-4573949756

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

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-05-29 10:47:58 +00:00
LjhAUMEMandGitHub 2b42699623 XICMP finalmask: Refactor & Speed up; Add multi ips and dgram mode (client) (#6168)
https://github.com/XTLS/Xray-core/pull/5872#issuecomment-4198514120
https://github.com/XTLS/Xray-core/pull/6168#issuecomment-4571606666
https://github.com/XTLS/Xray-core/pull/6168#issuecomment-4573294637

Example: https://github.com/XTLS/Xray-core/pull/6168#issue-4484980927

Closes https://github.com/XTLS/Xray-core/discussions/5879
2026-05-29 09:44:06 +00:00
LjhAUMEMandRPRX 66a8100737 Salamander finalmask: Support packetSize (Gecko in Hysteria v2.9.2) (#6198)
And some other refinements

https://github.com/XTLS/Xray-core/pull/6198#issuecomment-4567438813

Example: https://github.com/XTLS/Xray-core/pull/6198#issue-4522226670
2026-05-29 08:51:41 +00:00
LjhAUMEMandGitHub 36303694d1 Finalmask: Add Realm (UDP hole punching in Hysteria v2.9.1) (#6137)
https://github.com/XTLS/Xray-core/pull/5657#issuecomment-4446406536

https://github.com/XTLS/Xray-core/pull/6137#issuecomment-4469822775

Example: https://github.com/XTLS/Xray-core/pull/6137#issue-4454013510
2026-05-29 08:29:53 +00:00
Jason FoxandGitHub 4dcf802ae3 README.md: Add flutter_vless to Xray Wrapper in Others (#6197)
https://pub.dev/packages/flutter_vless
2026-05-28 20:15:38 +00:00
MeowandGitHub a2cec2e580 DNS: Avoid panic on domain too long (#6207)
Fixes https://github.com/XTLS/Xray-core/issues/6204
2026-05-28 20:06:32 +00:00
LjhAUMEMandGitHub 1cd7d25fec header-custom finalmask: Remove headerConnMode headerReadAddrAware interface (#6193)
https://github.com/XTLS/Xray-core/pull/5920#issuecomment-4253108964

https://github.com/XTLS/Xray-core/pull/6193#issuecomment-4531875088
2026-05-28 19:44:16 +00:00
LjhAUMEMandGitHub 09002ab763 noise finalmask: Better reset (#6188)
https://github.com/XTLS/Xray-core/pull/6170

Explanation: https://github.com/XTLS/Xray-core/pull/5657#issue-3901983829
2026-05-28 19:24:22 +00:00
cb206ddc74 DNS: Avoid passing domain to WriteTo func (#6163)
https://github.com/XTLS/Xray-core/pull/6163#issuecomment-4528771758

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-05-28 18:27:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fa466f8174 Bump golang.org/x/net from 0.54.0 to 0.55.0 (#6192)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.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-28 18:20:27 +00:00
e26f5e9548 Socks5 server: More standard UDP ASSOCIATE (RFC 1928) (#6149)
https://github.com/XTLS/Xray-core/pull/6149#issuecomment-4529069218

Fixes https://github.com/XTLS/Xray-core/issues/6145#issuecomment-4467482623

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-05-28 14:22:57 +00:00
风扇滑翔翼andRPRX 787aa7677b Hysteria server: tls.WithNextProto("h3") by default (#6186)
Client: https://github.com/XTLS/Xray-core/pull/6186#issuecomment-4528041838
2026-05-24 14:35:12 +00:00
风扇滑翔翼andGitHub d878fc83f8 SS2022 inbound: Fix potential panic when dispatching (#6162)
https://github.com/XTLS/Xray-core/pull/6162#issuecomment-4496738737
2026-05-24 12:50:02 +00:00
风扇滑翔翼andGitHub ee2b2c5ab6 leastLoad balancer: Implement missing tolerance (#6156)
Closes https://github.com/XTLS/Xray-core/issues/6154
2026-05-24 12:27:12 +00:00
Whale ChoiandGitHub 81d993f49d README.md: Remove Xray4Magisk; Add AsteriskNG to Android in GUI Clients (#6153)
https://github.com/XTLS/Xray-core/pull/6153#issuecomment-4522913987
2026-05-24 12:19:00 +00:00
风扇滑翔翼andGitHub 4c3842711d XHTTP client: Fix packet-up OpenUsage counting and H3 keepAlivePeriod parameter (#6140)
Fixes https://github.com/XTLS/Xray-core/pull/6140#issuecomment-4525409315
2026-05-24 12:05:40 +00:00
MeowandGitHub ab69985fcc Config: Warn when sockopt.trustedXForwardedFor is not set for XHTTP/WS/HU inbounds (#6159)
https://github.com/XTLS/Xray-core/pull/6110#issuecomment-4470157219

Usage: https://github.com/XTLS/Xray-core/pull/5331#issue-3655317949
2026-05-23 13:50:01 +00:00
风扇滑翔翼andGitHub 56bb63668c Geodata: Cleanup unneeded shared matchers (weakly referenced in map) (#6139)
https://github.com/XTLS/Xray-core/pull/6139#issuecomment-4525195265
2026-05-23 12:23:52 +00:00
bytecategoryandGitHub 359a28f876 XDNS finalmask: Support AAAA & A (#6123)
https://github.com/XTLS/Xray-core/pull/6123#issuecomment-4439994266
https://github.com/XTLS/Xray-core/pull/6123#issuecomment-4441819696
https://github.com/XTLS/Xray-core/pull/6123#issuecomment-4522352460

Example: https://github.com/XTLS/Xray-core/pull/6123#issue-4436283689

Document: https://xtls.github.io/config/transports/finalmask.html#xdns
2026-05-23 09:24:37 +00:00
Yury KastovandGitHub da9ba693cb Config: Support abstract unix sockets in remote loader (#6111)
Replaces https://github.com/XTLS/Xray-core/pull/5200
2026-05-22 19:49:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5488b86c67 Bump google.golang.org/grpc from 1.81.0 to 1.81.1 (#6133)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.0 to 1.81.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.81.0...v1.81.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.81.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-22 19:37:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a3c054a54c Bump golang.org/x/net from 0.53.0 to 0.54.0 (#6113)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.54.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-22 19:37:25 +00:00
RPRXandGitHub 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
fanymagnetandGitHub 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
风扇滑翔翼andGitHub 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
MeowandRPRX 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
MeowandRPRX 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
MeowandRPRX 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
MeowandRPRX 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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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
RPRXandGitHub 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
LjhAUMEMandGitHub 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
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
LjhAUMEMandGitHub 756a2d1327 Hysteria client: Fix sendThrough (#6063)
And fixes https://github.com/XTLS/Xray-core/issues/6046
2026-05-03 07:18:23 +00:00
RPRXandGitHub 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
KostaandGitHub 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
MeowandGitHub 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 KastovandGitHub 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
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
LjhAUMEMandGitHub 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
LjhAUMEMandGitHub 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
风扇滑翔翼andGitHub 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
风扇滑翔翼andGitHub 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
风扇滑翔翼andGitHub 7ab0a3ccb7 FakeDNS: Little fix (#6022)
https://github.com/XTLS/Xray-core/pull/6021#issuecomment-4344638838
2026-05-01 22:57:51 +00:00
yiguodevandGitHub 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
ZeroandGitHub 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
yiguodevandGitHub 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
hiDandelionandGitHub 5b552db781 README.md: Add Anywhere to Others/iOS (#5762)
https://t.me/projectXtls/2237
2026-05-01 22:34:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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
RPRXandGitHub 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
RPRXandGitHub 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
RPRXandGitHub 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
MeowandGitHub 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
LjhAUMEMandRPRX 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-MSDSMandGitHub 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
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
MeowandGitHub 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
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
风扇滑翔翼andGitHub 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
MeowandGitHub 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
MeowandGitHub 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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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
330 changed files with 10896 additions and 6049 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/05_inbounds.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/08_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/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
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
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=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+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/05_inbounds.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/08_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/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
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
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=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+2
View File
@@ -11,6 +11,7 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
@@ -75,6 +76,7 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env:
GOOS: ${{ matrix.goos}}
GOARCH: ${{ matrix.goarch }}
+5
View File
@@ -11,6 +11,7 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
@@ -161,6 +162,7 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
@@ -208,6 +210,9 @@ jobs:
go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
# The line below is for without running conhost.exe version. Commented for not being used. Provided for reference.
# go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
elif [[ ${GOOS} == 'android' ]]; then
echo 'Building Xray for Android...'
go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0" -v ./main
else
echo 'Building Xray...'
if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then
+23 -1
View File
@@ -1,4 +1,4 @@
name: Test
name: Tests and Checkings
on:
push:
@@ -8,6 +8,7 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
@@ -36,6 +37,7 @@ jobs:
check-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v6
@@ -50,8 +52,28 @@ jobs:
fi
done
check-format:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions:
contents: read
steps:
- name: Checkout codebase
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: false
- name: Check Format
run: |
go install -v mvdan.cc/gofumpt@latest
go run ./infra/vformat/main.go -mode check -pwd ./
test:
needs: check-assets
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions:
contents: read
runs-on: ${{ matrix.os }}
+16 -3
View File
@@ -73,8 +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)
- [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- Magisk
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk)
- [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
- Homebrew
- `brew install xray`
@@ -111,6 +109,8 @@
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- [XrayUI-dev](https://github.com/PhoenixNil/XrayUI-dev)
- Android
- [v2rayNG](https://github.com/2dust/v2rayNG)
- [X-flutter](https://github.com/XTLS/X-flutter)
@@ -118,7 +118,8 @@
- [SimpleXray](https://github.com/lhear/SimpleXray)
- [XrayFA](https://github.com/Q7DF1/XrayFA)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- [OneXray](https://github.com/OneXray/OneXray)
- [AsteriskNG](https://github.com/Asterisk4Magisk/AsteriskNG)
- 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)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064)
@@ -143,10 +144,14 @@
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- HarmonyOS
- [Hey](https://github.com/popsiclelmlm/Hey)
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
- iOS & macOS arm64 & tvOS
- [Anywhere](https://github.com/NodePassProject/Anywhere)
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
- [Loon](https://apps.apple.com/us/app/loon/id1373567447)
- [Egern](https://apps.apple.com/us/app/egern/id1616105820)
@@ -159,6 +164,7 @@
- [xtls-sdk](https://github.com/remnawave/xtls-sdk)
- [xtlsapi](https://github.com/hiddify/xtlsapi)
- [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
- [flutter_vless](https://github.com/XIIIFOX/flutter_vless)
- [Xray-core-python](https://github.com/LorenEteval/Xray-core-python)
- [xray-api](https://github.com/XVGuardian/xray-api)
- [XrayR](https://github.com/XrayR-project/XrayR)
@@ -203,6 +209,13 @@ Make sure that you are using the same Go version, and remember to set the git co
CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main
```
For Android:
```bash
GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
```
If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead:
```bash
+19 -5
View File
@@ -3,6 +3,7 @@ package commander
import (
"context"
"net"
"strings"
"sync"
"github.com/xtls/xray-core/common"
@@ -10,6 +11,7 @@ import (
"github.com/xtls/xray-core/common/signal/done"
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/transport/internet"
"google.golang.org/grpc"
)
@@ -67,20 +69,32 @@ func (c *Commander) Start() error {
}
c.Unlock()
var listen = func(listener net.Listener) {
listen := func(listener net.Listener) {
if err := c.server.Serve(listener); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start grpc server")
}
}
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)
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
}
+4 -3
View File
@@ -139,7 +139,8 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
if lenAfter == 0 {
if c.highWatermark >= minSizeForEmptyRebuild {
errors.LogDebug(context.Background(), c.name,
errors.LogDebug(
context.Background(), c.name,
" rebuilding empty cache map to reclaim memory.",
" size_before_cleanup=", lenBefore,
" peak_size_before_rebuild=", c.highWatermark,
@@ -153,7 +154,8 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold &&
float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold {
errors.LogDebug(context.Background(), c.name,
errors.LogDebug(
context.Background(), c.name,
" shrinking cache map to reclaim memory.",
" new_size=", lenAfter,
" peak_size_before_shrink=", c.highWatermark,
@@ -165,7 +167,6 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
c.highWatermark = lenAfter
go c.migrate()
}
}
type migrationEntry struct {
+31 -92
View File
@@ -5,18 +5,16 @@ import (
"context"
go_errors "errors"
"fmt"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/xtls/xray-core/common"
"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/session"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/dns"
)
@@ -88,7 +86,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
return nil, errors.New("failed to create hosts").Base(err)
}
var defaultTag = config.Tag
defaultTag := config.Tag
if len(config.Tag) == 0 {
defaultTag = generateRandomTag()
}
@@ -141,7 +139,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
serveExpiredTTL = *ns.ServeExpiredTTL
}
var tag = defaultTag
tag := defaultTag
if len(ns.Tag) > 0 {
tag = ns.Tag
}
@@ -158,9 +156,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, client)
}
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
var domainMatcher geodata.DomainMatcher
if len(effectiveRules) > 0 {
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
@@ -220,7 +221,7 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
}
if s.checkSystem {
supportIPv4, supportIPv6 := checkRoutes()
supportIPv4, supportIPv6 := utils.CheckRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6
} else {
@@ -271,25 +272,27 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching
hasMatch := false
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
if s.domainMatcher != nil {
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
}
@@ -534,67 +537,3 @@ func init() {
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
}
+8 -8
View File
@@ -148,7 +148,7 @@ func TestUDPServerSubnet(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -210,7 +210,7 @@ func TestUDPServer(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -350,7 +350,7 @@ func TestPrioritizedDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -421,7 +421,7 @@ func TestUDPServerIPv6(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -490,7 +490,7 @@ func TestStaticHostDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -577,7 +577,7 @@ func TestIPMatch(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -696,7 +696,7 @@ func TestLocalDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -953,7 +953,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+9 -4
View File
@@ -127,15 +127,20 @@ func genEDNS0Options(clientIP net.IP, padding int) *dnsmessage.Resource {
return opt
}
func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest {
func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) ([]*dnsRequest, error) {
name, err := dnsmessage.NewName(domain)
if err != nil {
return nil, err
}
qA := dnsmessage.Question{
Name: dnsmessage.MustNewName(domain),
Name: name,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
}
qAAAA := dnsmessage.Question{
Name: dnsmessage.MustNewName(domain),
Name: name,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
}
@@ -175,7 +180,7 @@ func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() ui
})
}
return reqs
return reqs, nil
}
// parseResponse parses DNS answers from the returned payload
+11 -3
View File
@@ -2,6 +2,7 @@ package dns
import (
"math/rand"
"strings"
"testing"
"time"
@@ -24,7 +25,8 @@ func Test_parseResponse(t *testing.T) {
ans = new(dns.Msg)
ans.Id = 1
ans.Answer = append(ans.Answer,
ans.Answer = append(
ans.Answer,
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")),
common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")),
common.Must2(dns.NewRR("google.com. IN A 8.8.8.8")),
@@ -34,7 +36,8 @@ func Test_parseResponse(t *testing.T) {
ans = new(dns.Msg)
ans.Id = 2
ans.Answer = append(ans.Answer,
ans.Answer = append(
ans.Answer,
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")),
common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")),
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")),
@@ -131,10 +134,15 @@ func Test_buildReqMsgs(t *testing.T) {
IPv6Enable: false,
FakeEnable: false,
}, nil}, 0},
{"name too long", args{strings.Repeat("a", 256), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
}, nil}, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) {
if got, _ := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) {
t.Errorf("buildReqMsgs() = %v, want %v", got, tt.want)
}
})
+8 -12
View File
@@ -17,7 +17,7 @@ import (
type Holder struct {
domainToIP cache.Lru
ipRange *net.IPNet
mu *sync.Mutex
mu sync.Mutex
config *FakeDnsPool
}
@@ -49,9 +49,7 @@ func (fkdns *Holder) Start() error {
}
func (fkdns *Holder) Close() error {
fkdns.domainToIP = nil
fkdns.ipRange = nil
fkdns.mu = nil
// nothing to do for now, just wait GC
return nil
}
@@ -70,7 +68,7 @@ func NewFakeDNSHolder() (*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 {
@@ -92,7 +90,6 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
}
fkdns.domainToIP = cache.NewLru(lruSize)
fkdns.ipRange = ipRange
fkdns.mu = new(sync.Mutex)
return nil
}
@@ -103,7 +100,7 @@ func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
if v, ok := fkdns.domainToIP.Get(domain); ok {
return []net.Address{v.(net.Address)}
}
currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
currentTimeMillis := uint64(time.Now().UnixMilli())
ones, bits := fkdns.ipRange.Mask.Size()
rooms := bits - ones
if rooms < 64 {
@@ -202,12 +199,11 @@ func (h *HolderMulti) Start() error {
}
func (h *HolderMulti) Close() error {
var errs []error
for _, v := range h.holders {
if err := v.Close(); err != nil {
return errors.New("Cannot close all fake dns pools").Base(err)
}
errs = append(errs, v.Close())
}
return nil
return errors.Combine(errs...)
}
func (h *HolderMulti) createHolderGroups() error {
@@ -222,7 +218,7 @@ func (h *HolderMulti) createHolderGroups() error {
}
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
holderMulti := &HolderMulti{nil, conf}
holderMulti := &HolderMulti{config: conf}
if err := holderMulti.createHolderGroups(); err != nil {
return nil, err
}
+10 -9
View File
@@ -129,15 +129,16 @@ func TestFakeDnsHolderCreateMappingAndRollOver(t *testing.T) {
}
func TestFakeDNSMulti(t *testing.T) {
fakeMulti, err := NewFakeDNSHolderMulti(&FakeDnsPoolMulti{
Pools: []*FakeDnsPool{{
IpPool: "240.0.0.0/12",
LruSize: 256,
}, {
IpPool: "fddd:c5b4:ff5f:f4f0::/64",
LruSize: 256,
}},
},
fakeMulti, err := NewFakeDNSHolderMulti(
&FakeDnsPoolMulti{
Pools: []*FakeDnsPool{{
IpPool: "240.0.0.0/12",
LruSize: 256,
}, {
IpPool: "fddd:c5b4:ff5f:f4f0::/64",
LruSize: 256,
}},
},
)
common.Must(err)
+14 -11
View File
@@ -13,8 +13,8 @@ import (
// StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct {
reps [][]net.Address
matcher geodata.DomainMatcher
responses [][]net.Address
matcher geodata.DomainMatcher
}
// NewStaticHosts creates a new StaticHosts instance.
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
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)
rules = append(rules, mapping.Domain)
}
if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
reps: reps,
matcher: matcher,
responses: reps,
matcher: matcher,
}, nil
}
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0)
found := false
for _, ruleIdx := range h.matcher.Match(domain) {
for _, rep := range h.reps[ruleIdx] {
for _, idx := range h.matcher.Match(domain) {
for _, rep := range h.responses[idx] {
if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
return nil, err
}
}
ips = append(ips, h.reps[ruleIdx]...)
ips = append(ips, h.responses[idx]...)
found = true
}
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.
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)
}
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"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/features/dns"
"github.com/xtls/xray-core/features/routing"
@@ -134,7 +135,7 @@ func NewClient(
}
}
var timeoutMs = 4000 * time.Millisecond
timeoutMs := 4000 * time.Millisecond
if ns.TimeoutMs > 0 {
timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond
}
@@ -166,7 +167,7 @@ func (c *Client) Name() string {
// 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) {
if c.checkSystem {
supportIPv4, supportIPv6 := checkRoutes()
supportIPv4, supportIPv6 := utils.CheckRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6
} else {
+20 -2
View File
@@ -137,14 +137,32 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
if s.Name()+"." == "DOH//"+fqdn {
errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead")
if noResponseErrCh != nil {
noResponseErrCh <- errors.New("tries to resolve itself!", s.Name())
err := errors.New("tries to resolve itself!", s.Name())
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
// As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467
// Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300))))
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300))))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
-1
View File
@@ -18,7 +18,6 @@ type LocalNameServer struct {
// QueryIP implements Server.
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) {
start := time.Now()
ips, ttl, err = s.client.LookupIP(domain, option)
+13 -1
View File
@@ -78,7 +78,19 @@ func (s *QUICNameServer) getCacheController() *CacheController { return s.cacheC
func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying: ", fqdn)
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
+13 -1
View File
@@ -113,7 +113,19 @@ func (s *TCPNameServer) getCacheController() *CacheController {
func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn)
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
+13 -5
View File
@@ -131,8 +131,6 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot
newReq.msg = &newMsg
s.addPendingRequest(&newReq)
b, _ := dns.PackMessage(newReq.msg)
copyDest := net.UDPDestination(s.address.Address, s.address.Port)
b.UDP = &copyDest
s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b)
return
}
@@ -163,7 +161,19 @@ func (s *ClassicNameServer) getCacheController() *CacheController {
func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn)
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
for _, req := range reqs {
udpReq := &udpDnsRequest{
@@ -179,8 +189,6 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<
}
return
}
copyDest := net.UDPDestination(s.address.Address, s.address.Port)
b.UDP = &copyDest
s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b)
}
}
+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))
}))
}
-1
View File
@@ -85,7 +85,6 @@ func (p *MetricsHandler) Type() interface{} {
}
func (p *MetricsHandler) Start() error {
// direct listen a port if listen is set
if p.listen != "" {
TCPlistener, err := net.Listen("tcp", p.listen)
-2
View File
@@ -2,7 +2,6 @@ package burst
import (
"context"
"sync"
"github.com/xtls/xray-core/app/observatory"
@@ -72,7 +71,6 @@ func (o *Observer) Start() error {
o.hp.StartScheduler(func() ([]string, error) {
hs, ok := o.ohm.(outbound.HandlerSelector)
if !ok {
return nil, errors.New("outbound.Manager is not a HandlerSelector")
}
+42 -24
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/dice"
@@ -24,11 +25,12 @@ type HealthPingSettings struct {
// HealthPing is the health checker for balancers
type HealthPing struct {
ctx context.Context
dispatcher routing.Dispatcher
access sync.Mutex
ticker *time.Ticker
tickerClose chan struct{}
ctx context.Context
cancelCtx context.CancelFunc
cancelPending atomic.Pointer[context.CancelFunc]
dispatcher routing.Dispatcher
access sync.Mutex
ticker *time.Ticker
Settings *HealthPingSettings
Results map[string]*HealthPingRTTS
@@ -62,10 +64,10 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H
settings.Destination = "https://connectivitycheck.gstatic.com/generate_204"
}
if settings.Interval == 0 {
settings.Interval = time.Duration(1) * time.Minute
} else if settings.Interval < 10 {
settings.Interval = 1 * time.Minute
} else if settings.Interval < 10*time.Second {
errors.LogWarning(ctx, "health check interval is too small, 10s is applied")
settings.Interval = time.Duration(10) * time.Second
settings.Interval = 10 * time.Second
}
if settings.SamplingCount <= 0 {
settings.SamplingCount = 10
@@ -73,10 +75,12 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H
if settings.Timeout <= 0 {
// results are saved after all health pings finish,
// a larger timeout could possibly makes checks run longer
settings.Timeout = time.Duration(5) * time.Second
settings.Timeout = 5 * time.Second
}
ctx, cancel := context.WithCancel(ctx)
return &HealthPing{
ctx: ctx,
cancelCtx: cancel,
dispatcher: dispatcher,
Settings: settings,
Results: nil,
@@ -90,9 +94,9 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
}
interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
ticker := time.NewTicker(interval)
tickerClose := make(chan struct{})
h.ticker = ticker
h.tickerClose = tickerClose
// init run to get a fast check result
go func() {
tags, err := selector()
if err != nil {
@@ -110,13 +114,20 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
errors.LogWarning(h.ctx, "error select outbounds for scheduled health check: ", err)
return
}
h.doCheck(tags, interval, h.Settings.SamplingCount)
subCtx, cancel := context.WithCancel(h.ctx)
old := h.cancelPending.Swap(&cancel)
if old != nil {
errors.LogDebug(h.ctx, "scheduled health check not finished before next round, canceling previous one")
(*old)()
}
h.doCheck(subCtx, tags, interval, h.Settings.SamplingCount)
h.cancelPending.CompareAndSwap(&cancel, nil)
h.Cleanup(tags)
}()
select {
case <-ticker.C:
continue
case <-tickerClose:
case <-h.ctx.Done():
return
}
}
@@ -130,8 +141,7 @@ func (h *HealthPing) StopScheduler() {
}
h.ticker.Stop()
h.ticker = nil
close(h.tickerClose)
h.tickerClose = nil
h.cancelCtx()
}
// Check implements the HealthChecker
@@ -140,7 +150,7 @@ func (h *HealthPing) Check(tags []string) error {
return nil
}
errors.LogInfo(h.ctx, "perform one-time health check for tags ", tags)
h.doCheck(tags, 0, 1)
h.doCheck(h.ctx, tags, 0, 1)
return nil
}
@@ -151,13 +161,14 @@ type rtt struct {
// doCheck performs the 'rounds' amount checks in given 'duration'. You should make
// sure all tags are valid for current balancer
func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) {
// cancel ctx will stop all pending checks
func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.Duration, rounds int) {
count := len(tags) * rounds
if count == 0 {
return
}
ch := make(chan *rtt, count)
timers := make([]*time.Timer, 0, count)
for _, tag := range tags {
handler := tag
client := newPingClient(
@@ -172,7 +183,7 @@ func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int)
if duration > 0 {
delay = time.Duration(dice.RollInt63n(int64(duration)))
}
time.AfterFunc(delay, func() {
timers = append(timers, time.AfterFunc(delay, func() {
errors.LogDebug(h.ctx, "checking ", handler)
delay, err := client.MeasureDelay(h.Settings.HttpMethod)
if err == nil {
@@ -200,14 +211,21 @@ func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int)
handler: handler,
value: rttFailed,
}
})
}))
}
}
for i := 0; i < count; i++ {
rtt := <-ch
if rtt.value > 0 {
// should not put results when network is down
h.PutResult(rtt.handler, rtt.value)
select {
case rtt := <-ch:
if rtt.value > 0 {
// should not put results when network is down
h.PutResult(rtt.handler, rtt.value)
}
case <-ctx.Done():
for _, timer := range timers {
timer.Stop()
}
return
}
}
}
+2 -2
View File
@@ -59,7 +59,7 @@ func (h *HealthPingRTTS) Put(d time.Duration) {
if h.rtts == nil {
h.rtts = make([]*pingRTT, h.cap)
for i := 0; i < h.cap; i++ {
h.rtts[i] = &pingRTT{}
h.rtts[i] = &pingRTT{value: rttUntested}
}
h.idx = -1
}
@@ -88,7 +88,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
validRTTs := make([]time.Duration, 0)
for _, rtt := range h.rtts {
switch {
case rtt.value == 0 || time.Since(rtt.time) > h.validity:
case rtt.value == rttUntested || time.Since(rtt.time) > h.validity:
continue
case rtt.value == rttFailed:
stats.Fail++
+1 -1
View File
@@ -186,7 +186,7 @@ func (o *Observer) probe(outbound string) ProbeResult {
return nil
})
if err != nil {
var errorMessage = "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed"
errorMessage := "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed"
errors.LogInfoInner(o.ctx, errorCollectorForRequest.UnderlyingError(), errorMessage)
return ProbeResult{Alive: false, LastErrorReason: errorMessage}
}
+1 -1
View File
@@ -138,7 +138,7 @@ func (s *handlerServer) GetInboundUsers(ctx context.Context, request *GetInbound
if len(request.Email) > 0 {
return &GetInboundUserResponse{Users: []*protocol.User{protocol.ToProtoUser(um.GetUser(ctx, request.Email))}}, nil
}
var result = make([]*protocol.User, 0, 100)
result := make([]*protocol.User, 0, 100)
users := um.GetUsers(ctx)
for _, u := range users {
result = append(result, protocol.ToProtoUser(u))
+3 -3
View File
@@ -16,10 +16,10 @@ import (
// Manager manages all inbound handlers.
type Manager struct {
access sync.RWMutex
access sync.RWMutex
untaggedHandlers []inbound.Handler
taggedHandlers map[string]inbound.Handler
running bool
taggedHandlers map[string]inbound.Handler
running bool
}
// New returns a new Manager for inbound handlers.
+4 -6
View File
@@ -18,9 +18,9 @@ import (
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/proxy/hysteria/account"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
hysteria_proxy "github.com/xtls/xray-core/proxy/hysteria"
"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/tcp"
"github.com/xtls/xray-core/transport/internet/udp"
@@ -134,10 +134,8 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
func (w *tcpWorker) Start() error {
ctx := context.Background()
type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator }
if v, ok := w.proxy.(HysteriaInboundValidator); ok {
ctx = hyCtx.ContextWithRequireDatagram(ctx, true)
ctx = hyCtx.ContextWithValidator(ctx, v.HysteriaInboundValidator())
if v, ok := w.proxy.(*hysteria_proxy.Server); ok {
ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator())
}
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
+1 -2
View File
@@ -348,7 +348,7 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
}
}
//case addr.Family().IsDomain():
// case addr.Family().IsDomain():
default:
ob.Gateway = addr
@@ -396,7 +396,6 @@ func (h *Handler) ProxySettings() *serial.TypedMessage {
}
func ParseRandomIP(addr net.Address, prefix string) net.Address {
_, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix)
ones, bits := ipnet.Mask.Size()
+7 -8
View File
@@ -22,8 +22,8 @@ import (
)
func TestInterfaces(t *testing.T) {
_ = (outbound.Handler)(new(Handler))
_ = (outbound.Manager)(new(Manager))
_ = outbound.Handler(new(Handler))
_ = outbound.Manager(new(Manager))
}
const xrayKey core.XrayKey = 1
@@ -43,12 +43,12 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
}
v, _ := core.New(config)
v.AddFeature((outbound.Manager)(new(Manager)))
v.AddFeature(outbound.Manager(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v)
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
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))
_, ok := conn.(*stat.CounterConnection)
@@ -73,12 +73,12 @@ func TestOutboundWithStatCounter(t *testing.T) {
}
v, _ := core.New(config)
v.AddFeature((outbound.Manager)(new(Manager)))
v.AddFeature(outbound.Manager(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v)
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
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))
_, ok := conn.(*stat.CounterConnection)
@@ -88,7 +88,6 @@ func TestOutboundWithStatCounter(t *testing.T) {
}
func TestTagsCache(t *testing.T) {
test_duration := 10 * time.Second
threads_num := 50
delay := 10 * time.Millisecond
@@ -118,7 +117,7 @@ func TestTagsCache(t *testing.T) {
tag := fmt.Sprintf("%s%d", tags_prefix, idx)
cfg := &core.OutboundHandlerConfig{
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 err := ohm.AddHandler(ctx, h); err == nil {
-1
View File
@@ -162,7 +162,6 @@ func (m *Manager) ListHandlers(ctx context.Context) []outbound.Handler {
// Select implements outbound.HandlerSelector.
func (m *Manager) Select(selectors []string) []string {
key := strings.Join(selectors, ",")
if cache, ok := m.tagsCache.Load(key); ok {
return cache.([]string)
-1
View File
@@ -58,7 +58,6 @@ func (s *routingServer) AddRule(ctx context.Context, request *AddRuleRequest) (*
return &AddRuleResponse{}, bo.AddRule(request.Config, request.ShouldAppend)
}
return nil, errors.New("unsupported router implementation")
}
func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) {
+8 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
)
type Condition interface {
@@ -356,7 +357,13 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
var dstIP string
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()
dstPort = uint16(ctx.GetTargetPort())
}
-2
View File
@@ -110,7 +110,6 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
// AddRule implements routing.Router.
func (r *Router) AddRule(config *serial.TypedMessage, shouldAppend bool) error {
inst, err := config.GetInstance()
if err != nil {
return err
@@ -227,7 +226,6 @@ func (r *Router) RemoveRule(tag string) error {
return nil
}
return errors.New("empty tag name!")
}
// ListRule implements routing.Router
+22 -6
View File
@@ -3,6 +3,7 @@ package router
import (
"context"
"math"
"slices"
"sort"
"time"
@@ -77,7 +78,7 @@ func (s *LeastLoadStrategy) PickOutbound(candidates []string) string {
}
func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node {
qualified := s.getNodes(candidates, time.Duration(s.settings.MaxRTT))
qualified := s.getNodes(candidates)
selects := s.selectLeastLoad(qualified)
return selects
}
@@ -138,7 +139,7 @@ func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node {
return nodes[:count]
}
func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) []*node {
func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
if s.observer == nil {
errors.LogError(s.ctx, "observer is nil")
return make([]*node, 0)
@@ -151,12 +152,10 @@ func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration)
results := observeResult.(*observatory.ObservationResult)
outboundlist := outboundList(candidates)
var ret []*node
for _, v := range results.Status {
if v.Alive && (v.Delay < maxRTT.Milliseconds() || maxRTT == 0) && outboundlist.contains(v.OutboundTag) {
if s.shouldSelectNode(v, candidates) {
record := &node{
Tag: v.OutboundTag,
CountAll: 1,
@@ -172,8 +171,8 @@ func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration)
record.RTTDeviationCost = time.Duration(s.costs.Apply(v.OutboundTag, float64(v.HealthPing.Deviation)))
record.CountAll = int(v.HealthPing.All)
record.CountFail = int(v.HealthPing.Fail)
}
ret = append(ret, record)
}
}
@@ -182,6 +181,23 @@ func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration)
return ret
}
func (s *LeastLoadStrategy) shouldSelectNode(v *observatory.OutboundStatus, candidates []string) bool {
maxRTT := time.Duration(s.settings.MaxRTT)
if !v.Alive {
return false
}
if maxRTT != 0 && v.Delay >= maxRTT.Milliseconds() {
return false
}
if !slices.Contains(candidates, v.OutboundTag) {
return false
}
if v.HealthPing != nil && v.HealthPing.All > 0 && s.settings.Tolerance > 0 && float64(v.HealthPing.Fail)/float64(v.HealthPing.All) > float64(s.settings.Tolerance) {
return false
}
return true
}
func leastloadSort(nodes []*node) {
sort.Slice(nodes, func(i, j int) bool {
left := nodes[i]
+5
View File
@@ -85,6 +85,7 @@ func TestSelectLeastExpected(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastExpected2(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -102,6 +103,7 @@ func TestSelectLeastExpected2(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastExpectedAndBaselines(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -122,6 +124,7 @@ func TestSelectLeastExpectedAndBaselines(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -142,6 +145,7 @@ func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastLoadBaselines(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -160,6 +164,7 @@ func TestSelectLeastLoadBaselines(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
+3 -49
View File
@@ -7,61 +7,15 @@ import (
"io"
"net"
"net/http"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session"
)
// parseURL splits a webhook URL into an HTTP URL and an optional Unix socket
// path. For regular http/https URLs the input is returned unchanged with an
// empty socketPath. For Unix sockets the format is:
//
// /path/to/socket.sock:/http/path
// @abstract:/http/path
// @@padded:/http/path
//
// The :/ separator after the socket path delimits the HTTP request path.
// If omitted, "/" is used.
func parseURL(raw string) (httpURL, socketPath string) {
if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') {
return raw, ""
}
if idx := strings.Index(raw, ":/"); idx >= 0 {
return "http://localhost" + raw[idx+1:], raw[:idx]
}
return "http://localhost/", raw
}
// resolveSocketPath applies platform-specific transformations to a Unix
// socket path, matching the behaviour of the listen side in
// transport/internet/system_listener.go.
//
// For abstract sockets (prefix @) on Linux/Android:
// - single @ — used as-is (lock-free abstract socket)
// - double @@ — stripped to single @ and padded to
// syscall.RawSockaddrUnix{}.Path length (HAProxy compat)
func resolveSocketPath(path string) string {
if len(path) == 0 || path[0] != '@' {
return path
}
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
return path
}
if len(path) > 1 && path[1] == '@' {
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path))
copy(fullAddr, path[1:])
return string(fullAddr)
}
return path
}
func ptr[T any](v T) *T { return &v }
type event struct {
@@ -96,7 +50,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
return nil, nil
}
httpURL, socketPath := parseURL(cfg.Url)
httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url)
h := &WebhookNotifier{
url: httpURL,
deduplication: cfg.Deduplication,
@@ -107,7 +61,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
}
if socketPath != "" {
dialAddr := resolveSocketPath(socketPath)
dialAddr := utils.ResolveSocketPath(socketPath)
h.client.Transport = &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
func TestInterface(t *testing.T) {
_ = (stats.Manager)(new(Manager))
_ = stats.Manager(new(Manager))
}
func TestStatsChannelRunnable(t *testing.T) {
+3 -2
View File
@@ -2,10 +2,11 @@ package version
import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"strconv"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
)
type Version struct {
+2 -2
View File
@@ -38,8 +38,8 @@ func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
n := len(dest)
if n > 0 && !(dest)[n-1].IsFull() {
nBytes, _ := (dest)[n-1].Write(src)
if n > 0 && !dest[n-1].IsFull() {
nBytes, _ := dest[n-1].Write(src)
src = src[nBytes:]
}
+6 -6
View File
@@ -121,11 +121,11 @@ func TestPacketReader_ReadMultiBuffer(t *testing.T) {
}
func TestReaderInterface(t *testing.T) {
_ = (io.Reader)(new(ReadVReader))
_ = (Reader)(new(ReadVReader))
_ = io.Reader(new(ReadVReader))
_ = Reader(new(ReadVReader))
_ = (Reader)(new(BufferedReader))
_ = (io.Reader)(new(BufferedReader))
_ = (io.ByteReader)(new(BufferedReader))
_ = (io.WriterTo)(new(BufferedReader))
_ = Reader(new(BufferedReader))
_ = io.Reader(new(BufferedReader))
_ = io.ByteReader(new(BufferedReader))
_ = io.WriterTo(new(BufferedReader))
}
+1 -1
View File
@@ -19,7 +19,7 @@ func (r *posixReader) Init(bs []*Buffer) {
}
for idx, b := range bs {
iovecs = append(iovecs, syscall.Iovec{
Base: &(b.v[0]),
Base: &b.v[0],
})
iovecs[idx].SetLen(int(Size))
}
+66 -13
View File
@@ -8,6 +8,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
)
type DomainMatcher interface {
@@ -23,10 +24,54 @@ type DomainMatcherFactory interface {
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.
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()
for i, r := range rules {
switch v := r.Value.(type) {
@@ -57,25 +102,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
if err := g.Build(); err != nil {
return nil, err
}
if key != "" {
f.shared.Store(key, g)
}
return g, nil
}
type CompactDomainMatcherFactory struct {
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
f.Lock()
defer f.Unlock()
if m := f.shared[key]; m != nil {
return m, nil
if s, ok := f.shared.Load(key); ok {
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)
if err != nil {
return nil, err
@@ -87,16 +137,19 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
g.Add(m, 0)
s.Add(m)
}
f.shared[key] = g
return g, err
f.shared.Store(key, s)
return s, err
}
// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
@@ -126,7 +179,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherGroup
matchers []strmatcher.MatcherSet
values []uint32
}
@@ -178,8 +231,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
default:
return &MphDomainMatcherFactory{}
return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
}
}
+8 -6
View File
@@ -7,10 +7,11 @@ import (
"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: make(map[string]strmatcher.MatcherGroup)}
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"}}},
@@ -31,7 +32,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
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"}}},
@@ -50,10 +51,11 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
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)
}
+79 -2
View File
@@ -1,11 +1,59 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
)
type DomainRegistry struct {
factory DomainMatcherFactory
mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
}
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 {
@@ -15,3 +63,32 @@ func newDomainRegistry() *DomainRegistry {
}
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
}
+19 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"go4.org/netipx"
)
@@ -806,7 +807,7 @@ func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) {
type IPSetFactory struct {
sync.Mutex
shared map[string]*IPSet // TODO: cleanup
shared *utils.WeakCacheMap[string, IPSet]
}
func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) {
@@ -815,9 +816,11 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
f.Lock()
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
}
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules {
@@ -833,7 +836,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
return nil
})
if err == nil {
f.shared[key] = ipset
f.shared.Store(key, ipset)
}
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)
}
var has4, has6 bool
var max4, max6 int
for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 {
max4 = b
}
}
for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 {
max6 = b
}
}
if max4 == 0 {
if !has4 {
max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
}
if max6 == 0 {
if !has6 {
max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
}
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
@@ -1007,3 +1017,7 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
return &HeuristicMultiIPMatcher{matchers: subs}, nil
}
}
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: utils.NewWeakCacheMap[string, IPSet]()}
}
+87 -3
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) {
matcher := buildIPMatcher(
"98.108.20.0/22",
@@ -231,7 +315,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
}
if !matcher.AnyMatch([]net.IP{
net.IP{},
{},
ip("1.1.1.1"),
ip("8.8.8.8"),
}) {
@@ -261,7 +345,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
if matcher.Matches([]net.IP{
ip("8.8.8.8"),
net.IP{},
{},
}) {
t.Fatal("expect Matches to be false when any IP is invalid")
}
@@ -278,7 +362,7 @@ func TestIPMatcherFilterIPs(t *testing.T) {
}
matched, unmatched := matcher.FilterIPs([]net.IP{
net.IP{},
{},
ip("8.8.8.8"),
ip("91.108.255.254"),
ip("1.1.1.1"),
+120 -2
View File
@@ -1,17 +1,135 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
}
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 {
return &IPRegistry{
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
ipsetFactory: newIPSetFactory(),
}
}
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
}
@@ -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)
}
+62 -4
View File
@@ -100,10 +100,6 @@ func (t Type) New(pattern string) (Matcher, error) {
case Substr:
return SubstrMatcher(pattern), nil
case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern)
@@ -288,3 +284,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
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)
}
}
}
+19 -1
View File
@@ -15,7 +15,7 @@ const (
)
// 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 returns the matcher's type.
Type() Type
@@ -101,3 +101,21 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found.
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
}
-1
View File
@@ -98,7 +98,6 @@ func (l *generalLogger) run() {
}
func (l *generalLogger) Handle(msg Message) {
select {
case l.buffer <- msg:
default:
+40 -2
View File
@@ -1,6 +1,7 @@
package filesystem
import (
"errors"
"io"
"os"
"path/filepath"
@@ -26,11 +27,48 @@ func ReadFile(path 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) {
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) {
+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"
UseVmessPadding = "xray.vmess.padding"
UseCone = "xray.cone.disabled"
UseStrictJSON = "xray.json.strict"
BufferSize = "xray.ray.buffer.size"
BrowserDialerAddress = "xray.browser.dialer"
+3 -2
View File
@@ -36,9 +36,10 @@ func TestNormalizeEnvName(t *testing.T) {
}
func TestEnvFlag(t *testing.T) {
if v := (EnvFlag{
v := EnvFlag{
Name: "xxxxx.y",
}.GetValueAsInt(10)); v != 10 {
}.GetValueAsInt(10)
if v != 10 {
t.Error("env value: ", v)
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ type ID struct {
// Equals returns true if this ID equals to the other one.
func (id *ID) Equals(another *ID) bool {
return id.uuid.Equals(&(another.uuid))
return id.uuid.Equals(&another.uuid)
}
func (id *ID) Bytes() []byte {
+1 -1
View File
@@ -229,7 +229,7 @@ func TestSniffQUICComplex(t *testing.T) {
t.Errorf("SniffQUIC() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (errors.Is(err, protocol.ErrProtoNeedMoreData)) != tt.needsMoreData {
if errors.Is(err, protocol.ErrProtoNeedMoreData) != tt.needsMoreData {
t.Errorf("SniffQUIC() error = %v, expectsNoClue %v", err, tt.needsMoreData)
return
}
+2 -2
View File
@@ -5,8 +5,8 @@ import (
)
type ServerSpec struct {
Destination net.Destination
User *MemoryUser
Destination net.Destination
User *MemoryUser
}
func NewServerSpec(dest net.Destination, user *MemoryUser) *ServerSpec {
-1
View File
@@ -228,7 +228,6 @@ func isValueKind(kind reflect.Kind) bool {
}
func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} {
if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok {
return r
}
+1 -3
View File
@@ -27,7 +27,6 @@ func TestMashalAccount(t *testing.T) {
j, ok := MarshalToJson(user, false)
if !ok || strings.Contains(j, "_TypedMessage_") {
t.Error("marshal account failed")
}
@@ -79,13 +78,12 @@ func TestMashalStruct(t *testing.T) {
v := (*f2.Arr)[0]["foo"]["hello"]
if f1.N != f2.N || *(f1.Np) != *(f2.Np) || f1.S != f2.S || v != "world" {
if f1.N != f2.N || *f1.Np != *f2.Np || f1.S != f2.S || v != "world" {
t.Error("f1 not equal to f2")
}
}
func TestMarshalConfigJson(t *testing.T) {
buf := bytes.NewBufferString(getConfig())
config, err := iserial.DecodeJSONConfig(buf)
if err != nil {
-29
View File
@@ -4,7 +4,6 @@ import (
"testing"
. "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
)
func TestGetInstance(t *testing.T) {
@@ -23,31 +22,3 @@ func TestConvertingNilMessage(t *testing.T) {
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())
}
}
+5 -4
View File
@@ -3,6 +3,7 @@ package singbridge
import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
@@ -17,14 +18,14 @@ func ToNetwork(network string) net.Network {
}
}
func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination {
func ToDestination(socksaddr M.Socksaddr, network net.Network) (net.Destination, error) {
// IsFqdn() implicitly checks if the domain name is valid
if socksaddr.IsFqdn() {
return net.Destination{
Network: network,
Address: net.DomainAddress(socksaddr.Fqdn),
Port: net.Port(socksaddr.Port),
}
}, nil
}
// IsIP() implicitly checks if the IP address is valid
@@ -33,10 +34,10 @@ func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination {
Network: network,
Address: net.IPAddress(socksaddr.Addr.AsSlice()),
Port: net.Port(socksaddr.Port),
}
}, nil
}
return net.Destination{}
return net.Destination{}, errors.New("invalid socks address: ", socksaddr)
}
func ToSocksaddr(destination net.Destination) M.Socksaddr {
+10 -2
View File
@@ -26,7 +26,11 @@ func NewDialer(dialer internet.Dialer) *XrayDialer {
}
func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.Dialer.Dial(ctx, ToDestination(destination, ToNetwork(network)))
dest, err := ToDestination(destination, ToNetwork(network))
if err != nil {
return nil, err
}
return d.Dialer.Dial(ctx, dest)
}
func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
@@ -43,13 +47,17 @@ func NewOutboundDialer(outbound proxy.Outbound, dialer internet.Dialer) *XrayOut
}
func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
dest, err := ToDestination(destination, ToNetwork(network))
if err != nil {
return nil, err
}
outbounds := session.OutboundsFromContext(ctx)
if len(outbounds) == 0 {
outbounds = []*session.Outbound{{}}
ctx = session.ContextWithOutbounds(ctx, outbounds)
}
ob := outbounds[len(outbounds)-1]
ob.Target = ToDestination(destination, ToNetwork(network))
ob.Target = dest
opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)}
uplinkReader, uplinkWriter := pipe.New(opts...)
+10 -2
View File
@@ -31,15 +31,23 @@ func NewDispatcher(dispatcher routing.Dispatcher, newErrorFunc func(values ...an
}
func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
dest, err := ToDestination(metadata.Destination, net.Network_TCP)
if err != nil {
return err
}
xConn := NewConn(conn)
return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_TCP), &transport.Link{
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
Reader: xConn,
Writer: xConn,
})
}
func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_UDP), &transport.Link{
dest, err := ToDestination(metadata.Destination, net.Network_UDP)
if err != nil {
return err
}
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
Reader: buf.NewPacketReader(conn.(io.Reader)),
Writer: buf.NewWriter(conn.(io.Writer)),
})
+31 -30
View File
@@ -10,15 +10,21 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/transport"
)
func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error {
cancel := func() {
common.Interrupt(link.Reader)
common.Interrupt(serverConn)
}
conn := &PacketConnWrapper{
Reader: link.Reader,
Writer: link.Writer,
Dest: destination,
Conn: inboundConn,
T: signal.CancelAfterInactivity(ctx, cancel, 300*time.Second),
}
return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn)))
}
@@ -29,11 +35,19 @@ type PacketConnWrapper struct {
net.Conn
Dest net.Destination
cached buf.MultiBuffer
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
T *signal.ActivityTimer
}
// This ReadPacket implemented a timeout to avoid goroutine leak like PipeConnWrapper.Read()
// as a temporarily solution
func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) {
func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) {
w.T.Update()
defer func() {
if err != nil {
// uplinkonly
w.T.SetTimeout(2 * time.Second)
}
}()
if w.cached != nil {
mb, bb := buf.SplitFirst(w.cached)
if bb == nil {
@@ -51,30 +65,7 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) {
return ToSocksaddr(destination), nil
}
}
// timeout
type readResult struct {
mb buf.MultiBuffer
err error
}
c := make(chan readResult, 1)
go func() {
mb, err := w.ReadMultiBuffer()
c <- readResult{mb: mb, err: err}
}()
var mb buf.MultiBuffer
select {
case <-time.After(60 * time.Second):
common.Close(w.Reader)
common.Interrupt(w.Reader)
return M.Socksaddr{}, buf.ErrReadTimeout
case result := <-c:
if result.err != nil {
return M.Socksaddr{}, result.err
}
mb = result.mb
}
mb, err := w.ReadMultiBuffer()
nb, bb := buf.SplitFirst(mb)
if bb == nil {
return M.Socksaddr{}, nil
@@ -92,12 +83,22 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) {
}
}
func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) error {
func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) (err error) {
w.T.Update()
defer func() {
if err != nil {
// downlinkonly
w.T.SetTimeout(5 * time.Second)
}
}()
endpoint, err := ToDestination(destination, net.Network_UDP)
if err != nil {
return err
}
vBuf := buf.New()
vBuf.Write(buffer.Bytes())
endpoint := ToDestination(destination, net.Network_UDP)
vBuf.UDP = &endpoint
return w.Writer.WriteMultiBuffer(buf.MultiBuffer{vBuf})
return w.WriteMultiBuffer(buf.MultiBuffer{vBuf})
}
func (w *PacketConnWrapper) Close() error {
+18 -18
View File
@@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing/common/bufio"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/transport"
)
@@ -22,6 +23,11 @@ func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, s
} else {
conn.R = &buf.BufferedReader{Reader: link.Reader}
}
cancel := func() {
common.Interrupt(link.Reader)
common.Interrupt(serverConn)
}
conn.T = signal.CancelAfterInactivity(ctx, cancel, 300*time.Second)
return ReturnError(bufio.CopyConn(ctx, conn, serverConn))
}
@@ -29,35 +35,27 @@ type PipeConnWrapper struct {
R io.Reader
W buf.Writer
net.Conn
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
T *signal.ActivityTimer
}
func (w *PipeConnWrapper) Close() error {
return nil
}
// This Read implemented a timeout to avoid goroutine leak.
// as a temporarily solution
func (w *PipeConnWrapper) Read(b []byte) (n int, err error) {
type readResult struct {
n int
err error
}
c := make(chan readResult, 1)
go func() {
n, err := w.R.Read(b)
c <- readResult{n: n, err: err}
}()
select {
case result := <-c:
return result.n, result.err
case <-time.After(300 * time.Second):
common.Close(w.R)
common.Interrupt(w.R)
return 0, buf.ErrReadTimeout
w.T.Update()
n, err = w.R.Read(b)
if err != nil {
// uplinkonly
w.T.SetTimeout(2 * time.Second)
}
return
}
func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
w.T.Update()
n = len(p)
var mb buf.MultiBuffer
pLen := len(p)
@@ -76,6 +74,8 @@ func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
if err != nil {
n = 0
buf.ReleaseMulti(mb)
// downlinkonly
w.T.SetTimeout(5 * time.Second)
}
return
}
+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)
}
}
+14 -7
View File
@@ -10,37 +10,44 @@ func TestByteSizes(t *testing.T) {
size := units.ByteSize(0)
assertSizeString(t, size, "0")
size++
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00B"),
size,
)
size <<= 10
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00KB"),
size,
)
size <<= 10
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00MB"),
size,
)
size <<= 10
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00GB"),
size,
)
size <<= 10
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00TB"),
size,
)
size <<= 10
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00PB"),
size,
)
size <<= 10
assertSizeValue(t,
assertSizeValue(
t,
assertSizeString(t, size, "1.00EB"),
size,
)
+77 -43
View File
@@ -4,10 +4,10 @@ import (
"hash/fnv"
"math"
"math/rand"
"strconv"
"time"
"net/http"
"strconv"
"strings"
"time"
"github.com/klauspost/cpuid/v2"
)
@@ -18,6 +18,7 @@ func GetRandomizer() *rand.Rand {
fnvHash.Write([]byte(strconv.Itoa(cpuid.CPU.Family) + strconv.Itoa(cpuid.CPU.Model) + strconv.Itoa(cpuid.CPU.PhysicalCores) + strconv.Itoa(cpuid.CPU.LogicalCores) + strconv.Itoa(cpuid.CPU.CacheLine) + strconv.Itoa(cpuid.CPU.ThreadsPerCore)))
return rand.New(rand.NewSource(int64(fnvHash.Sum64())))
}
var globalRng *rand.Rand = GetRandomizer()
// The Chrome version generator will suffer from deviation of a normal distribution.
@@ -26,83 +27,114 @@ func ChromeVersion() int {
var startVersion int = 144
var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeCurrent int64 = time.Now().Unix() / 86400
var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 105))
var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*105))
return startVersion + (timeDiff / 35) // It's 31.15 currently.
}
var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1,
var safariMinorMap [25]int = [25]int{
0, 0, 0, 1, 1,
1, 2, 2, 2, 2, 3, 3, 3, 4, 4,
4, 5, 5, 5, 5, 5, 6, 6, 6, 6}
4, 5, 5, 5, 5, 5, 6, 6, 6, 6,
}
// The following version generators use deterministic generators, but with the distribution scaled by a curve.
func CurlVersion() string {
// curl 8.0.0 was released on 20/03/2023.
var timeCurrent int64 = time.Now().Unix() / 86400
var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 165))
var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*165))
var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days.
return "8." + strconv.Itoa(minorValue) + ".0"
}
func FirefoxVersion() int {
// Firefox 128 ESR was released on 09/07/2023.
var timeCurrent int64 = time.Now().Unix() / 86400
var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2) * 50))
return int(timeDiff / 30) + 128
timeDiff := timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2)*50))
return int(timeDiff/30) + 128
}
func SafariVersion() string {
var anchoredTime time.Time = time.Now()
var releaseYear int = anchoredTime.Year()
var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
var delayedDays = int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75))
delayedDays := int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75))
splitPoint = splitPoint.AddDate(0, 0, delayedDays)
if (anchoredTime.Compare(splitPoint) < 0) {
releaseYear --
if anchoredTime.Compare(splitPoint) < 0 {
releaseYear--
splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
splitPoint = splitPoint.AddDate(0, 0, delayedDays)
}
var minorVersion = safariMinorMap[(anchoredTime.Unix() - splitPoint.Unix()) / 1296000]
return strconv.Itoa(releaseYear - 1999) + "." + strconv.Itoa(minorVersion)
minorVersion := safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000]
return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion)
}
// The full Chromium brand GREASE implementation
var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"}
var clientHintVersionNA = []string{"8", "99", "24"}
var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
var clientHintShuffle4 = [][4]int{
{0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1},
{1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0},
{2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0},
{3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}}
var (
clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"}
clientHintVersionNA = []string{"8", "99", "24"}
clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
clientHintShuffle4 = [][4]int{
{0, 1, 2, 3},
{0, 1, 3, 2},
{0, 2, 1, 3},
{0, 2, 3, 1},
{0, 3, 1, 2},
{0, 3, 2, 1},
{1, 0, 2, 3},
{1, 0, 3, 2},
{1, 2, 0, 3},
{1, 2, 3, 0},
{1, 3, 0, 2},
{1, 3, 2, 0},
{2, 0, 1, 3},
{2, 0, 3, 1},
{2, 1, 0, 3},
{2, 1, 3, 0},
{2, 3, 0, 1},
{2, 3, 1, 0},
{3, 0, 1, 2},
{3, 0, 2, 1},
{3, 1, 0, 2},
{3, 1, 2, 0},
{3, 2, 0, 1},
{3, 2, 1, 0},
}
)
func getGreasedChInvalidBrand(seed int) string {
return "\"Not" + clientHintGreaseNA[seed % len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed + 1) % len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed % len(clientHintVersionNA)] + "\"";
return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\""
}
func getGreasedChOrder(brandLength int, seed int) []int {
switch brandLength {
case 1:
return []int{0}
case 2:
return []int{seed % brandLength, (seed + 1) % brandLength}
case 3:
return clientHintShuffle3[seed % len(clientHintShuffle3)][:]
default:
return clientHintShuffle4[seed % len(clientHintShuffle4)][:]
case 1:
return []int{0}
case 2:
return []int{seed % brandLength, (seed + 1) % brandLength}
case 3:
return clientHintShuffle3[seed%len(clientHintShuffle3)][:]
default:
return clientHintShuffle4[seed%len(clientHintShuffle4)][:]
}
//return []int{}
}
func getUngreasedChUa(majorVersion int, forkName string) []string {
// Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice
baseChUa := make([]string, 0, 4)
baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion),
"\"Chromium\";v=\"" + strconv.Itoa(majorVersion) + "\"")
"\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"")
switch forkName {
case "chrome":
baseChUa = append(baseChUa, "\"Google Chrome\";v=\"" + strconv.Itoa(majorVersion) + "\"")
baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"")
case "edge":
baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\"" + strconv.Itoa(majorVersion) + "\"")
baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"")
}
return baseChUa
}
func getGreasedChUa(majorVersion int, forkName string) string {
ungreasedCh := getUngreasedChUa(majorVersion, forkName)
shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion)
@@ -114,16 +146,18 @@ func getGreasedChUa(majorVersion int, forkName string) string {
}
// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG.
var CurlUA = "curl/" + CurlVersion()
var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion())
var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0"
var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15"
// Chromium browsers.
var AnchoredChromeVersion = ChromeVersion()
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36"
var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome")
var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0"
var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge")
var (
CurlUA = "curl/" + CurlVersion()
AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion())
FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0"
SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15"
// Chromium browsers.
AnchoredChromeVersion = ChromeVersion()
ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36"
ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome")
MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0"
MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge")
)
func applyMasqueradedHeaders(header http.Header, browser string, variant string) {
// Browser-specific.
+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
}
+1 -1
View File
@@ -109,4 +109,4 @@ func (m *TypedSyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) {
previous = anyPrevious.(V)
}
return previous, loaded
}
}
+54
View File
@@ -0,0 +1,54 @@
package utils
import (
"path/filepath"
"runtime"
"strings"
"syscall"
)
// ResolveSocketPath applies platform-specific transformations to a Unix
// socket path, matching the listen-side behaviour in
// transport/internet/system_listener.go.
//
// For abstract sockets (prefix @) on Linux/Android:
// - single @ — used as-is (lock-free abstract socket)
// - double @@ — stripped to single @ and padded to
// syscall.RawSockaddrUnix{}.Path length (HAProxy compat)
//
// Filesystem paths and abstract sockets on other platforms are returned
// unchanged.
func ResolveSocketPath(path string) string {
if len(path) == 0 || path[0] != '@' {
return path
}
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
return path
}
if len(path) > 1 && path[1] == '@' {
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path))
copy(fullAddr, path[1:])
return string(fullAddr)
}
return path
}
// SplitHTTPUnixURL splits a target into an HTTP URL and an optional Unix
// socket path. For regular http(s) URLs the input is returned unchanged
// with an empty socketPath. For Unix sockets the format is:
//
// /path/to/socket.sock[:/http/path]
// @abstract[:/http/path]
// @@padded[:/http/path]
//
// The :/ separator delimits the socket path from the HTTP request path.
// If omitted, "/" is used.
func SplitHTTPUnixURL(raw string) (httpURL, socketPath string) {
if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') {
return raw, ""
}
if idx := strings.Index(raw, ":/"); idx >= 0 {
return "http://localhost" + raw[idx+1:], raw[:idx]
}
return "http://localhost/", raw
}
+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{}{})
}
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 4
Version_z byte = 17
Version_y byte = 6
Version_z byte = 1
)
var (
+3 -3
View File
@@ -53,7 +53,7 @@ func TestXrayDial(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -105,7 +105,7 @@ func TestXrayDialUDPConn(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -174,7 +174,7 @@ func TestXrayDialUDP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
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),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(0),
Networks: []net.Network{net.Network_TCP},
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
RewritePort: uint32(0),
AllowedNetworks: []net.Network{net.Network_TCP},
}),
},
},
@@ -66,7 +66,7 @@ func TestXrayClose(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(0),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
}),
+7 -3
View File
@@ -80,13 +80,17 @@ func New() *Client {
d := &net.Dialer{
Timeout: time.Second * 16,
Control: func(network, address string, c syscall.RawConn) error {
var errs []error
for _, ctl := range internet.Controllers {
if err := ctl(network, address, c); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
return err
errs = append(errs, err)
}
}
return nil
err := errors.Combine(errs...)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
}
return err
},
}
-1
View File
@@ -34,7 +34,6 @@ func (ctx *Context) GetSourceIPs() []net.IP {
}
return nil
}
// GetSourcePort implements routing.Context.
+16 -10
View File
@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
go 1.26
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/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
@@ -12,23 +12,25 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.2
github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.50.0
golang.org/x/crypto v0.51.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.53.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.43.0
golang.org/x/sys v0.45.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v0.6.1
google.golang.org/grpc v1.80.0
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
@@ -42,14 +44,18 @@ require (
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+42 -30
View File
@@ -1,7 +1,7 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY=
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 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
@@ -45,6 +45,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -53,6 +61,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
@@ -65,39 +75,41 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
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/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
@@ -109,21 +121,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -131,14 +143,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/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/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+2 -1
View File
@@ -48,4 +48,5 @@ var configLoader = NewJSONConfigLoader(
"http": func() interface{} { return new(HTTPResponse) },
},
"type",
"")
"",
)
+2 -1
View File
@@ -3,6 +3,7 @@ package conf
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
@@ -199,7 +200,7 @@ func (v *PortRange) UnmarshalJSON(data []byte) error {
if err == nil {
v.From = uint32(from)
v.To = uint32(to)
if v.From > v.To {
if v.From > v.To || v.To > math.MaxUint16 {
return errors.New("invalid port range ", v.From, " -> ", v.To)
}
return nil
+148 -17
View File
@@ -1,38 +1,169 @@
package conf
import (
"strings"
"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/proxy/dns"
"google.golang.org/protobuf/proto"
)
type DNSOutboundRuleConfig struct {
Action string `json:"action"`
QType *PortList `json:"qType"`
Domain *StringList `json:"domain"`
RCode uint32 `json:"rCode"`
}
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 "return":
rule.Action = dns.RuleAction_Return
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 {
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
}
if c.RCode > 65535 {
return nil, errors.New("rCode out of range: ", c.RCode)
}
rule.RCode = c.RCode
return rule, nil
}
type DNSOutboundConfig struct {
Network Network `json:"network"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
UserLevel uint32 `json:"userLevel"`
NonIPQuery string `json:"nonIPQuery"`
BlockTypes []int32 `json:"blockTypes"`
RewriteNetwork Network `json:"rewriteNetwork"`
RewriteAddress *Address `json:"rewriteAddress"`
RewritePort uint16 `json:"rewritePort"`
Network Network `json:"network"`
Address *Address `json:"address"`
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) {
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{
Server: &net.Endpoint{
Network: c.Network.Build(),
Port: uint32(c.Port),
RewriteServer: &net.Endpoint{
Network: c.RewriteNetwork.Build(),
Port: uint32(c.RewritePort),
},
UserLevel: c.UserLevel,
}
if c.Address != nil {
config.Server.Address = c.Address.Build()
if c.RewriteAddress != nil {
config.RewriteServer.Address = c.RewriteAddress.Build()
}
switch c.NonIPQuery {
case "", "reject", "drop", "skip":
default:
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
// todo: remove legacy
if c.NonIPQuery != nil || c.BlockTypes != nil {
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
}
// 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_Return
rule.RCode = 5
}
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_Return}
if mode == "reject" {
rule.Action = dns.RuleAction_Return
rule.RCode = 5
} 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
}
+211 -1
View File
@@ -1,8 +1,10 @@
package conf_test
import (
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/dns"
@@ -22,12 +24,220 @@ func TestDnsProxyConfig(t *testing.T) {
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{
RewriteServer: &net.Endpoint{
Network: net.Network_TCP,
Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})),
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": "return",
"rCode": 5,
"domain": "keyword:example"
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Return,
RCode: 5,
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_Return,
RCode: 5,
},
},
},
},
{
Input: `{
"blockTypes": [1, 65]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
RewriteServer: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Return,
QType: []int32{1, 65},
RCode: 5,
},
{
Action: dns.RuleAction_Hijack,
QType: []int32{1, 28},
},
{
Action: dns.RuleAction_Return,
RCode: 5,
},
},
},
},
{
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 {
AllowedNetwork *NetworkList `json:"allowedNetwork"`
RewriteAddress *Address `json:"rewriteAddress"`
RewritePort uint16 `json:"rewritePort"`
Network *NetworkList `json:"network"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
PortMap map[string]string `json:"portMap"`
Network *NetworkList `json:"network"`
FollowRedirect bool `json:"followRedirect"`
UserLevel uint32 `json:"userLevel"`
}
func (v *DokodemoConfig) Build() (proto.Message, error) {
config := new(dokodemo.Config)
if v.Address != nil {
config.Address = v.Address.Build()
if v.Network != nil {
v.AllowedNetwork = v.Network
}
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
for _, v := range config.PortMap {
if _, _, err := net.SplitHostPort(v); err != nil {
return nil, errors.New("invalid portMap: ", v).Base(err)
}
}
config.Networks = v.Network.Build()
config.FollowRedirect = v.FollowRedirect
config.UserLevel = v.UserLevel
return config, nil

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