Compare commits

...
55 Commits
Author SHA1 Message Date
Fangliding 2f16233259 upd reality 2026-05-01 17:50:53 +08: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
RPRXandGitHub b4650360d6 v26.4.17
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-04-17 23:04:05 +00:00
d52f15060b Direct/Freedom outbound: Block UDP responses that are come from ipsBlocked as well (#5952)
https://github.com/XTLS/Xray-core/pull/5947#issuecomment-4258980670

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

---------

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 21:33:38 +00:00
RPRXandGitHub c5edc122b7 v26.4.15
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-04-15 23:56:34 +00:00
RPRXandGitHub 9dd17d55fb XUDP GetGlobalID(): Remove inbound.Name == "wireguard" for now
https://github.com/XTLS/Xray-core/pull/5947#issuecomment-4256423483
2026-04-15 23:47:21 +00:00
310b764811 Direct/Freedom outbound: Add ipsBlocked (supports IP, CIDR, "geoip:", "ext:") and apply a default safe policy (#5947)
https://github.com/XTLS/Xray-core/pull/5892#issuecomment-4254056911

---------

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

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

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

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

---------

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

---
updated-dependencies:
- dependency-name: golang.zx2c4.com/wireguard/windows
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 11:54:49 +00:00
RPRXandGitHub 14e8ecfacf v26.4.13
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-13 22:49:40 +00:00
LjhAUMEMandGitHub 7094f8dc07 mKCP transport: Add cwndMultiplier; Apply unaggressive strategy by default (#5890)
https://github.com/XTLS/Xray-core/issues/4846#issuecomment-4150329444

https://github.com/XTLS/Xray-core/pull/5872#issuecomment-4184774915

https://github.com/XTLS/Xray-core/pull/5890#issuecomment-4240052251
2026-04-13 22:41:38 +00:00
MeowandGitHub d342361c89 Sniffing: Add ipsExcluded (supports IP, CIDR, "geoip:", "ext:") (#5929)
https://github.com/XTLS/Xray-core/pull/5927#issuecomment-4238197075

https://github.com/XTLS/Xray-core/pull/5929#issuecomment-4238550443
2026-04-13 18:08:51 +00:00
MeowandGitHub f17fabfff5 Sniffing: domainsExcluded supports "geosite:" (#5927)
https://github.com/XTLS/Xray-core/pull/5927#issuecomment-4238238050

https://github.com/XTLS/Xray-core/pull/5927#issuecomment-4238119874
2026-04-13 17:39:53 +00:00
MeowandGitHub 05a11910d4 DomainMatcher: Reduce runtime memory usage and startup peak memory on iOS (#5924)
https://github.com/XTLS/Xray-core/pull/5814#issuecomment-4231071433

Closes https://github.com/XTLS/Xray-core/issues/4422
2026-04-13 16:54:43 +00:00
MeowandGitHub 82624bcaf0 Xray-core: Refactor geodata (#5814)
https://github.com/XTLS/Xray-core/issues/4422#issuecomment-3533007890

Breaking changes https://github.com/XTLS/Xray-core/pull/5569

Reverts https://github.com/XTLS/Xray-core/pull/5505

Closes https://github.com/XTLS/Xray-core/pull/643
2026-04-13 16:42:29 +00:00
风扇滑翔翼andGitHub e9f7d61c2e Hysteria transport: Fix client-side clientManager (#5928)
Fixes https://github.com/XTLS/Xray-core/issues/5911
2026-04-13 16:14:15 +00:00
LjhAUMEMandGitHub 806b8dc27d TUN inbound: Add gateway, dns, autoSystemRoutingTable, autoOutboundsInterface for Windows (#5887)
And refactor `mtu` to support setting IPv4/v6 separately

Example: https://github.com/XTLS/Xray-core/pull/5887#issue-4198837696
2026-04-13 13:38:10 +00:00
f27edc3172 Routing: process supports UID on Android (#5915)
Example: https://github.com/XTLS/Xray-core/pull/5915#issuecomment-4232122895

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-04-13 13:17:53 +00:00
Lumière ÉlevéandGitHub c93478b891 Global HTTP headers' masquerading: Add "curl"; Improve version generators (#5916)
https://github.com/XTLS/Xray-core/pull/5802
https://github.com/XTLS/Xray-core/pull/5689
https://github.com/XTLS/Xray-core/pull/5658
2026-04-11 21:16:58 +00:00
1642fdfbdd XDNS finalmask: Support resolvers (client) and domains (server) instead of domain (#5872)
https://github.com/XTLS/Xray-core/pull/5872#issuecomment-4192730898

Example: https://github.com/XTLS/Xray-core/pull/5872#issuecomment-4196172391

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-04-11 19:37:32 +00:00
Yury KastovandGitHub a91a88c7b2 API & Commands: Add GetUsersStatsRequest(); Improve api statsonlineiplist (#5776)
https://github.com/XTLS/Xray-core/pull/5776#issuecomment-4230007504
2026-04-11 19:09:24 +00:00
32937846c5 Tunnel inbound: Compatible with listening UNIX domain socket (#5693)
https://github.com/XTLS/Xray-core/pull/5693#issuecomment-4229947428

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-04-11 18:39:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
93225a1132 Bump golang.org/x/net from 0.52.0 to 0.53.0 (#5899)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.52.0 to 0.53.0.
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 17:24:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4d958cbfd3 Bump actions/github-script from 8 to 9 (#5898)
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  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-11 17:24:14 +00:00
e5a9fb752e QUIC sniffer: Fix potential panic on malformed QUIC packets (#5866)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-hrp5-2rwj-wvmv

---------

Co-authored-by: kastov <yk@sent.com>
2026-04-07 10:10:12 +00:00
RynnyaandFangliding 6a1a13b797 TUN inbound: Closable by AlwaysOnInboundHandler (#5860)
https://github.com/XTLS/Xray-core/pull/5860#issuecomment-4193477738
2026-04-07 16:32:06 +08:00
Alexey CherednichenkoandFangliding 6c4008edad Observatory: Clear removed outbounds (#5876)
* fix: prune stale observatory status

* More readable

Refactor observer to clear removed outbounds instead of updating status. Introduced slices package for improved outbound checking.

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-04-07 16:32:06 +08:00
LjhAUMEMandGitHub 3f608b3a58 Finalmask quicParams: Add bbrProfile ("conservative" / "standard" (default) / "aggressive") (#5869)
And Update github.com/apernet/quic-go to 20260330051153
2026-04-05 13:45:50 +00:00
LjhAUMEMandGitHub 4c3020ca6f TUN inbound: Fix UDP FullCone NAT (#5888)
Fixes https://github.com/XTLS/Xray-core/issues/5845
2026-04-05 12:59:22 +00:00
LjhAUMEMandGitHub ba88aa173c WireGuard outbound: Fix UDP FullCone NAT on Linux (#5858)
Fixes https://github.com/XTLS/Xray-core/issues/5848
2026-04-05 12:57:08 +00:00
225 changed files with 15570 additions and 6501 deletions
+2 -2
View File
@@ -45,8 +45,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+2 -2
View File
@@ -54,8 +54,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
- name: Trigger Asset Update Workflow if Assets Missing - name: Trigger Asset Update Workflow if Assets Missing
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
uses: actions/github-script@v8 uses: actions/github-script@v9
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
+15 -29
View File
@@ -2,7 +2,6 @@ package dispatcher
import ( import (
"context" "context"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -181,12 +180,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
} }
if p.Stats.UserOnline { if p.Stats.UserOnline {
name := "user>>>" + user.Email + ">>>online" trackOnlineIP(ctx, d.stats, user.Email, sessionInbound.Source.Address.String())
if om, _ := stats.GetOrRegisterOnlineMap(d.stats, name); om != nil {
userIP := sessionInbound.Source.Address.String()
om.AddIP(userIP)
context.AfterFunc(ctx, func() { om.RemoveIP(userIP) })
}
} }
} }
@@ -220,39 +214,31 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
} }
} }
if p.Stats.UserOnline { if p.Stats.UserOnline {
name := "user>>>" + user.Email + ">>>online" trackOnlineIP(ctx, statsManager, user.Email, sessionInbound.Source.Address.String())
if om, _ := stats.GetOrRegisterOnlineMap(statsManager, name); om != nil {
userIP := sessionInbound.Source.Address.String()
om.AddIP(userIP)
context.AfterFunc(ctx, func() { om.RemoveIP(userIP) })
}
} }
} }
return link return link
} }
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
name := "user>>>" + email + ">>>online"
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
om.AddIP(ip)
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
}
}
func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool { func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool {
domain := result.Domain() domain := result.Domain()
if domain == "" { if domain == "" {
return false return false
} }
for _, d := range request.ExcludeForDomain { if request.ExcludeForDomain != nil && request.ExcludeForDomain.MatchAny(strings.ToLower(domain)) {
if strings.HasPrefix(d, "regexp:") { return false
pattern := d[7:] }
re, err := regexp.Compile(pattern) if request.ExcludeForIP != nil && destination.Address.Family().IsIP() && request.ExcludeForIP.Match(destination.Address.IP()) {
if err != nil { return false
errors.LogInfo(ctx, "Unable to compile regex")
continue
}
if re.MatchString(domain) {
return false
}
} else {
if strings.ToLower(domain) == d {
return false
}
}
} }
protocolString := result.Protocol() protocolString := result.Protocol()
if resComp, ok := result.(SnifferResultComposite); ok { if resComp, ok := result.(SnifferResultComposite); ok {
+11 -35
View File
@@ -2,48 +2,24 @@ package dns
import ( import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/common/uuid"
) )
var typeMap = map[DomainMatchingType]strmatcher.Type{
DomainMatchingType_Full: strmatcher.Full,
DomainMatchingType_Subdomain: strmatcher.Domain,
DomainMatchingType_Keyword: strmatcher.Substr,
DomainMatchingType_Regex: strmatcher.Regex,
}
// References: // References:
// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml // https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan // https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
var localTLDsAndDotlessDomains = []*NameServer_PriorityDomain{ var localTLDsAndDotlessDomainsRules = []*geodata.DomainRule{
{Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^[^.]+$"}}}, // This will only match domains without any dot
{Type: DomainMatchingType_Subdomain, Domain: "local"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "local"}}},
{Type: DomainMatchingType_Subdomain, Domain: "localdomain"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "localdomain"}}},
{Type: DomainMatchingType_Subdomain, Domain: "localhost"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "localhost"}}},
{Type: DomainMatchingType_Subdomain, Domain: "lan"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "lan"}}},
{Type: DomainMatchingType_Subdomain, Domain: "home.arpa"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "home.arpa"}}},
{Type: DomainMatchingType_Subdomain, Domain: "example"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "example"}}},
{Type: DomainMatchingType_Subdomain, Domain: "invalid"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "invalid"}}},
{Type: DomainMatchingType_Subdomain, Domain: "test"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "test"}}},
}
var localTLDsAndDotlessDomainsRule = &NameServer_OriginalRule{
Rule: "geosite:private",
Size: uint32(len(localTLDsAndDotlessDomains)),
}
func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
strMType, f := typeMap[t]
if !f {
return nil, errors.New("unknown mapping type", t).AtWarning()
}
matcher, err := strMType.New(domain)
if err != nil {
return nil, errors.New("failed to create str matcher").Base(err)
}
return matcher, nil
} }
func toNetIP(addrs []net.Address) ([]net.IP, error) { func toNetIP(addrs []net.Address) ([]net.IP, error) {
+72 -260
View File
@@ -7,7 +7,7 @@
package dns package dns
import ( import (
router "github.com/xtls/xray-core/app/router" geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net" net "github.com/xtls/xray-core/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -23,58 +23,6 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type DomainMatchingType int32
const (
DomainMatchingType_Full DomainMatchingType = 0
DomainMatchingType_Subdomain DomainMatchingType = 1
DomainMatchingType_Keyword DomainMatchingType = 2
DomainMatchingType_Regex DomainMatchingType = 3
)
// Enum value maps for DomainMatchingType.
var (
DomainMatchingType_name = map[int32]string{
0: "Full",
1: "Subdomain",
2: "Keyword",
3: "Regex",
}
DomainMatchingType_value = map[string]int32{
"Full": 0,
"Subdomain": 1,
"Keyword": 2,
"Regex": 3,
}
)
func (x DomainMatchingType) Enum() *DomainMatchingType {
p := new(DomainMatchingType)
*p = x
return p
}
func (x DomainMatchingType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DomainMatchingType) Descriptor() protoreflect.EnumDescriptor {
return file_app_dns_config_proto_enumTypes[0].Descriptor()
}
func (DomainMatchingType) Type() protoreflect.EnumType {
return &file_app_dns_config_proto_enumTypes[0]
}
func (x DomainMatchingType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DomainMatchingType.Descriptor instead.
func (DomainMatchingType) EnumDescriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0}
}
type QueryStrategy int32 type QueryStrategy int32
const ( const (
@@ -111,11 +59,11 @@ func (x QueryStrategy) String() string {
} }
func (QueryStrategy) Descriptor() protoreflect.EnumDescriptor { func (QueryStrategy) Descriptor() protoreflect.EnumDescriptor {
return file_app_dns_config_proto_enumTypes[1].Descriptor() return file_app_dns_config_proto_enumTypes[0].Descriptor()
} }
func (QueryStrategy) Type() protoreflect.EnumType { func (QueryStrategy) Type() protoreflect.EnumType {
return &file_app_dns_config_proto_enumTypes[1] return &file_app_dns_config_proto_enumTypes[0]
} }
func (x QueryStrategy) Number() protoreflect.EnumNumber { func (x QueryStrategy) Number() protoreflect.EnumNumber {
@@ -124,30 +72,29 @@ func (x QueryStrategy) Number() protoreflect.EnumNumber {
// Deprecated: Use QueryStrategy.Descriptor instead. // Deprecated: Use QueryStrategy.Descriptor instead.
func (QueryStrategy) EnumDescriptor() ([]byte, []int) { func (QueryStrategy) EnumDescriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{1} return file_app_dns_config_proto_rawDescGZIP(), []int{0}
} }
type NameServer struct { type NameServer struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
ClientIp []byte `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"` ClientIp []byte `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"`
SkipFallback bool `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,omitempty"` SkipFallback bool `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,omitempty"`
PrioritizedDomain []*NameServer_PriorityDomain `protobuf:"bytes,2,rep,name=prioritized_domain,json=prioritizedDomain,proto3" json:"prioritized_domain,omitempty"` Domain []*geodata.DomainRule `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
ExpectedGeoip []*router.GeoIP `protobuf:"bytes,3,rep,name=expected_geoip,json=expectedGeoip,proto3" json:"expected_geoip,omitempty"` ExpectedIp []*geodata.IPRule `protobuf:"bytes,3,rep,name=expected_ip,json=expectedIp,proto3" json:"expected_ip,omitempty"`
OriginalRules []*NameServer_OriginalRule `protobuf:"bytes,4,rep,name=original_rules,json=originalRules,proto3" json:"original_rules,omitempty"` QueryStrategy QueryStrategy `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
QueryStrategy QueryStrategy `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"` ActPrior bool `protobuf:"varint,8,opt,name=actPrior,proto3" json:"actPrior,omitempty"`
ActPrior bool `protobuf:"varint,8,opt,name=actPrior,proto3" json:"actPrior,omitempty"` Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"`
Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"` TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"`
TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"` DisableCache *bool `protobuf:"varint,11,opt,name=disableCache,proto3,oneof" json:"disableCache,omitempty"`
DisableCache *bool `protobuf:"varint,11,opt,name=disableCache,proto3,oneof" json:"disableCache,omitempty"` ServeStale *bool `protobuf:"varint,15,opt,name=serveStale,proto3,oneof" json:"serveStale,omitempty"`
ServeStale *bool `protobuf:"varint,15,opt,name=serveStale,proto3,oneof" json:"serveStale,omitempty"` ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"`
ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"` FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"`
FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"` UnexpectedIp []*geodata.IPRule `protobuf:"bytes,13,rep,name=unexpected_ip,json=unexpectedIp,proto3" json:"unexpected_ip,omitempty"`
UnexpectedGeoip []*router.GeoIP `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"` ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"`
ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"` PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"`
PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"` unknownFields protoimpl.UnknownFields
unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache
sizeCache protoimpl.SizeCache
} }
func (x *NameServer) Reset() { func (x *NameServer) Reset() {
@@ -201,23 +148,16 @@ func (x *NameServer) GetSkipFallback() bool {
return false return false
} }
func (x *NameServer) GetPrioritizedDomain() []*NameServer_PriorityDomain { func (x *NameServer) GetDomain() []*geodata.DomainRule {
if x != nil { if x != nil {
return x.PrioritizedDomain return x.Domain
} }
return nil return nil
} }
func (x *NameServer) GetExpectedGeoip() []*router.GeoIP { func (x *NameServer) GetExpectedIp() []*geodata.IPRule {
if x != nil { if x != nil {
return x.ExpectedGeoip return x.ExpectedIp
}
return nil
}
func (x *NameServer) GetOriginalRules() []*NameServer_OriginalRule {
if x != nil {
return x.OriginalRules
} }
return nil return nil
} }
@@ -278,9 +218,9 @@ func (x *NameServer) GetFinalQuery() bool {
return false return false
} }
func (x *NameServer) GetUnexpectedGeoip() []*router.GeoIP { func (x *NameServer) GetUnexpectedIp() []*geodata.IPRule {
if x != nil { if x != nil {
return x.UnexpectedGeoip return x.UnexpectedIp
} }
return nil return nil
} }
@@ -429,114 +369,9 @@ func (x *Config) GetEnableParallelQuery() bool {
return false return false
} }
type NameServer_PriorityDomain struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NameServer_PriorityDomain) Reset() {
*x = NameServer_PriorityDomain{}
mi := &file_app_dns_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NameServer_PriorityDomain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer_PriorityDomain) ProtoMessage() {}
func (x *NameServer_PriorityDomain) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[2]
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 NameServer_PriorityDomain.ProtoReflect.Descriptor instead.
func (*NameServer_PriorityDomain) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0, 0}
}
func (x *NameServer_PriorityDomain) GetType() DomainMatchingType {
if x != nil {
return x.Type
}
return DomainMatchingType_Full
}
func (x *NameServer_PriorityDomain) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
type NameServer_OriginalRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
Rule string `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"`
Size uint32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NameServer_OriginalRule) Reset() {
*x = NameServer_OriginalRule{}
mi := &file_app_dns_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NameServer_OriginalRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer_OriginalRule) ProtoMessage() {}
func (x *NameServer_OriginalRule) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NameServer_OriginalRule.ProtoReflect.Descriptor instead.
func (*NameServer_OriginalRule) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0, 1}
}
func (x *NameServer_OriginalRule) GetRule() string {
if x != nil {
return x.Rule
}
return ""
}
func (x *NameServer_OriginalRule) GetSize() uint32 {
if x != nil {
return x.Size
}
return 0
}
type Config_HostMapping struct { type Config_HostMapping struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"` Domain *geodata.DomainRule `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"` Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"`
// ProxiedDomain indicates the mapped domain has the same IP address on this // ProxiedDomain indicates the mapped domain has the same IP address on this
// domain. Xray will use this domain for IP queries. // domain. Xray will use this domain for IP queries.
@@ -547,7 +382,7 @@ type Config_HostMapping struct {
func (x *Config_HostMapping) Reset() { func (x *Config_HostMapping) Reset() {
*x = Config_HostMapping{} *x = Config_HostMapping{}
mi := &file_app_dns_config_proto_msgTypes[4] mi := &file_app_dns_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -559,7 +394,7 @@ func (x *Config_HostMapping) String() string {
func (*Config_HostMapping) ProtoMessage() {} func (*Config_HostMapping) ProtoMessage() {}
func (x *Config_HostMapping) ProtoReflect() protoreflect.Message { func (x *Config_HostMapping) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[4] mi := &file_app_dns_config_proto_msgTypes[2]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -575,18 +410,11 @@ func (*Config_HostMapping) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{1, 0} return file_app_dns_config_proto_rawDescGZIP(), []int{1, 0}
} }
func (x *Config_HostMapping) GetType() DomainMatchingType { func (x *Config_HostMapping) GetDomain() *geodata.DomainRule {
if x != nil {
return x.Type
}
return DomainMatchingType_Full
}
func (x *Config_HostMapping) GetDomain() string {
if x != nil { if x != nil {
return x.Domain return x.Domain
} }
return "" return nil
} }
func (x *Config_HostMapping) GetIp() [][]byte { func (x *Config_HostMapping) GetIp() [][]byte {
@@ -607,15 +435,15 @@ var File_app_dns_config_proto protoreflect.FileDescriptor
const file_app_dns_config_proto_rawDesc = "" + const file_app_dns_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x17app/router/config.proto\"\xdf\a\n" + "\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\xde\x05\n" +
"\n" + "\n" +
"NameServer\x123\n" + "NameServer\x123\n" +
"\aaddress\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\aaddress\x12\x1b\n" + "\aaddress\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\aaddress\x12\x1b\n" +
"\tclient_ip\x18\x05 \x01(\fR\bclientIp\x12\"\n" + "\tclient_ip\x18\x05 \x01(\fR\bclientIp\x12\"\n" +
"\fskipFallback\x18\x06 \x01(\bR\fskipFallback\x12V\n" + "\fskipFallback\x18\x06 \x01(\bR\fskipFallback\x127\n" +
"\x12prioritized_domain\x18\x02 \x03(\v2'.xray.app.dns.NameServer.PriorityDomainR\x11prioritizedDomain\x12=\n" + "\x06domain\x18\x02 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12<\n" +
"\x0eexpected_geoip\x18\x03 \x03(\v2\x16.xray.app.router.GeoIPR\rexpectedGeoip\x12L\n" + "\vexpected_ip\x18\x03 \x03(\v2\x1b.xray.common.geodata.IPRuleR\n" +
"\x0eoriginal_rules\x18\x04 \x03(\v2%.xray.app.dns.NameServer.OriginalRuleR\roriginalRules\x12B\n" + "expectedIp\x12B\n" +
"\x0equery_strategy\x18\a \x01(\x0e2\x1b.xray.app.dns.QueryStrategyR\rqueryStrategy\x12\x1a\n" + "\x0equery_strategy\x18\a \x01(\x0e2\x1b.xray.app.dns.QueryStrategyR\rqueryStrategy\x12\x1a\n" +
"\bactPrior\x18\b \x01(\bR\bactPrior\x12\x10\n" + "\bactPrior\x18\b \x01(\bR\bactPrior\x12\x10\n" +
"\x03tag\x18\t \x01(\tR\x03tag\x12\x1c\n" + "\x03tag\x18\t \x01(\tR\x03tag\x12\x1c\n" +
@@ -628,21 +456,15 @@ const file_app_dns_config_proto_rawDesc = "" +
"\x0fserveExpiredTTL\x18\x10 \x01(\rH\x02R\x0fserveExpiredTTL\x88\x01\x01\x12\x1e\n" + "\x0fserveExpiredTTL\x18\x10 \x01(\rH\x02R\x0fserveExpiredTTL\x88\x01\x01\x12\x1e\n" +
"\n" + "\n" +
"finalQuery\x18\f \x01(\bR\n" + "finalQuery\x18\f \x01(\bR\n" +
"finalQuery\x12A\n" + "finalQuery\x12@\n" +
"\x10unexpected_geoip\x18\r \x03(\v2\x16.xray.app.router.GeoIPR\x0funexpectedGeoip\x12\x1e\n" + "\runexpected_ip\x18\r \x03(\v2\x1b.xray.common.geodata.IPRuleR\funexpectedIp\x12\x1e\n" +
"\n" + "\n" +
"actUnprior\x18\x0e \x01(\bR\n" + "actUnprior\x18\x0e \x01(\bR\n" +
"actUnprior\x12\x1a\n" + "actUnprior\x12\x1a\n" +
"\bpolicyID\x18\x11 \x01(\rR\bpolicyID\x1a^\n" + "\bpolicyID\x18\x11 \x01(\rR\bpolicyIDB\x0f\n" +
"\x0ePriorityDomain\x124\n" +
"\x04type\x18\x01 \x01(\x0e2 .xray.app.dns.DomainMatchingTypeR\x04type\x12\x16\n" +
"\x06domain\x18\x02 \x01(\tR\x06domain\x1a6\n" +
"\fOriginalRule\x12\x12\n" +
"\x04rule\x18\x01 \x01(\tR\x04rule\x12\x12\n" +
"\x04size\x18\x02 \x01(\rR\x04sizeB\x0f\n" +
"\r_disableCacheB\r\n" + "\r_disableCacheB\r\n" +
"\v_serveStaleB\x12\n" + "\v_serveStaleB\x12\n" +
"\x10_serveExpiredTTL\"\x98\x05\n" + "\x10_serveExpiredTTLJ\x04\b\x04\x10\x05\"\x82\x05\n" +
"\x06Config\x129\n" + "\x06Config\x129\n" +
"\vname_server\x18\x05 \x03(\v2\x18.xray.app.dns.NameServerR\n" + "\vname_server\x18\x05 \x03(\v2\x18.xray.app.dns.NameServerR\n" +
"nameServer\x12\x1b\n" + "nameServer\x12\x1b\n" +
@@ -658,17 +480,11 @@ const file_app_dns_config_proto_rawDesc = "" +
"\x0fdisableFallback\x18\n" + "\x0fdisableFallback\x18\n" +
" \x01(\bR\x0fdisableFallback\x126\n" + " \x01(\bR\x0fdisableFallback\x126\n" +
"\x16disableFallbackIfMatch\x18\v \x01(\bR\x16disableFallbackIfMatch\x120\n" + "\x16disableFallbackIfMatch\x18\v \x01(\bR\x16disableFallbackIfMatch\x120\n" +
"\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x1a\x92\x01\n" + "\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x1a}\n" +
"\vHostMapping\x124\n" + "\vHostMapping\x127\n" +
"\x04type\x18\x01 \x01(\x0e2 .xray.app.dns.DomainMatchingTypeR\x04type\x12\x16\n" + "\x06domain\x18\x02 \x01(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12\x0e\n" +
"\x06domain\x18\x02 \x01(\tR\x06domain\x12\x0e\n" +
"\x02ip\x18\x03 \x03(\fR\x02ip\x12%\n" + "\x02ip\x18\x03 \x03(\fR\x02ip\x12%\n" +
"\x0eproxied_domain\x18\x04 \x01(\tR\rproxiedDomainJ\x04\b\a\x10\b*E\n" + "\x0eproxied_domain\x18\x04 \x01(\tR\rproxiedDomainJ\x04\b\a\x10\b*B\n" +
"\x12DomainMatchingType\x12\b\n" +
"\x04Full\x10\x00\x12\r\n" +
"\tSubdomain\x10\x01\x12\v\n" +
"\aKeyword\x10\x02\x12\t\n" +
"\x05Regex\x10\x03*B\n" +
"\rQueryStrategy\x12\n" + "\rQueryStrategy\x12\n" +
"\n" + "\n" +
"\x06USE_IP\x10\x00\x12\v\n" + "\x06USE_IP\x10\x00\x12\v\n" +
@@ -689,36 +505,32 @@ func file_app_dns_config_proto_rawDescGZIP() []byte {
return file_app_dns_config_proto_rawDescData return file_app_dns_config_proto_rawDescData
} }
var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_app_dns_config_proto_goTypes = []any{ var file_app_dns_config_proto_goTypes = []any{
(DomainMatchingType)(0), // 0: xray.app.dns.DomainMatchingType (QueryStrategy)(0), // 0: xray.app.dns.QueryStrategy
(QueryStrategy)(0), // 1: xray.app.dns.QueryStrategy (*NameServer)(nil), // 1: xray.app.dns.NameServer
(*NameServer)(nil), // 2: xray.app.dns.NameServer (*Config)(nil), // 2: xray.app.dns.Config
(*Config)(nil), // 3: xray.app.dns.Config (*Config_HostMapping)(nil), // 3: xray.app.dns.Config.HostMapping
(*NameServer_PriorityDomain)(nil), // 4: xray.app.dns.NameServer.PriorityDomain (*net.Endpoint)(nil), // 4: xray.common.net.Endpoint
(*NameServer_OriginalRule)(nil), // 5: xray.app.dns.NameServer.OriginalRule (*geodata.DomainRule)(nil), // 5: xray.common.geodata.DomainRule
(*Config_HostMapping)(nil), // 6: xray.app.dns.Config.HostMapping (*geodata.IPRule)(nil), // 6: xray.common.geodata.IPRule
(*net.Endpoint)(nil), // 7: xray.common.net.Endpoint
(*router.GeoIP)(nil), // 8: xray.app.router.GeoIP
} }
var file_app_dns_config_proto_depIdxs = []int32{ var file_app_dns_config_proto_depIdxs = []int32{
7, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint 4, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint
4, // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain 5, // 1: xray.app.dns.NameServer.domain:type_name -> xray.common.geodata.DomainRule
8, // 2: xray.app.dns.NameServer.expected_geoip:type_name -> xray.app.router.GeoIP 6, // 2: xray.app.dns.NameServer.expected_ip:type_name -> xray.common.geodata.IPRule
5, // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule 0, // 3: xray.app.dns.NameServer.query_strategy:type_name -> xray.app.dns.QueryStrategy
1, // 4: xray.app.dns.NameServer.query_strategy:type_name -> xray.app.dns.QueryStrategy 6, // 4: xray.app.dns.NameServer.unexpected_ip:type_name -> xray.common.geodata.IPRule
8, // 5: xray.app.dns.NameServer.unexpected_geoip:type_name -> xray.app.router.GeoIP 1, // 5: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer
2, // 6: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer 3, // 6: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping
6, // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping 0, // 7: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy
1, // 8: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy 5, // 8: xray.app.dns.Config.HostMapping.domain:type_name -> xray.common.geodata.DomainRule
0, // 9: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType 9, // [9:9] is the sub-list for method output_type
0, // 10: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType 9, // [9:9] is the sub-list for method input_type
11, // [11:11] is the sub-list for method output_type 9, // [9:9] is the sub-list for extension type_name
11, // [11:11] is the sub-list for method input_type 9, // [9:9] is the sub-list for extension extendee
11, // [11:11] is the sub-list for extension type_name 0, // [0:9] is the sub-list for field type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
} }
func init() { file_app_dns_config_proto_init() } func init() { file_app_dns_config_proto_init() }
@@ -732,8 +544,8 @@ func file_app_dns_config_proto_init() {
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dns_config_proto_rawDesc), len(file_app_dns_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dns_config_proto_rawDesc), len(file_app_dns_config_proto_rawDesc)),
NumEnums: 2, NumEnums: 1,
NumMessages: 5, NumMessages: 3,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+6 -25
View File
@@ -7,26 +7,15 @@ option java_package = "com.xray.app.dns";
option java_multiple_files = true; option java_multiple_files = true;
import "common/net/destination.proto"; import "common/net/destination.proto";
import "app/router/config.proto"; import "common/geodata/geodat.proto";
message NameServer { message NameServer {
xray.common.net.Endpoint address = 1; xray.common.net.Endpoint address = 1;
bytes client_ip = 5; bytes client_ip = 5;
bool skipFallback = 6; bool skipFallback = 6;
repeated xray.common.geodata.DomainRule domain = 2;
message PriorityDomain { repeated xray.common.geodata.IPRule expected_ip = 3;
DomainMatchingType type = 1; reserved 4;
string domain = 2;
}
message OriginalRule {
string rule = 1;
uint32 size = 2;
}
repeated PriorityDomain prioritized_domain = 2;
repeated xray.app.router.GeoIP expected_geoip = 3;
repeated OriginalRule original_rules = 4;
QueryStrategy query_strategy = 7; QueryStrategy query_strategy = 7;
bool actPrior = 8; bool actPrior = 8;
string tag = 9; string tag = 9;
@@ -35,18 +24,11 @@ message NameServer {
optional bool serveStale = 15; optional bool serveStale = 15;
optional uint32 serveExpiredTTL = 16; optional uint32 serveExpiredTTL = 16;
bool finalQuery = 12; bool finalQuery = 12;
repeated xray.app.router.GeoIP unexpected_geoip = 13; repeated xray.common.geodata.IPRule unexpected_ip = 13;
bool actUnprior = 14; bool actUnprior = 14;
uint32 policyID = 17; uint32 policyID = 17;
} }
enum DomainMatchingType {
Full = 0;
Subdomain = 1;
Keyword = 2;
Regex = 3;
}
enum QueryStrategy { enum QueryStrategy {
USE_IP = 0; USE_IP = 0;
USE_IP4 = 1; USE_IP4 = 1;
@@ -64,8 +46,7 @@ message Config {
bytes client_ip = 3; bytes client_ip = 3;
message HostMapping { message HostMapping {
DomainMatchingType type = 1; xray.common.geodata.DomainRule domain = 2;
string domain = 2;
repeated bytes ip = 3; repeated bytes ip = 3;
+71 -72
View File
@@ -12,13 +12,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
@@ -32,15 +30,15 @@ type DNS struct {
hosts *StaticHosts hosts *StaticHosts
clients []*Client clients []*Client
ctx context.Context ctx context.Context
domainMatcher strmatcher.IndexMatcher domainMatcher geodata.DomainMatcher
matcherInfos []*DomainMatcherInfo matcherInfos []*DomainMatcherInfo
checkSystem bool checkSystem bool
} }
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher // DomainMatcherInfo contains information attached to index returned by Server.domainMatcher.
type DomainMatcherInfo struct { type DomainMatcherInfo struct {
clientIdx uint16 clientIdx uint16
domainRuleIdx uint16 domainRule string
} }
// New creates a new DNS server with given configuration. // New creates a new DNS server with given configuration.
@@ -85,56 +83,40 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
return nil, errors.New("unexpected query strategy ", config.QueryStrategy) return nil, errors.New("unexpected query strategy ", config.QueryStrategy)
} }
var hosts *StaticHosts hosts, err := NewStaticHosts(config.StaticHosts)
mphLoaded := false if err != nil {
domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) return nil, errors.New("failed to create hosts").Base(err)
if domainMatcherPath != "" {
if f, err := os.Open(domainMatcherPath); err == nil {
defer f.Close()
if m, err := router.LoadGeoSiteMatcher(f, "HOSTS"); err == nil {
f.Seek(0, 0)
if hostIPs, err := router.LoadGeoSiteHosts(f); err == nil {
if sh, err := NewStaticHostsFromCache(m, hostIPs); err == nil {
hosts = sh
mphLoaded = true
errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for DNS hosts, size: ", sh.matchers.Size())
}
}
}
}
} }
if !mphLoaded {
sh, err := NewStaticHosts(config.StaticHosts)
if err != nil {
return nil, errors.New("failed to create hosts").Base(err)
}
hosts = sh
}
var clients []*Client
domainRuleCount := 0
var defaultTag = config.Tag var defaultTag = config.Tag
if len(config.Tag) == 0 { if len(config.Tag) == 0 {
defaultTag = generateRandomTag() defaultTag = generateRandomTag()
} }
for _, ns := range config.NameServer { clients := make([]*Client, 0, len(config.NameServer))
domainRuleCount += len(ns.PrioritizedDomain) matcherInfos := make([]*DomainMatcherInfo, 0)
} effectiveRules := make([]*geodata.DomainRule, 0)
// MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1
matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1)
domainMatcher := &strmatcher.MatcherGroup{}
for _, ns := range config.NameServer { for _, ns := range config.NameServer {
clientIdx := len(clients) clientIdx := len(clients)
updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) { updateRules := func(isLocalNameServer bool) {
midx := domainMatcher.Add(domainRule) // Prioritize local domains with specific TLDs or those without any dot for the local DNS
matcherInfos[midx] = &DomainMatcherInfo{ if isLocalNameServer {
clientIdx: uint16(clientIdx), effectiveRules = append(effectiveRules, localTLDsAndDotlessDomainsRules...)
domainRuleIdx: uint16(originalRuleIdx), for _, rule := range localTLDsAndDotlessDomainsRules {
matcherInfos = append(matcherInfos, &DomainMatcherInfo{
clientIdx: uint16(clientIdx),
domainRule: rule.String(),
})
}
}
effectiveRules = append(effectiveRules, ns.Domain...)
for _, rule := range ns.Domain {
matcherInfos = append(matcherInfos, &DomainMatcherInfo{
clientIdx: uint16(clientIdx),
domainRule: rule.String(),
})
} }
} }
@@ -163,18 +145,27 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
if len(ns.Tag) > 0 { if len(ns.Tag) > 0 {
tag = ns.Tag tag = ns.Tag
} }
clientIPOption := ResolveIpOptionOverride(ns.QueryStrategy, ipOption) clientIPOption := ResolveIpOptionOverride(ns.QueryStrategy, ipOption)
if !clientIPOption.IPv4Enable && !clientIPOption.IPv6Enable { if !clientIPOption.IPv4Enable && !clientIPOption.IPv6Enable {
return nil, errors.New("no QueryStrategy available for ", ns.Address) return nil, errors.New("no QueryStrategy available for ", ns.Address)
} }
client, err := NewClient(ctx, ns, myClientIP, disableCache, serveStale, serveExpiredTTL, tag, clientIPOption, &matcherInfos, updateDomain) client, err := NewClient(ctx, ns, myClientIP, disableCache, serveStale, serveExpiredTTL, tag, clientIPOption, updateRules)
if err != nil { if err != nil {
return nil, errors.New("failed to create client").Base(err) return nil, errors.New("failed to create client").Base(err)
} }
clients = append(clients, client) clients = append(clients, client)
} }
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 // If there is no DNS client in config, add a `localhost` DNS client
if len(clients) == 0 { if len(clients) == 0 {
clients = append(clients, NewLocalDNSClient(ipOption)) clients = append(clients, NewLocalDNSClient(ipOption))
@@ -283,24 +274,27 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching // Priority domain matching
hasMatch := false hasMatch := false
MatchSlice := s.domainMatcher.Match(domain) if s.domainMatcher != nil {
sort.Slice(MatchSlice, func(i, j int) bool { matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
return MatchSlice[i] < MatchSlice[j] sort.Slice(matchSlice, func(i, j int) bool {
}) return matchSlice[i] < matchSlice[j]
for _, match := range MatchSlice { })
info := s.matcherInfos[match] for _, match := range matchSlice {
client := s.clients[info.clientIdx] info := s.matcherInfos[match]
domainRule := client.domains[info.domainRuleIdx] client := s.clients[info.clientIdx]
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx)) domainRule := info.domainRule
if clientUsed[info.clientIdx] { domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
continue if clientUsed[info.clientIdx] {
} continue
clientUsed[info.clientIdx] = true }
clients = append(clients, client) clientUsed[info.clientIdx] = true
clientNames = append(clientNames, client.Name()) clients = append(clients, client)
hasMatch = true clientNames = append(clientNames, client.Name())
if client.finalQuery { hasMatch = true
return clients if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
} }
} }
@@ -314,17 +308,13 @@ func (s *DNS) sortClients(domain string) []*Client {
clients = append(clients, client) clients = append(clients, client)
clientNames = append(clientNames, client.Name()) clientNames = append(clientNames, client.Name())
if client.finalQuery { if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients return clients
} }
} }
} }
if len(domainRules) > 0 { logDecision(s.ctx, domain, domainRules, clientNames)
errors.LogDebug(s.ctx, "domain ", domain, " matches following rules: ", domainRules)
}
if len(clientNames) > 0 {
errors.LogDebug(s.ctx, "domain ", domain, " will use DNS in order: ", clientNames)
}
if len(clients) == 0 { if len(clients) == 0 {
if len(s.clients) > 0 { if len(s.clients) > 0 {
@@ -339,6 +329,15 @@ func (s *DNS) sortClients(domain string) []*Client {
return clients return clients
} }
func logDecision(ctx context.Context, domain string, domainRules []string, clientNames []string) {
if len(domainRules) > 0 {
errors.LogDebug(ctx, "domain ", domain, " matches following rules: ", domainRules)
}
if len(clientNames) > 0 {
errors.LogDebug(ctx, "domain ", domain, " will use DNS in order: ", clientNames)
}
}
func mergeQueryErrors(domain string, errs []error) error { func mergeQueryErrors(domain string, errs []error) error {
if len(errs) == 0 { if len(errs) == 0 {
return dns.ErrEmptyResponse return dns.ErrEmptyResponse
+68 -100
View File
@@ -11,9 +11,9 @@ import (
"github.com/xtls/xray-core/app/policy" "github.com/xtls/xray-core/app/policy"
"github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/app/proxyman"
_ "github.com/xtls/xray-core/app/proxyman/outbound" _ "github.com/xtls/xray-core/app/proxyman/outbound"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
@@ -147,7 +147,9 @@ func TestUDPServerSubnet(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -207,7 +209,9 @@ func TestUDPServer(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -331,10 +335,9 @@ func TestPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
{ {
Type: DomainMatchingType_Full, Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "google.com"}},
Domain: "google.com",
}, },
}, },
}, },
@@ -346,7 +349,9 @@ func TestPrioritizedDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -415,7 +420,9 @@ func TestUDPServerIPv6(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -471,8 +478,7 @@ func TestStaticHostDomain(t *testing.T) {
}, },
StaticHosts: []*Config_HostMapping{ StaticHosts: []*Config_HostMapping{
{ {
Type: DomainMatchingType_Full, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "example.com"}}},
Domain: "example.com",
ProxiedDomain: "google.com", ProxiedDomain: "google.com",
}, },
}, },
@@ -483,7 +489,9 @@ func TestStaticHostDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -539,17 +547,9 @@ func TestIPMatch(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ // inner ip, will not match
CountryCode: "local", {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
Cidr: []*router.CIDR{
{
// inner ip, will not match
Ip: []byte{192, 168, 11, 1},
Prefix: 32,
},
},
},
}, },
}, },
// second dns, match ip // second dns, match ip
@@ -563,25 +563,9 @@ func TestIPMatch(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
CountryCode: "test", {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
Cidr: []*router.CIDR{
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
CountryCode: "test",
Cidr: []*router.CIDR{
{
Ip: []byte{8, 8, 8, 4},
Prefix: 32,
},
},
},
}, },
}, },
}, },
@@ -592,7 +576,9 @@ func TestIPMatch(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -663,19 +649,15 @@ func TestLocalDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
// Equivalent of dotless:localhost // Equivalent of dotless:localhost
{Type: DomainMatchingType_Regex, Domain: "^[^.]*localhost[^.]*$"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^[^.]*localhost[^.]*$"}}},
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ // Will match localhost, localhost-a and localhost-b, // Will match localhost, localhost-a and localhost-b,
CountryCode: "local", {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}},
Cidr: []*router.CIDR{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}},
{Ip: []byte{127, 0, 0, 2}, Prefix: 32}, {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}},
{Ip: []byte{127, 0, 0, 3}, Prefix: 32},
{Ip: []byte{127, 0, 0, 4}, Prefix: 32},
},
},
}, },
}, },
{ {
@@ -688,23 +670,21 @@ func TestLocalDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
// Equivalent of dotless: and domain:local // Equivalent of dotless: and domain:local
{Type: DomainMatchingType_Regex, Domain: "^[^.]*$"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^[^.]*$"}}},
{Type: DomainMatchingType_Subdomain, Domain: "local"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "local"}}},
{Type: DomainMatchingType_Subdomain, Domain: "localdomain"}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "localdomain"}}},
}, },
}, },
}, },
StaticHosts: []*Config_HostMapping{ StaticHosts: []*Config_HostMapping{
{ {
Type: DomainMatchingType_Full, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "hostnamestatic"}}},
Domain: "hostnamestatic",
Ip: [][]byte{{127, 0, 0, 53}}, Ip: [][]byte{{127, 0, 0, 53}},
}, },
{ {
Type: DomainMatchingType_Full, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "hostnamealias"}}},
Domain: "hostnamealias",
ProxiedDomain: "hostname.localdomain", ProxiedDomain: "hostname.localdomain",
}, },
}, },
@@ -715,7 +695,9 @@ func TestLocalDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
@@ -891,19 +873,15 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
{ {
Type: DomainMatchingType_Subdomain, Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "google.com"}},
Domain: "google.com",
}, },
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ // Will only match 8.8.8.8 and 8.8.4.4 // Will only match 8.8.8.8 and 8.8.4.4
Cidr: []*router.CIDR{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
{Ip: []byte{8, 8, 8, 8}, Prefix: 32}, {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}},
{Ip: []byte{8, 8, 4, 4}, Prefix: 32},
},
},
}, },
}, },
{ {
@@ -916,18 +894,14 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
{ {
Type: DomainMatchingType_Subdomain, Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "google.com"}},
Domain: "google.com",
}, },
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ // Will match 8.8.8.8 and 8.8.8.7, etc // Will match 8.8.8.8 and 8.8.8.7, etc
Cidr: []*router.CIDR{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
{Ip: []byte{8, 8, 8, 7}, Prefix: 24},
},
},
}, },
}, },
{ {
@@ -940,18 +914,14 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
{ {
Type: DomainMatchingType_Subdomain, Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "api.google.com"}},
Domain: "api.google.com",
}, },
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ // Will only match 8.8.7.7 (api.google.com) // Will only match 8.8.7.7 (api.google.com)
Cidr: []*router.CIDR{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
{Ip: []byte{8, 8, 7, 7}, Prefix: 32},
},
},
}, },
}, },
{ {
@@ -964,18 +934,14 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
PrioritizedDomain: []*NameServer_PriorityDomain{ Domain: []*geodata.DomainRule{
{ {
Type: DomainMatchingType_Full, Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "v2.api.google.com"}},
Domain: "v2.api.google.com",
}, },
}, },
ExpectedGeoip: []*router.GeoIP{ ExpectedIp: []*geodata.IPRule{
{ // Will only match 8.8.7.8 (v2.api.google.com) // Will only match 8.8.7.8 (v2.api.google.com)
Cidr: []*router.CIDR{ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
{Ip: []byte{8, 8, 7, 8}, Prefix: 32},
},
},
}, },
}, },
}, },
@@ -986,7 +952,9 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{}), ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
}, },
}, },
} }
+37 -81
View File
@@ -2,39 +2,28 @@ package dns
import ( import (
"context" "context"
"runtime"
"strconv" "strconv"
"strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
// StaticHosts represents static domain-ip mapping in DNS server. // StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct { type StaticHosts struct {
ips [][]net.Address responses [][]net.Address
matchers strmatcher.IndexMatcher matcher geodata.DomainMatcher
} }
// NewStaticHosts creates a new StaticHosts instance. // NewStaticHosts creates a new StaticHosts instance.
func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) { func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
g := new(strmatcher.MatcherGroup) reps := make([][]net.Address, 0, len(hosts))
sh := &StaticHosts{ rules := make([]*geodata.DomainRule, 0, len(hosts))
ips: make([][]net.Address, len(hosts)+16),
matchers: g,
}
defer runtime.GC() for _, mapping := range hosts {
for i, mapping := range hosts { rep := make([]net.Address, 0, len(mapping.Ip))
hosts[i] = nil
matcher, err := toStrMatcher(mapping.Type, mapping.Domain)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to create domain matcher, ignore domain rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]")
continue
}
id := g.Add(matcher)
ips := make([]net.Address, 0, len(mapping.Ip)+1)
switch { switch {
case len(mapping.ProxiedDomain) > 0: case len(mapping.ProxiedDomain) > 0:
if mapping.ProxiedDomain[0] == '#' { if mapping.ProxiedDomain[0] == '#' {
@@ -42,28 +31,36 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
ips = append(ips, dns.RCodeError(rcode)) rep = append(rep, dns.RCodeError(rcode))
} else { } else {
ips = append(ips, net.DomainAddress(mapping.ProxiedDomain)) rep = append(rep, net.DomainAddress(mapping.ProxiedDomain))
} }
case len(mapping.Ip) > 0: case len(mapping.Ip) > 0:
for _, ip := range mapping.Ip { for _, ip := range mapping.Ip {
addr := net.IPAddress(ip) addr := net.IPAddress(ip)
if addr == nil { if addr == nil {
errors.LogError(context.Background(), "invalid IP address in static hosts: ", ip, ", ignore this ip for rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]") errors.LogError(context.Background(), "invalid IP address in static hosts: ", ip, ", ignore this ip for rule: ", mapping.Domain)
continue continue
} }
ips = append(ips, addr) rep = append(rep, addr)
}
if len(ips) == 0 {
continue
} }
} }
reps = append(reps, rep)
sh.ips[id] = ips rules = append(rules, mapping.Domain)
} }
return sh, nil if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
responses: reps,
matcher: matcher,
}, nil
} }
func filterIP(ips []net.Address, option dns.IPOption) []net.Address { func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
@@ -79,16 +76,16 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) { func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0) ips := make([]net.Address, 0)
found := false found := false
for _, id := range h.matchers.Match(domain) { for _, idx := range h.matcher.Match(domain) {
for _, v := range h.ips[id] { for _, rep := range h.responses[idx] {
if err, ok := v.(dns.RCodeError); ok { if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 { if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
} }
return nil, err return nil, err
} }
} }
ips = append(ips, h.ips[id]...) ips = append(ips, h.responses[idx]...)
found = true found = true
} }
if !found { if !found {
@@ -98,10 +95,13 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
} }
func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) ([]net.Address, error) { func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) ([]net.Address, error) {
domain = strings.ToLower(domain)
switch addrs, err := h.lookupInternal(domain); { switch addrs, err := h.lookupInternal(domain); {
case err != nil: case err != nil:
return nil, err return nil, err
case len(addrs) == 0: // Not recorded in static hosts, return nil case addrs == nil: // Not recorded in static hosts, return nil
return nil, nil
case len(addrs) == 0: // Domain recorded, but no valid IP returned
return addrs, nil return addrs, nil
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
errors.LogDebug(context.Background(), "found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it") errors.LogDebug(context.Background(), "found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it")
@@ -122,52 +122,8 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts. // Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) { func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5) return h.lookup(domain, option, 5)
} }
func NewStaticHostsFromCache(matcher strmatcher.IndexMatcher, hostIPs map[string][]string) (*StaticHosts, error) {
sh := &StaticHosts{
ips: make([][]net.Address, matcher.Size()+1),
matchers: matcher,
}
order := hostIPs["_ORDER"]
var offset uint32
img, ok := matcher.(*strmatcher.IndexMatcherGroup)
if !ok {
// Single matcher (e.g. only manual or only one geosite)
if len(order) > 0 {
pattern := order[0]
ips := parseIPs(hostIPs[pattern])
for i := uint32(1); i <= matcher.Size(); i++ {
sh.ips[i] = ips
}
}
return sh, nil
}
for i, m := range img.Matchers {
if i < len(order) {
pattern := order[i]
ips := parseIPs(hostIPs[pattern])
for j := uint32(1); j <= m.Size(); j++ {
sh.ips[offset+j] = ips
}
offset += m.Size()
}
}
return sh, nil
}
func parseIPs(raw []string) []net.Address {
addrs := make([]net.Address, 0, len(raw))
for _, s := range raw {
if len(s) > 1 && s[0] == '#' {
rcode, _ := strconv.Atoi(s[1:])
addrs = append(addrs, dns.RCodeError(rcode))
} else {
addrs = append(addrs, net.ParseAddress(s))
}
}
return addrs
}
+7 -68
View File
@@ -1,13 +1,12 @@
package dns_test package dns_test
import ( import (
"bytes"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
. "github.com/xtls/xray-core/app/dns" . "github.com/xtls/xray-core/app/dns"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
@@ -15,20 +14,17 @@ import (
func TestStaticHosts(t *testing.T) { func TestStaticHosts(t *testing.T) {
pb := []*Config_HostMapping{ pb := []*Config_HostMapping{
{ {
Type: DomainMatchingType_Subdomain, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "lan"}}},
Domain: "lan",
ProxiedDomain: "#3", ProxiedDomain: "#3",
}, },
{ {
Type: DomainMatchingType_Full, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "example.com"}}},
Domain: "example.com",
Ip: [][]byte{ Ip: [][]byte{
{1, 1, 1, 1}, {1, 1, 1, 1},
}, },
}, },
{ {
Type: DomainMatchingType_Full, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "proxy.xray.com"}}},
Domain: "proxy.xray.com",
Ip: [][]byte{ Ip: [][]byte{
{1, 2, 3, 4}, {1, 2, 3, 4},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
@@ -36,20 +32,17 @@ func TestStaticHosts(t *testing.T) {
ProxiedDomain: "another-proxy.xray.com", ProxiedDomain: "another-proxy.xray.com",
}, },
{ {
Type: DomainMatchingType_Full, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "proxy2.xray.com"}}},
Domain: "proxy2.xray.com",
ProxiedDomain: "proxy.xray.com", ProxiedDomain: "proxy.xray.com",
}, },
{ {
Type: DomainMatchingType_Subdomain, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "example.cn"}}},
Domain: "example.cn",
Ip: [][]byte{ Ip: [][]byte{
{2, 2, 2, 2}, {2, 2, 2, 2},
}, },
}, },
{ {
Type: DomainMatchingType_Subdomain, Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "baidu.com"}}},
Domain: "baidu.com",
Ip: [][]byte{ Ip: [][]byte{
{127, 0, 0, 1}, {127, 0, 0, 1},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
@@ -132,57 +125,3 @@ func TestStaticHosts(t *testing.T) {
} }
} }
} }
func TestStaticHostsFromCache(t *testing.T) {
sites := []*router.GeoSite{
{
CountryCode: "cloudflare-dns.com",
Domain: []*router.Domain{
{Type: router.Domain_Full, Value: "example.com"},
},
},
{
CountryCode: "geosite:cn",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "baidu.cn"},
},
},
}
deps := map[string][]string{
"HOSTS": {"cloudflare-dns.com", "geosite:cn"},
}
hostIPs := map[string][]string{
"cloudflare-dns.com": {"1.1.1.1"},
"geosite:cn": {"2.2.2.2"},
"_ORDER": {"cloudflare-dns.com", "geosite:cn"},
}
var buf bytes.Buffer
err := router.SerializeGeoSiteList(sites, deps, hostIPs, &buf)
common.Must(err)
// Load matcher
m, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "HOSTS")
common.Must(err)
// Load hostIPs
f := bytes.NewReader(buf.Bytes())
hips, err := router.LoadGeoSiteHosts(f)
common.Must(err)
hosts, err := NewStaticHostsFromCache(m, hips)
common.Must(err)
{
ips, _ := hosts.Lookup("example.com", dns.IPOption{IPv4Enable: true})
if len(ips) != 1 || ips[0].String() != "1.1.1.1" {
t.Error("failed to lookup example.com from cache")
}
}
{
ips, _ := hosts.Lookup("baidu.cn", dns.IPOption{IPv4Enable: true})
if len(ips) != 1 || ips[0].String() != "2.2.2.2" {
t.Error("failed to lookup baidu.cn from cache deps")
}
}
}
+12 -104
View File
@@ -3,34 +3,18 @@ package dns
import ( import (
"context" "context"
"net/url" "net/url"
"runtime"
"strings" "strings"
"time" "time"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
type mphMatcherWrapper struct {
m strmatcher.IndexMatcher
}
func (w *mphMatcherWrapper) Match(s string) bool {
return w.m.Match(s) != nil
}
func (w *mphMatcherWrapper) String() string {
return "mph-matcher"
}
// Server is the interface for Name Server. // Server is the interface for Name Server.
type Server interface { type Server interface {
// Name of the Client. // Name of the Client.
@@ -46,9 +30,8 @@ type Server interface {
type Client struct { type Client struct {
server Server server Server
skipFallback bool skipFallback bool
domains []string expectedIPs geodata.IPMatcher
expectedIPs router.GeoIPMatcher unexpectedIPs geodata.IPMatcher
unexpectedIPs router.GeoIPMatcher
actPrior bool actPrior bool
actUnprior bool actUnprior bool
tag string tag string
@@ -111,11 +94,9 @@ func NewClient(
disableCache bool, serveStale bool, serveExpiredTTL uint32, disableCache bool, serveStale bool, serveExpiredTTL uint32,
tag string, tag string,
ipOption dns.IPOption, ipOption dns.IPOption,
matcherInfos *[]*DomainMatcherInfo, updateRules func(bool),
updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo),
) (*Client, error) { ) (*Client, error) {
client := &Client{} client := &Client{}
err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
// Create a new server for each client for now // Create a new server for each client for now
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP) server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
@@ -123,97 +104,25 @@ func NewClient(
return errors.New("failed to create nameserver").Base(err).AtWarning() return errors.New("failed to create nameserver").Base(err).AtWarning()
} }
// Prioritize local domains with specific TLDs or those without any dot for the local DNS _, isLocalDNS := server.(*LocalNameServer)
if _, isLocalDNS := server.(*LocalNameServer); isLocalDNS { updateRules(isLocalDNS)
ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
ns.OriginalRules = append(ns.OriginalRules, localTLDsAndDotlessDomainsRule)
// The following lines is a solution to avoid core panicsrule index out of range when setting `localhost` DNS client in config.
// Because the `localhost` DNS client will append len(localTLDsAndDotlessDomains) rules into matcherInfos to match `geosite:private` default rule.
// But `matcherInfos` has no enough length to add rules, which leads to core panics (rule index out of range).
// To avoid this, the length of `matcherInfos` must be equal to the expected, so manually append it with Golang default zero value first for later modification.
// Related issues:
// https://github.com/v2fly/v2ray-core/issues/529
// https://github.com/v2fly/v2ray-core/issues/719
for i := 0; i < len(localTLDsAndDotlessDomains); i++ {
*matcherInfos = append(*matcherInfos, &DomainMatcherInfo{
clientIdx: uint16(0),
domainRuleIdx: uint16(0),
})
}
}
// Establish domain rules
var rules []string
ruleCurr := 0
ruleIter := 0
// Check if domain matcher cache is provided via environment
domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" })
var mphLoaded bool
if domainMatcherPath != "" && ns.Tag != "" {
f, err := filesystem.NewFileReader(domainMatcherPath)
if err == nil {
defer f.Close()
g, err := router.LoadGeoSiteMatcher(f, ns.Tag)
if err == nil {
errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for ", ns.Tag, " dns tag)")
updateDomainRule(&mphMatcherWrapper{m: g}, 0, *matcherInfos)
rules = append(rules, "[MPH Cache]")
mphLoaded = true
}
}
}
if !mphLoaded {
for i, domain := range ns.PrioritizedDomain {
ns.PrioritizedDomain[i] = nil
domainRule, err := toStrMatcher(domain.Type, domain.Domain)
if err != nil {
errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]")
domainRule, _ = toStrMatcher(DomainMatchingType_Full, "hack.fix.index.for.illegal.domain.rule")
}
originalRuleIdx := ruleCurr
if ruleCurr < len(ns.OriginalRules) {
rule := ns.OriginalRules[ruleCurr]
if ruleCurr >= len(rules) {
rules = append(rules, rule.Rule)
}
ruleIter++
if ruleIter >= int(rule.Size) {
ruleIter = 0
ruleCurr++
}
} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
rules = append(rules, domainRule.String())
ruleCurr++
}
updateDomainRule(domainRule, originalRuleIdx, *matcherInfos)
}
}
ns.PrioritizedDomain = nil
runtime.GC()
// Establish expected IPs // Establish expected IPs
var expectedMatcher router.GeoIPMatcher var expectedMatcher geodata.IPMatcher
if len(ns.ExpectedGeoip) > 0 { if len(ns.ExpectedIp) > 0 {
expectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.ExpectedGeoip...) expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp)
if err != nil { if err != nil {
return errors.New("failed to create expected ip matcher").Base(err).AtWarning() return errors.New("failed to create expected ip matcher").Base(err).AtWarning()
} }
ns.ExpectedGeoip = nil
runtime.GC()
} }
// Establish unexpected IPs // Establish unexpected IPs
var unexpectedMatcher router.GeoIPMatcher var unexpectedMatcher geodata.IPMatcher
if len(ns.UnexpectedGeoip) > 0 { if len(ns.UnexpectedIp) > 0 {
unexpectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.UnexpectedGeoip...) unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp)
if err != nil { if err != nil {
return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning() return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning()
} }
ns.UnexpectedGeoip = nil
runtime.GC()
} }
if len(clientIP) > 0 { if len(clientIP) > 0 {
@@ -234,7 +143,6 @@ func NewClient(
client.server = server client.server = server
client.skipFallback = ns.SkipFallback client.skipFallback = ns.SkipFallback
client.domains = rules
client.expectedIPs = expectedMatcher client.expectedIPs = expectedMatcher
client.unexpectedIPs = unexpectedMatcher client.unexpectedIPs = unexpectedMatcher
client.actPrior = ns.ActPrior client.actPrior = ns.ActPrior
+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 -6
View File
@@ -8,7 +8,6 @@ import (
"strings" "strings"
"github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
@@ -39,16 +38,12 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er
c.ohm = om c.ohm = om
})) }))
expvar.Publish("stats", expvar.Func(func() interface{} { expvar.Publish("stats", expvar.Func(func() interface{} {
manager, ok := c.statsManager.(*stats.Manager)
if !ok {
return nil
}
resp := map[string]map[string]map[string]int64{ resp := map[string]map[string]map[string]int64{
"inbound": {}, "inbound": {},
"outbound": {}, "outbound": {},
"user": {}, "user": {},
} }
manager.VisitCounters(func(name string, counter feature_stats.Counter) bool { c.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>") nameSplit := strings.Split(name, ">>>")
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3] typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
if item, found := resp[typeName][tagOrUser]; found { if item, found := resp[typeName][tagOrUser]; found {
+13 -4
View File
@@ -5,6 +5,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"sort" "sort"
"sync" "sync"
"time" "time"
@@ -70,7 +71,7 @@ func (o *Observer) background() {
outbounds := hs.Select(o.config.SubjectSelector) outbounds := hs.Select(o.config.SubjectSelector)
o.updateStatus(outbounds) o.clearRemovedOutbounds(outbounds)
sleepTime := time.Second * 10 sleepTime := time.Second * 10
if o.config.ProbeInterval != 0 { if o.config.ProbeInterval != 0 {
@@ -111,11 +112,19 @@ func (o *Observer) background() {
} }
} }
func (o *Observer) updateStatus(outbounds []string) { func (o *Observer) clearRemovedOutbounds(outbounds []string) {
o.statusLock.Lock() o.statusLock.Lock()
defer o.statusLock.Unlock() defer o.statusLock.Unlock()
// TODO should remove old inbound that is removed if len(o.status) == 0 {
_ = outbounds return
}
var pruned []*OutboundStatus
for _, status := range o.status {
if slices.Contains(outbounds, status.OutboundTag) {
pruned = append(pruned, status)
}
}
o.status = pruned
} }
func (o *Observer) probe(outbound string) ProbeResult { func (o *Observer) probe(outbound string) ProbeResult {
+64
View File
@@ -0,0 +1,64 @@
package observatory
import "testing"
func TestObserverUpdateStatusPrunesStaleOutbounds(t *testing.T) {
observer := &Observer{
status: []*OutboundStatus{
{
OutboundTag: "keep",
Alive: true,
Delay: 42,
LastErrorReason: "",
LastSeenTime: 111,
LastTryTime: 222,
},
{
OutboundTag: "drop",
Alive: false,
Delay: 99999999,
LastErrorReason: "probe failed",
LastSeenTime: 333,
LastTryTime: 444,
},
},
}
observer.clearRemovedOutbounds([]string{"keep"})
if len(observer.status) != 1 {
t.Fatalf("expected 1 status after pruning, got %d", len(observer.status))
}
got := observer.status[0]
if got.OutboundTag != "keep" {
t.Fatalf("expected remaining status for keep, got %q", got.OutboundTag)
}
if !got.Alive {
t.Fatal("expected remaining status to preserve Alive field")
}
if got.Delay != 42 {
t.Fatalf("expected remaining status to preserve Delay, got %d", got.Delay)
}
if got.LastSeenTime != 111 {
t.Fatalf("expected remaining status to preserve LastSeenTime, got %d", got.LastSeenTime)
}
if got.LastTryTime != 222 {
t.Fatalf("expected remaining status to preserve LastTryTime, got %d", got.LastTryTime)
}
}
func TestObserverUpdateStatusClearsWhenNoOutboundsRemain(t *testing.T) {
observer := &Observer{
status: []*OutboundStatus{
{OutboundTag: "drop-1"},
{OutboundTag: "drop-2"},
},
}
observer.clearRemovedOutbounds(nil)
if len(observer.status) != 0 {
t.Fatalf("expected all statuses to be removed, got %d", len(observer.status))
}
}
+33
View File
@@ -1 +1,34 @@
package proxyman package proxyman
import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/session"
)
func BuildSniffingRequest(config *SniffingConfig) (session.SniffingRequest, error) {
if config == nil {
return session.SniffingRequest{}, nil
}
request := session.SniffingRequest{
Enabled: config.Enabled,
OverrideDestinationForProtocol: config.DestinationOverride,
MetadataOnly: config.MetadataOnly,
RouteOnly: config.RouteOnly,
}
if len(config.DomainsExcluded) > 0 {
excludeForDomain, err := geodata.DomainReg.BuildDomainMatcher(config.DomainsExcluded)
if err != nil {
return session.SniffingRequest{}, err
}
request.ExcludeForDomain = excludeForDomain
}
if len(config.IpsExcluded) > 0 {
excludeForIP, err := geodata.IPReg.BuildIPMatcher(config.IpsExcluded)
if err != nil {
return session.SniffingRequest{}, err
}
request.ExcludeForIP = excludeForIP
}
return request, nil
}
+43 -29
View File
@@ -7,6 +7,7 @@
package proxyman package proxyman
import ( import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net" net "github.com/xtls/xray-core/common/net"
serial "github.com/xtls/xray-core/common/serial" serial "github.com/xtls/xray-core/common/serial"
internet "github.com/xtls/xray-core/transport/internet" internet "github.com/xtls/xray-core/transport/internet"
@@ -66,8 +67,9 @@ type SniffingConfig struct {
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
// Override target destination if sniff'ed protocol is in the given list. // Override target destination if sniff'ed protocol is in the given list.
// Supported values are "http", "tls", "fakedns". // Supported values are "http", "tls", "fakedns".
DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"`
DomainsExcluded []string `protobuf:"bytes,3,rep,name=domains_excluded,json=domainsExcluded,proto3" json:"domains_excluded,omitempty"` DomainsExcluded []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domains_excluded,json=domainsExcluded,proto3" json:"domains_excluded,omitempty"`
IpsExcluded []*geodata.IPRule `protobuf:"bytes,6,rep,name=ips_excluded,json=ipsExcluded,proto3" json:"ips_excluded,omitempty"`
// Whether should only try to sniff metadata without waiting for client input. // Whether should only try to sniff metadata without waiting for client input.
// Can be used to support SMTP like protocol where server send the first // Can be used to support SMTP like protocol where server send the first
// message. // message.
@@ -121,13 +123,20 @@ func (x *SniffingConfig) GetDestinationOverride() []string {
return nil return nil
} }
func (x *SniffingConfig) GetDomainsExcluded() []string { func (x *SniffingConfig) GetDomainsExcluded() []*geodata.DomainRule {
if x != nil { if x != nil {
return x.DomainsExcluded return x.DomainsExcluded
} }
return nil return nil
} }
func (x *SniffingConfig) GetIpsExcluded() []*geodata.IPRule {
if x != nil {
return x.IpsExcluded
}
return nil
}
func (x *SniffingConfig) GetMetadataOnly() bool { func (x *SniffingConfig) GetMetadataOnly() bool {
if x != nil { if x != nil {
return x.MetadataOnly return x.MetadataOnly
@@ -477,12 +486,13 @@ var File_app_proxyman_config_proto protoreflect.FileDescriptor
const file_app_proxyman_config_proto_rawDesc = "" + const file_app_proxyman_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x19app/proxyman/config.proto\x12\x11xray.app.proxyman\x1a\x18common/net/address.proto\x1a\x15common/net/port.proto\x1a\x1ftransport/internet/config.proto\x1a!common/serial/typed_message.proto\"\x0f\n" + "\x19app/proxyman/config.proto\x12\x11xray.app.proxyman\x1a\x18common/net/address.proto\x1a\x15common/net/port.proto\x1a\x1ftransport/internet/config.proto\x1a!common/serial/typed_message.proto\x1a\x1bcommon/geodata/geodat.proto\"\x0f\n" +
"\rInboundConfig\"\xcc\x01\n" + "\rInboundConfig\"\xad\x02\n" +
"\x0eSniffingConfig\x12\x18\n" + "\x0eSniffingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x121\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x121\n" +
"\x14destination_override\x18\x02 \x03(\tR\x13destinationOverride\x12)\n" + "\x14destination_override\x18\x02 \x03(\tR\x13destinationOverride\x12J\n" +
"\x10domains_excluded\x18\x03 \x03(\tR\x0fdomainsExcluded\x12#\n" + "\x10domains_excluded\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x0fdomainsExcluded\x12>\n" +
"\fips_excluded\x18\x06 \x03(\v2\x1b.xray.common.geodata.IPRuleR\vipsExcluded\x12#\n" +
"\rmetadata_only\x18\x04 \x01(\bR\fmetadataOnly\x12\x1d\n" + "\rmetadata_only\x18\x04 \x01(\bR\fmetadataOnly\x12\x1d\n" +
"\n" + "\n" +
"route_only\x18\x05 \x01(\bR\trouteOnly\"\xe5\x02\n" + "route_only\x18\x05 \x01(\bR\trouteOnly\"\xe5\x02\n" +
@@ -532,30 +542,34 @@ var file_app_proxyman_config_proto_goTypes = []any{
(*OutboundConfig)(nil), // 4: xray.app.proxyman.OutboundConfig (*OutboundConfig)(nil), // 4: xray.app.proxyman.OutboundConfig
(*SenderConfig)(nil), // 5: xray.app.proxyman.SenderConfig (*SenderConfig)(nil), // 5: xray.app.proxyman.SenderConfig
(*MultiplexingConfig)(nil), // 6: xray.app.proxyman.MultiplexingConfig (*MultiplexingConfig)(nil), // 6: xray.app.proxyman.MultiplexingConfig
(*net.PortList)(nil), // 7: xray.common.net.PortList (*geodata.DomainRule)(nil), // 7: xray.common.geodata.DomainRule
(*net.IPOrDomain)(nil), // 8: xray.common.net.IPOrDomain (*geodata.IPRule)(nil), // 8: xray.common.geodata.IPRule
(*internet.StreamConfig)(nil), // 9: xray.transport.internet.StreamConfig (*net.PortList)(nil), // 9: xray.common.net.PortList
(*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage (*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
(*internet.ProxyConfig)(nil), // 11: xray.transport.internet.ProxyConfig (*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
(internet.DomainStrategy)(0), // 12: xray.transport.internet.DomainStrategy (*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
} }
var file_app_proxyman_config_proto_depIdxs = []int32{ var file_app_proxyman_config_proto_depIdxs = []int32{
7, // 0: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList 7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
8, // 1: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain 8, // 1: xray.app.proxyman.SniffingConfig.ips_excluded:type_name -> xray.common.geodata.IPRule
9, // 2: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig 9, // 2: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList
1, // 3: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig 10, // 3: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain
10, // 4: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage 11, // 4: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
10, // 5: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage 1, // 5: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig
8, // 6: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain 12, // 6: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage
9, // 7: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig 12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
11, // 8: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig 10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
6, // 9: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig 11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
12, // 10: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy 13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
11, // [11:11] is the sub-list for method output_type 6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
11, // [11:11] is the sub-list for method input_type 14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
11, // [11:11] is the sub-list for extension type_name 13, // [13:13] is the sub-list for method output_type
11, // [11:11] is the sub-list for extension extendee 13, // [13:13] is the sub-list for method input_type
0, // [0:11] is the sub-list for field type_name 13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
} }
func init() { file_app_proxyman_config_proto_init() } func init() { file_app_proxyman_config_proto_init() }
+5 -1
View File
@@ -10,6 +10,7 @@ import "common/net/address.proto";
import "common/net/port.proto"; import "common/net/port.proto";
import "transport/internet/config.proto"; import "transport/internet/config.proto";
import "common/serial/typed_message.proto"; import "common/serial/typed_message.proto";
import "common/geodata/geodat.proto";
message InboundConfig {} message InboundConfig {}
@@ -20,7 +21,10 @@ message SniffingConfig {
// Override target destination if sniff'ed protocol is in the given list. // Override target destination if sniff'ed protocol is in the given list.
// Supported values are "http", "tls", "fakedns". // Supported values are "http", "tls", "fakedns".
repeated string destination_override = 2; repeated string destination_override = 2;
repeated string domains_excluded = 3;
repeated xray.common.geodata.DomainRule domains_excluded = 3;
repeated xray.common.geodata.IPRule ips_excluded = 6;
// Whether should only try to sniff metadata without waiting for client input. // Whether should only try to sniff metadata without waiting for client input.
// Can be used to support SMTP like protocol where server send the first // Can be used to support SMTP like protocol where server send the first
+10 -10
View File
@@ -53,18 +53,17 @@ type AlwaysOnInboundHandler struct {
} }
func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) { func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) {
sniffingRequest, err := proxyman.BuildSniffingRequest(receiverConfig.SniffingSettings)
if err != nil {
return nil, err
}
// Set tag and sniffing config in context before creating proxy // Set tag and sniffing config in context before creating proxy
// This allows proxies like TUN to access these settings // This allows proxies like TUN to access these settings
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag}) ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag})
if receiverConfig.SniffingSettings != nil { if receiverConfig.SniffingSettings != nil {
ctx = session.ContextWithContent(ctx, &session.Content{ ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: session.SniffingRequest{ SniffingRequest: sniffingRequest,
Enabled: receiverConfig.SniffingSettings.Enabled,
OverrideDestinationForProtocol: receiverConfig.SniffingSettings.DestinationOverride,
ExcludeForDomain: receiverConfig.SniffingSettings.DomainsExcluded,
MetadataOnly: receiverConfig.SniffingSettings.MetadataOnly,
RouteOnly: receiverConfig.SniffingSettings.RouteOnly,
},
}) })
} }
rawProxy, err := common.CreateObject(ctx, proxyConfig) rawProxy, err := common.CreateObject(ctx, proxyConfig)
@@ -117,7 +116,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
stream: mss, stream: mss,
tag: tag, tag: tag,
dispatcher: h.mux, dispatcher: h.mux,
sniffingConfig: receiverConfig.SniffingSettings, sniffingRequest: sniffingRequest,
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
ctx: ctx, ctx: ctx,
@@ -139,7 +138,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
recvOrigDest: receiverConfig.ReceiveOriginalDestination, recvOrigDest: receiverConfig.ReceiveOriginalDestination,
tag: tag, tag: tag,
dispatcher: h.mux, dispatcher: h.mux,
sniffingConfig: receiverConfig.SniffingSettings, sniffingRequest: sniffingRequest,
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
ctx: ctx, ctx: ctx,
@@ -154,7 +153,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
address: address, address: address,
port: net.Port(port), port: net.Port(port),
dispatcher: h.mux, dispatcher: h.mux,
sniffingConfig: receiverConfig.SniffingSettings, sniffingRequest: sniffingRequest,
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
stream: mss, stream: mss,
@@ -186,6 +185,7 @@ func (h *AlwaysOnInboundHandler) Close() error {
errs = append(errs, worker.Close()) errs = append(errs, worker.Close())
} }
errs = append(errs, h.mux.Close()) errs = append(errs, h.mux.Close())
errs = append(errs, common.Close(h.proxy))
if err := errors.Combine(errs...); err != nil { if err := errors.Combine(errs...); err != nil {
return errors.New("failed to close all resources").Base(err) return errors.New("failed to close all resources").Base(err)
} }
+6 -25
View File
@@ -6,7 +6,6 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
c "github.com/xtls/xray-core/common/ctx" c "github.com/xtls/xray-core/common/ctx"
@@ -43,7 +42,7 @@ type tcpWorker struct {
recvOrigDest bool recvOrigDest bool
tag string tag string
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
sniffingConfig *proxyman.SniffingConfig sniffingRequest session.SniffingRequest
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
@@ -118,13 +117,7 @@ func (w *tcpWorker) callback(conn stat.Connection) {
}) })
content := new(session.Content) content := new(session.Content)
if w.sniffingConfig != nil { content.SniffingRequest = w.sniffingRequest
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
}
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil {
@@ -275,7 +268,7 @@ type udpWorker struct {
tag string tag string
stream *internet.MemoryStreamConfig stream *internet.MemoryStreamConfig
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
sniffingConfig *proxyman.SniffingConfig sniffingRequest session.SniffingRequest
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
@@ -365,13 +358,7 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
Tag: w.tag, Tag: w.tag,
}) })
content := new(session.Content) content := new(session.Content)
if w.sniffingConfig != nil { content.SniffingRequest = w.sniffingRequest
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
}
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil {
errors.LogInfoInner(ctx, err, "connection ends") errors.LogInfoInner(ctx, err, "connection ends")
@@ -487,7 +474,7 @@ type dsWorker struct {
stream *internet.MemoryStreamConfig stream *internet.MemoryStreamConfig
tag string tag string
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
sniffingConfig *proxyman.SniffingConfig sniffingRequest session.SniffingRequest
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
@@ -517,13 +504,7 @@ func (w *dsWorker) callback(conn stat.Connection) {
}) })
content := new(session.Content) content := new(session.Content)
if w.sniffingConfig != nil { content.SniffingRequest = w.sniffingRequest
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
}
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil {
+4 -3
View File
@@ -12,6 +12,7 @@ import (
. "github.com/xtls/xray-core/app/router/command" . "github.com/xtls/xray-core/app/router/command"
"github.com/xtls/xray-core/app/stats" "github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/testing/mocks" "github.com/xtls/xray-core/testing/mocks"
@@ -303,12 +304,12 @@ func TestServiceTestRoute(t *testing.T) {
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
Domain: []*router.Domain{{Type: router.Domain_Domain, Value: "com"}}, Domain: []*geodata.DomainRule{{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "com"}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
SourceGeoip: []*router.GeoIP{{CountryCode: "private", Cidr: []*router.CIDR{{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}, SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
UserEmail: []string{"example@example.com"}, UserEmail: []string{"example@example.com"},
+29 -66
View File
@@ -2,7 +2,6 @@ package router
import ( import (
"context" "context"
"io"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -10,9 +9,10 @@ import (
"strings" "strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
) )
type Condition interface { type Condition interface {
@@ -45,67 +45,18 @@ func (v *ConditionChan) Len() int {
return len(*v) return len(*v)
} }
var matcherTypeMap = map[Domain_Type]strmatcher.Type{ type DomainMatcher struct{ geodata.DomainMatcher }
Domain_Plain: strmatcher.Substr,
Domain_Regex: strmatcher.Regex,
Domain_Domain: strmatcher.Domain,
Domain_Full: strmatcher.Full,
}
type DomainMatcher struct { func NewDomainMatcher(rules []*geodata.DomainRule) (*DomainMatcher, error) {
Matchers strmatcher.IndexMatcher m, err := geodata.DomainReg.BuildDomainMatcher(rules)
}
func SerializeDomainMatcher(domains []*Domain, w io.Writer) error {
g := strmatcher.NewMphMatcherGroup()
for _, d := range domains {
matcherType, f := matcherTypeMap[d.Type]
if !f {
continue
}
_, err := g.AddPattern(d.Value, matcherType)
if err != nil {
return err
}
}
g.Build()
// serialize
return g.Serialize(w)
}
func NewDomainMatcherFromBuffer(data []byte) (*strmatcher.MphMatcherGroup, error) {
matcher, err := strmatcher.NewMphMatcherGroupFromBuffer(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return matcher, nil return &DomainMatcher{DomainMatcher: m}, nil
}
func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) {
g := strmatcher.NewMphMatcherGroup()
for i, d := range domains {
domains[i] = nil
matcherType, f := matcherTypeMap[d.Type]
if !f {
errors.LogError(context.Background(), "ignore unsupported domain type ", d.Type, " of rule ", d.Value)
continue
}
_, err := g.AddPattern(d.Value, matcherType)
if err != nil {
errors.LogErrorInner(context.Background(), err, "ignore domain rule ", d.Type, " ", d.Value)
continue
}
}
g.Build()
return &DomainMatcher{
Matchers: g,
}, nil
} }
func (m *DomainMatcher) ApplyDomain(domain string) bool { func (m *DomainMatcher) ApplyDomain(domain string) bool {
return len(m.Matchers.Match(strings.ToLower(domain))) > 0 return m.DomainMatcher.MatchAny(strings.ToLower(domain))
} }
// Apply implements Condition. // Apply implements Condition.
@@ -114,7 +65,7 @@ func (m *DomainMatcher) Apply(ctx routing.Context) bool {
if len(domain) == 0 { if len(domain) == 0 {
return false return false
} }
return m.ApplyDomain(domain) return m.DomainMatcher.MatchAny(strings.ToLower(domain))
} }
type MatcherAsType byte type MatcherAsType byte
@@ -127,16 +78,16 @@ const (
) )
type IPMatcher struct { type IPMatcher struct {
matcher GeoIPMatcher matcher geodata.IPMatcher
asType MatcherAsType asType MatcherAsType
} }
func NewIPMatcher(geoips []*GeoIP, asType MatcherAsType) (*IPMatcher, error) { func NewIPMatcher(rules []*geodata.IPRule, asType MatcherAsType) (*IPMatcher, error) {
matcher, err := BuildOptimizedGeoIPMatcher(geoips...) m, err := geodata.IPReg.BuildIPMatcher(rules)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &IPMatcher{matcher: matcher, asType: asType}, nil return &IPMatcher{matcher: m, asType: asType}, nil
} }
// Apply implements Condition. // Apply implements Condition.
@@ -390,8 +341,10 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
if len(ctx.GetSourceIPs()) == 0 { if len(ctx.GetSourceIPs()) == 0 {
return false return false
} }
srcPort := ctx.GetSourcePort().String()
srcPort := uint16(ctx.GetSourcePort())
srcIP := ctx.GetSourceIPs()[0].String() srcIP := ctx.GetSourceIPs()[0].String()
var network string var network string
switch ctx.GetNetwork() { switch ctx.GetNetwork() {
case net.Network_TCP: case net.Network_TCP:
@@ -401,11 +354,21 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
default: default:
return false return false
} }
src, err := net.ParseDestination(strings.Join([]string{network, srcIP, srcPort}, ":"))
if err != nil { var dstIP string
return false var dstPort uint16 = 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())
} }
pid, name, absPath, err := net.FindProcess(src)
pid, name, absPath, err := net.FindProcess(network, srcIP, uint16(srcPort), dstIP, uint16(dstPort))
if err != nil { if err != nil {
if err != net.ErrNotLocal { if err != net.ErrNotLocal {
errors.LogError(context.Background(), "Unables to find local process name: ", err) errors.LogError(context.Background(), "Unables to find local process name: ", err)
-266
View File
@@ -1,266 +0,0 @@
package router_test
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
func getAssetPath(file string) (string, error) {
path := platform.GetAssetLocation(file)
_, err := os.Stat(path)
if os.IsNotExist(err) {
path := filepath.Join("..", "..", "resources", file)
_, err := os.Stat(path)
if os.IsNotExist(err) {
return "", fmt.Errorf("can't find %s in standard asset locations or {project_root}/resources", file)
}
if err != nil {
return "", fmt.Errorf("can't stat %s: %v", path, err)
}
return path, nil
}
if err != nil {
return "", fmt.Errorf("can't stat %s: %v", path, err)
}
return path, nil
}
func TestGeoIPMatcher(t *testing.T) {
cidrList := []*router.CIDR{
{Ip: []byte{0, 0, 0, 0}, Prefix: 8},
{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
{Ip: []byte{100, 64, 0, 0}, Prefix: 10},
{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
{Ip: []byte{169, 254, 0, 0}, Prefix: 16},
{Ip: []byte{172, 16, 0, 0}, Prefix: 12},
{Ip: []byte{192, 0, 0, 0}, Prefix: 24},
{Ip: []byte{192, 0, 2, 0}, Prefix: 24},
{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
{Ip: []byte{192, 18, 0, 0}, Prefix: 15},
{Ip: []byte{198, 51, 100, 0}, Prefix: 24},
{Ip: []byte{203, 0, 113, 0}, Prefix: 24},
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{91, 108, 4, 0}, Prefix: 16},
}
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: cidrList,
})
common.Must(err)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "192.0.0.0",
Output: true,
},
{
Input: "192.0.1.0",
Output: false,
},
{
Input: "0.1.0.0",
Output: true,
},
{
Input: "1.0.0.1",
Output: false,
},
{
Input: "8.8.8.7",
Output: false,
},
{
Input: "8.8.8.8",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
Output: true,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPMatcherRegression(t *testing.T) {
cidrList := []*router.CIDR{
{Ip: []byte{98, 108, 20, 0}, Prefix: 22},
{Ip: []byte{98, 108, 20, 0}, Prefix: 23},
}
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: cidrList,
})
common.Must(err)
testCases := []struct {
Input string
Output bool
}{
{
Input: "98.108.22.11",
Output: true,
},
{
Input: "98.108.25.0",
Output: false,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPReverseMatcher(t *testing.T) {
cidrList := []*router.CIDR{
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{91, 108, 4, 0}, Prefix: 16},
}
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: cidrList,
})
common.Must(err)
matcher.SetReverse(true) // Reverse match
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "91.108.255.254",
Output: false,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPMatcher4CN(t *testing.T) {
ips, err := loadGeoIP("CN")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
if matcher.Match([]byte{8, 8, 8, 8}) {
t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does")
}
}
func TestGeoIPMatcher6US(t *testing.T) {
ips, err := loadGeoIP("US")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
}
}
func loadGeoIP(country string) ([]*router.CIDR, error) {
path, err := getAssetPath("geoip.dat")
if err != nil {
return nil, err
}
geoipBytes, err := filesystem.ReadFile(path)
if err != nil {
return nil, err
}
var geoipList router.GeoIPList
if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
return nil, err
}
for _, geoip := range geoipList.Entry {
if geoip.CountryCode == country {
return geoip.Cidr, nil
}
}
panic("country not found: " + country)
}
func BenchmarkGeoIPMatcher4CN(b *testing.B) {
ips, err := loadGeoIP("CN")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match([]byte{8, 8, 8, 8})
}
}
func BenchmarkGeoIPMatcher6US(b *testing.B) {
ips, err := loadGeoIP("US")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP())
}
}
-167
View File
@@ -1,167 +0,0 @@
package router_test
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common/platform/filesystem"
)
func TestDomainMatcherSerialization(t *testing.T) {
domains := []*router.Domain{
{Type: router.Domain_Domain, Value: "google.com"},
{Type: router.Domain_Domain, Value: "v2ray.com"},
{Type: router.Domain_Full, Value: "full.example.com"},
}
var buf bytes.Buffer
if err := router.SerializeDomainMatcher(domains, &buf); err != nil {
t.Fatalf("Serialize failed: %v", err)
}
matcher, err := router.NewDomainMatcherFromBuffer(buf.Bytes())
if err != nil {
t.Fatalf("Deserialize failed: %v", err)
}
dMatcher := &router.DomainMatcher{
Matchers: matcher,
}
testCases := []struct {
Input string
Match bool
}{
{"google.com", true},
{"maps.google.com", true},
{"v2ray.com", true},
{"full.example.com", true},
{"example.com", false},
}
for _, tc := range testCases {
if res := dMatcher.ApplyDomain(tc.Input); res != tc.Match {
t.Errorf("Match(%s) = %v, want %v", tc.Input, res, tc.Match)
}
}
}
func TestGeoSiteSerialization(t *testing.T) {
sites := []*router.GeoSite{
{
CountryCode: "CN",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "baidu.cn"},
{Type: router.Domain_Domain, Value: "qq.com"},
},
},
{
CountryCode: "US",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "google.com"},
{Type: router.Domain_Domain, Value: "facebook.com"},
},
},
}
var buf bytes.Buffer
if err := router.SerializeGeoSiteList(sites, nil, nil, &buf); err != nil {
t.Fatalf("SerializeGeoSiteList failed: %v", err)
}
tmp := t.TempDir()
path := filepath.Join(tmp, "matcher.cache")
f, err := os.Create(path)
require.NoError(t, err)
_, err = f.Write(buf.Bytes())
require.NoError(t, err)
f.Close()
f, err = os.Open(path)
require.NoError(t, err)
defer f.Close()
require.NoError(t, err)
data, _ := filesystem.ReadFile(path)
// cn
gp, err := router.LoadGeoSiteMatcher(bytes.NewReader(data), "CN")
if err != nil {
t.Fatalf("LoadGeoSiteMatcher(CN) failed: %v", err)
}
cnMatcher := &router.DomainMatcher{
Matchers: gp,
}
if !cnMatcher.ApplyDomain("baidu.cn") {
t.Error("CN matcher should match baidu.cn")
}
if cnMatcher.ApplyDomain("google.com") {
t.Error("CN matcher should NOT match google.com")
}
// us
gp, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "US")
if err != nil {
t.Fatalf("LoadGeoSiteMatcher(US) failed: %v", err)
}
usMatcher := &router.DomainMatcher{
Matchers: gp,
}
if !usMatcher.ApplyDomain("google.com") {
t.Error("US matcher should match google.com")
}
if usMatcher.ApplyDomain("baidu.cn") {
t.Error("US matcher should NOT match baidu.cn")
}
// unknown
_, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "unknown")
if err == nil {
t.Error("LoadGeoSiteMatcher(unknown) should fail")
}
}
func TestGeoSiteSerializationWithDeps(t *testing.T) {
sites := []*router.GeoSite{
{
CountryCode: "geosite:cn",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "baidu.cn"},
},
},
{
CountryCode: "geosite:google@cn",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "google.cn"},
},
},
{
CountryCode: "rule-1",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "google.com"},
},
},
}
deps := map[string][]string{
"rule-1": {"geosite:cn", "geosite:google@cn"},
}
var buf bytes.Buffer
err := router.SerializeGeoSiteList(sites, deps, nil, &buf)
require.NoError(t, err)
matcher, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "rule-1")
require.NoError(t, err)
require.True(t, matcher.Match("google.com") != nil)
require.True(t, matcher.Match("baidu.cn") != nil)
require.True(t, matcher.Match("google.cn") != nil)
}
+38 -94
View File
@@ -1,20 +1,19 @@
package router_test package router_test
import ( import (
"path/filepath"
"strconv" "strconv"
"testing" "testing"
. "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/protocol/http" "github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session" routing_session "github.com/xtls/xray-core/features/routing/session"
"google.golang.org/protobuf/proto"
) )
func withBackground() routing.Context { func withBackground() routing.Context {
@@ -45,18 +44,15 @@ func TestRoutingRule(t *testing.T) {
}{ }{
{ {
rule: &RoutingRule{ rule: &RoutingRule{
Domain: []*Domain{ Domain: []*geodata.DomainRule{
{ {
Value: "example.com", Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Substr, Value: "example.com"}},
Type: Domain_Plain,
}, },
{ {
Value: "google.com", Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "google.com"}},
Type: Domain_Domain,
}, },
{ {
Value: "^facebook\\.com$", Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^facebook\\.com$"}},
Type: Domain_Regex,
}, },
}, },
}, },
@@ -93,20 +89,25 @@ func TestRoutingRule(t *testing.T) {
}, },
{ {
rule: &RoutingRule{ rule: &RoutingRule{
Geoip: []*GeoIP{ Ip: []*geodata.IPRule{
{ {
Cidr: []*CIDR{ Value: &geodata.IPRule_Custom{
{ Custom: &geodata.CIDRRule{
Ip: []byte{8, 8, 8, 8}, Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
Prefix: 32,
}, },
{ },
Ip: []byte{8, 8, 8, 8}, },
Prefix: 32, {
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
}, },
{ },
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), },
Prefix: 128, {
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
}, },
}, },
}, },
@@ -133,12 +134,11 @@ func TestRoutingRule(t *testing.T) {
}, },
{ {
rule: &RoutingRule{ rule: &RoutingRule{
SourceGeoip: []*GeoIP{ SourceIp: []*geodata.IPRule{
{ {
Cidr: []*CIDR{ Value: &geodata.IPRule_Custom{
{ Custom: &geodata.CIDRRule{
Ip: []byte{192, 168, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
Prefix: 16,
}, },
}, },
}, },
@@ -300,35 +300,12 @@ func TestRoutingRule(t *testing.T) {
} }
} }
func loadGeoSite(country string) ([]*Domain, error) {
path, err := getAssetPath("geosite.dat")
if err != nil {
return nil, err
}
geositeBytes, err := filesystem.ReadFile(path)
if err != nil {
return nil, err
}
var geositeList GeoSiteList
if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
return nil, err
}
for _, site := range geositeList.Entry {
if site.CountryCode == country {
return site.Domain, nil
}
}
return nil, errors.New("country not found: " + country)
}
func TestChinaSites(t *testing.T) { func TestChinaSites(t *testing.T) {
domains, err := loadGeoSite("CN") t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
common.Must(err) common.Must(err)
acMatcher, err := NewMphMatcherGroup(domains) matcher, err := NewDomainMatcher(rules)
common.Must(err) common.Must(err)
type TestCase struct { type TestCase struct {
@@ -359,18 +336,19 @@ func TestChinaSites(t *testing.T) {
} }
for _, testCase := range testCases { for _, testCase := range testCases {
r := acMatcher.ApplyDomain(testCase.Domain) r := matcher.ApplyDomain(testCase.Domain)
if r != testCase.Output { if r != testCase.Output {
t.Error("ACDomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r) t.Error("DomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r)
} }
} }
} }
func BenchmarkMphDomainMatcher(b *testing.B) { func BenchmarkMphDomainMatcher(b *testing.B) {
domains, err := loadGeoSite("CN") b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
common.Must(err) common.Must(err)
matcher, err := NewMphMatcherGroup(domains) matcher, err := NewDomainMatcher(rules)
common.Must(err) common.Must(err)
type TestCase struct { type TestCase struct {
@@ -409,45 +387,11 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
} }
func BenchmarkMultiGeoIPMatcher(b *testing.B) { func BenchmarkMultiGeoIPMatcher(b *testing.B) {
var geoips []*GeoIP b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
common.Must(err)
{ matcher, err := NewIPMatcher(rules, MatcherAsType_Target)
ips, err := loadGeoIP("CN")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "CN",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("JP")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "JP",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("CA")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "CA",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("US")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "US",
Cidr: ips,
})
}
matcher, err := NewIPMatcher(geoips, MatcherAsType_Target)
common.Must(err) common.Must(err)
ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}) ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)})
+10 -53
View File
@@ -3,12 +3,9 @@ package router
import ( import (
"context" "context"
"regexp" "regexp"
"runtime"
"strings" "strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
@@ -76,60 +73,37 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds.Add(&AttributeMatcher{configuredKeys}) conds.Add(&AttributeMatcher{configuredKeys})
} }
if len(rr.Geoip) > 0 { if len(rr.Ip) > 0 {
cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) cond, err := NewIPMatcher(rr.Ip, MatcherAsType_Target)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conds.Add(cond) conds.Add(cond)
rr.Geoip = nil
runtime.GC()
} }
if len(rr.SourceGeoip) > 0 { if len(rr.SourceIp) > 0 {
cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) cond, err := NewIPMatcher(rr.SourceIp, MatcherAsType_Source)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conds.Add(cond) conds.Add(cond)
rr.SourceGeoip = nil
runtime.GC()
} }
if len(rr.LocalGeoip) > 0 { if len(rr.LocalIp) > 0 {
cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) cond, err := NewIPMatcher(rr.LocalIp, MatcherAsType_Local)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conds.Add(cond) conds.Add(cond)
errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces")
rr.LocalGeoip = nil
runtime.GC()
} }
if len(rr.Domain) > 0 { if len(rr.Domain) > 0 {
var matcher *DomainMatcher cond, err := NewDomainMatcher(rr.Domain)
var err error if err != nil {
// Check if domain matcher cache is provided via environment return nil, err
domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" })
if domainMatcherPath != "" {
matcher, err = GetDomainMatcherWithRuleTag(domainMatcherPath, rr.RuleTag)
if err != nil {
return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err)
}
errors.LogDebug(context.Background(), "MphDomainMatcher loaded from cache for ", rr.RuleTag, " rule tag)")
} else {
matcher, err = NewMphMatcherGroup(rr.Domain)
if err != nil {
return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err)
}
errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)")
} }
conds.Add(matcher) conds.Add(cond)
rr.Domain = nil
runtime.GC()
} }
if len(rr.Process) > 0 { if len(rr.Process) > 0 {
@@ -189,20 +163,3 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
return nil, errors.New("unrecognized balancer type") return nil, errors.New("unrecognized balancer type")
} }
} }
func GetDomainMatcherWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) {
f, err := filesystem.NewFileReader(domainMatcherPath)
if err != nil {
return nil, errors.New("failed to load file: ", domainMatcherPath).Base(err)
}
defer f.Close()
g, err := LoadGeoSiteMatcher(f, ruleTag)
if err != nil {
return nil, errors.New("failed to load file:", domainMatcherPath).Base(err)
}
return &DomainMatcher{
Matchers: g,
}, nil
}
+95 -609
View File
@@ -7,6 +7,7 @@
package router package router
import ( import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net" net "github.com/xtls/xray-core/common/net"
serial "github.com/xtls/xray-core/common/serial" serial "github.com/xtls/xray-core/common/serial"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
@@ -23,63 +24,6 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
// Type of domain value.
type Domain_Type int32
const (
// The value is used as is.
Domain_Plain Domain_Type = 0
// The value is used as a regular expression.
Domain_Regex Domain_Type = 1
// The value is a root domain.
Domain_Domain Domain_Type = 2
// The value is a domain.
Domain_Full Domain_Type = 3
)
// Enum value maps for Domain_Type.
var (
Domain_Type_name = map[int32]string{
0: "Plain",
1: "Regex",
2: "Domain",
3: "Full",
}
Domain_Type_value = map[string]int32{
"Plain": 0,
"Regex": 1,
"Domain": 2,
"Full": 3,
}
)
func (x Domain_Type) Enum() *Domain_Type {
p := new(Domain_Type)
*p = x
return p
}
func (x Domain_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
return file_app_router_config_proto_enumTypes[0].Descriptor()
}
func (Domain_Type) Type() protoreflect.EnumType {
return &file_app_router_config_proto_enumTypes[0]
}
func (x Domain_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Domain_Type.Descriptor instead.
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{0, 0}
}
type Config_DomainStrategy int32 type Config_DomainStrategy int32
const ( const (
@@ -116,11 +60,11 @@ func (x Config_DomainStrategy) String() string {
} }
func (Config_DomainStrategy) Descriptor() protoreflect.EnumDescriptor { func (Config_DomainStrategy) Descriptor() protoreflect.EnumDescriptor {
return file_app_router_config_proto_enumTypes[1].Descriptor() return file_app_router_config_proto_enumTypes[0].Descriptor()
} }
func (Config_DomainStrategy) Type() protoreflect.EnumType { func (Config_DomainStrategy) Type() protoreflect.EnumType {
return &file_app_router_config_proto_enumTypes[1] return &file_app_router_config_proto_enumTypes[0]
} }
func (x Config_DomainStrategy) Number() protoreflect.EnumNumber { func (x Config_DomainStrategy) Number() protoreflect.EnumNumber {
@@ -129,326 +73,7 @@ func (x Config_DomainStrategy) Number() protoreflect.EnumNumber {
// Deprecated: Use Config_DomainStrategy.Descriptor instead. // Deprecated: Use Config_DomainStrategy.Descriptor instead.
func (Config_DomainStrategy) EnumDescriptor() ([]byte, []int) { func (Config_DomainStrategy) EnumDescriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{11, 0} return file_app_router_config_proto_rawDescGZIP(), []int{5, 0}
}
// Domain for routing decision.
type Domain struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Domain matching type.
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.router.Domain_Type" json:"type,omitempty"`
// Domain value.
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
// Attributes of this domain. May be used for filtering.
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Domain) Reset() {
*x = Domain{}
mi := &file_app_router_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_app_router_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 Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{0}
}
func (x *Domain) GetType() Domain_Type {
if x != nil {
return x.Type
}
return Domain_Plain
}
func (x *Domain) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Domain) GetAttribute() []*Domain_Attribute {
if x != nil {
return x.Attribute
}
return nil
}
// IP for routing decision, in CIDR form.
type CIDR struct {
state protoimpl.MessageState `protogen:"open.v1"`
// IP address, should be either 4 or 16 bytes.
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
// Number of leading ones in the network mask.
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CIDR) Reset() {
*x = CIDR{}
mi := &file_app_router_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CIDR) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDR) ProtoMessage() {}
func (x *CIDR) ProtoReflect() protoreflect.Message {
mi := &file_app_router_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 CIDR.ProtoReflect.Descriptor instead.
func (*CIDR) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{1}
}
func (x *CIDR) GetIp() []byte {
if x != nil {
return x.Ip
}
return nil
}
func (x *CIDR) GetPrefix() uint32 {
if x != nil {
return x.Prefix
}
return 0
}
type GeoIP struct {
state protoimpl.MessageState `protogen:"open.v1"`
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
mi := &file_app_router_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[2]
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 GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{2}
}
func (x *GeoIP) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *GeoIP) GetCidr() []*CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *GeoIP) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type GeoIPList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
mi := &file_app_router_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIPList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{3}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
if x != nil {
return x.Entry
}
return nil
}
type GeoSite struct {
state protoimpl.MessageState `protogen:"open.v1"`
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSite) Reset() {
*x = GeoSite{}
mi := &file_app_router_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSite) ProtoMessage() {}
func (x *GeoSite) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead.
func (*GeoSite) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{4}
}
func (x *GeoSite) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *GeoSite) GetDomain() []*Domain {
if x != nil {
return x.Domain
}
return nil
}
type GeoSiteList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSiteList) Reset() {
*x = GeoSiteList{}
mi := &file_app_router_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSiteList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteList) ProtoMessage() {}
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[5]
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 GeoSiteList.ProtoReflect.Descriptor instead.
func (*GeoSiteList) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{5}
}
func (x *GeoSiteList) GetEntry() []*GeoSite {
if x != nil {
return x.Entry
}
return nil
} }
type RoutingRule struct { type RoutingRule struct {
@@ -460,37 +85,35 @@ type RoutingRule struct {
TargetTag isRoutingRule_TargetTag `protobuf_oneof:"target_tag"` TargetTag isRoutingRule_TargetTag `protobuf_oneof:"target_tag"`
RuleTag string `protobuf:"bytes,19,opt,name=rule_tag,json=ruleTag,proto3" json:"rule_tag,omitempty"` RuleTag string `protobuf:"bytes,19,opt,name=rule_tag,json=ruleTag,proto3" json:"rule_tag,omitempty"`
// List of domains for target domain matching. // List of domains for target domain matching.
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` Domain []*geodata.DomainRule `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
// List of GeoIPs for target IP address matching. If this entry exists, the // List of IPs for target IP address matching.
// cidr above will have no effect. GeoIP fields with the same country code are Ip []*geodata.IPRule `protobuf:"bytes,10,rep,name=ip,proto3" json:"ip,omitempty"`
// supposed to contain exactly same content. They will be merged during // List of ports for target port matching.
// runtime. For customized GeoIPs, please leave country code empty.
Geoip []*GeoIP `protobuf:"bytes,10,rep,name=geoip,proto3" json:"geoip,omitempty"`
// List of ports.
PortList *net.PortList `protobuf:"bytes,14,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"` PortList *net.PortList `protobuf:"bytes,14,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"`
// List of networks for matching. // List of networks for matching.
Networks []net.Network `protobuf:"varint,13,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` Networks []net.Network `protobuf:"varint,13,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"`
// List of GeoIPs for source IP address matching. If this entry exists, the // List of IPs for source IP address matching.
// source_cidr above will have no effect. SourceIp []*geodata.IPRule `protobuf:"bytes,11,rep,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"`
SourceGeoip []*GeoIP `protobuf:"bytes,11,rep,name=source_geoip,json=sourceGeoip,proto3" json:"source_geoip,omitempty"`
// List of ports for source port matching. // List of ports for source port matching.
SourcePortList *net.PortList `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"` SourcePortList *net.PortList `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"`
UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"`
InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"` InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"`
Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"` Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"`
Attributes map[string]string `protobuf:"bytes,15,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` Attributes map[string]string `protobuf:"bytes,15,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
LocalGeoip []*GeoIP `protobuf:"bytes,17,rep,name=local_geoip,json=localGeoip,proto3" json:"local_geoip,omitempty"` // List of IPs for local IP address matching.
LocalPortList *net.PortList `protobuf:"bytes,18,opt,name=local_port_list,json=localPortList,proto3" json:"local_port_list,omitempty"` LocalIp []*geodata.IPRule `protobuf:"bytes,17,rep,name=local_ip,json=localIp,proto3" json:"local_ip,omitempty"`
VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"` // List of ports for local port matching.
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` LocalPortList *net.PortList `protobuf:"bytes,18,opt,name=local_port_list,json=localPortList,proto3" json:"local_port_list,omitempty"`
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"`
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *RoutingRule) Reset() { func (x *RoutingRule) Reset() {
*x = RoutingRule{} *x = RoutingRule{}
mi := &file_app_router_config_proto_msgTypes[6] mi := &file_app_router_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -502,7 +125,7 @@ func (x *RoutingRule) String() string {
func (*RoutingRule) ProtoMessage() {} func (*RoutingRule) ProtoMessage() {}
func (x *RoutingRule) ProtoReflect() protoreflect.Message { func (x *RoutingRule) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[6] mi := &file_app_router_config_proto_msgTypes[0]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -515,7 +138,7 @@ func (x *RoutingRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead. // Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead.
func (*RoutingRule) Descriptor() ([]byte, []int) { func (*RoutingRule) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{6} return file_app_router_config_proto_rawDescGZIP(), []int{0}
} }
func (x *RoutingRule) GetTargetTag() isRoutingRule_TargetTag { func (x *RoutingRule) GetTargetTag() isRoutingRule_TargetTag {
@@ -550,16 +173,16 @@ func (x *RoutingRule) GetRuleTag() string {
return "" return ""
} }
func (x *RoutingRule) GetDomain() []*Domain { func (x *RoutingRule) GetDomain() []*geodata.DomainRule {
if x != nil { if x != nil {
return x.Domain return x.Domain
} }
return nil return nil
} }
func (x *RoutingRule) GetGeoip() []*GeoIP { func (x *RoutingRule) GetIp() []*geodata.IPRule {
if x != nil { if x != nil {
return x.Geoip return x.Ip
} }
return nil return nil
} }
@@ -578,9 +201,9 @@ func (x *RoutingRule) GetNetworks() []net.Network {
return nil return nil
} }
func (x *RoutingRule) GetSourceGeoip() []*GeoIP { func (x *RoutingRule) GetSourceIp() []*geodata.IPRule {
if x != nil { if x != nil {
return x.SourceGeoip return x.SourceIp
} }
return nil return nil
} }
@@ -620,9 +243,9 @@ func (x *RoutingRule) GetAttributes() map[string]string {
return nil return nil
} }
func (x *RoutingRule) GetLocalGeoip() []*GeoIP { func (x *RoutingRule) GetLocalIp() []*geodata.IPRule {
if x != nil { if x != nil {
return x.LocalGeoip return x.LocalIp
} }
return nil return nil
} }
@@ -684,7 +307,7 @@ type WebhookConfig struct {
func (x *WebhookConfig) Reset() { func (x *WebhookConfig) Reset() {
*x = WebhookConfig{} *x = WebhookConfig{}
mi := &file_app_router_config_proto_msgTypes[7] mi := &file_app_router_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -696,7 +319,7 @@ func (x *WebhookConfig) String() string {
func (*WebhookConfig) ProtoMessage() {} func (*WebhookConfig) ProtoMessage() {}
func (x *WebhookConfig) ProtoReflect() protoreflect.Message { func (x *WebhookConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[7] mi := &file_app_router_config_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -709,7 +332,7 @@ func (x *WebhookConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use WebhookConfig.ProtoReflect.Descriptor instead. // Deprecated: Use WebhookConfig.ProtoReflect.Descriptor instead.
func (*WebhookConfig) Descriptor() ([]byte, []int) { func (*WebhookConfig) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{7} return file_app_router_config_proto_rawDescGZIP(), []int{1}
} }
func (x *WebhookConfig) GetUrl() string { func (x *WebhookConfig) GetUrl() string {
@@ -746,7 +369,7 @@ type BalancingRule struct {
func (x *BalancingRule) Reset() { func (x *BalancingRule) Reset() {
*x = BalancingRule{} *x = BalancingRule{}
mi := &file_app_router_config_proto_msgTypes[8] mi := &file_app_router_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -758,7 +381,7 @@ func (x *BalancingRule) String() string {
func (*BalancingRule) ProtoMessage() {} func (*BalancingRule) ProtoMessage() {}
func (x *BalancingRule) ProtoReflect() protoreflect.Message { func (x *BalancingRule) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[8] mi := &file_app_router_config_proto_msgTypes[2]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -771,7 +394,7 @@ func (x *BalancingRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use BalancingRule.ProtoReflect.Descriptor instead. // Deprecated: Use BalancingRule.ProtoReflect.Descriptor instead.
func (*BalancingRule) Descriptor() ([]byte, []int) { func (*BalancingRule) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{8} return file_app_router_config_proto_rawDescGZIP(), []int{2}
} }
func (x *BalancingRule) GetTag() string { func (x *BalancingRule) GetTag() string {
@@ -820,7 +443,7 @@ type StrategyWeight struct {
func (x *StrategyWeight) Reset() { func (x *StrategyWeight) Reset() {
*x = StrategyWeight{} *x = StrategyWeight{}
mi := &file_app_router_config_proto_msgTypes[9] mi := &file_app_router_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -832,7 +455,7 @@ func (x *StrategyWeight) String() string {
func (*StrategyWeight) ProtoMessage() {} func (*StrategyWeight) ProtoMessage() {}
func (x *StrategyWeight) ProtoReflect() protoreflect.Message { func (x *StrategyWeight) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[9] mi := &file_app_router_config_proto_msgTypes[3]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -845,7 +468,7 @@ func (x *StrategyWeight) ProtoReflect() protoreflect.Message {
// Deprecated: Use StrategyWeight.ProtoReflect.Descriptor instead. // Deprecated: Use StrategyWeight.ProtoReflect.Descriptor instead.
func (*StrategyWeight) Descriptor() ([]byte, []int) { func (*StrategyWeight) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{9} return file_app_router_config_proto_rawDescGZIP(), []int{3}
} }
func (x *StrategyWeight) GetRegexp() bool { func (x *StrategyWeight) GetRegexp() bool {
@@ -887,7 +510,7 @@ type StrategyLeastLoadConfig struct {
func (x *StrategyLeastLoadConfig) Reset() { func (x *StrategyLeastLoadConfig) Reset() {
*x = StrategyLeastLoadConfig{} *x = StrategyLeastLoadConfig{}
mi := &file_app_router_config_proto_msgTypes[10] mi := &file_app_router_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -899,7 +522,7 @@ func (x *StrategyLeastLoadConfig) String() string {
func (*StrategyLeastLoadConfig) ProtoMessage() {} func (*StrategyLeastLoadConfig) ProtoMessage() {}
func (x *StrategyLeastLoadConfig) ProtoReflect() protoreflect.Message { func (x *StrategyLeastLoadConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[10] mi := &file_app_router_config_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -912,7 +535,7 @@ func (x *StrategyLeastLoadConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use StrategyLeastLoadConfig.ProtoReflect.Descriptor instead. // Deprecated: Use StrategyLeastLoadConfig.ProtoReflect.Descriptor instead.
func (*StrategyLeastLoadConfig) Descriptor() ([]byte, []int) { func (*StrategyLeastLoadConfig) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{10} return file_app_router_config_proto_rawDescGZIP(), []int{4}
} }
func (x *StrategyLeastLoadConfig) GetCosts() []*StrategyWeight { func (x *StrategyLeastLoadConfig) GetCosts() []*StrategyWeight {
@@ -961,7 +584,7 @@ type Config struct {
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_app_router_config_proto_msgTypes[11] mi := &file_app_router_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -973,7 +596,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[11] mi := &file_app_router_config_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -986,7 +609,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{11} return file_app_router_config_proto_rawDescGZIP(), []int{5}
} }
func (x *Config) GetDomainStrategy() Config_DomainStrategy { func (x *Config) GetDomainStrategy() Config_DomainStrategy {
@@ -1010,141 +633,21 @@ func (x *Config) GetBalancingRule() []*BalancingRule {
return nil return nil
} }
type Domain_Attribute struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Types that are valid to be assigned to TypedValue:
//
// *Domain_Attribute_BoolValue
// *Domain_Attribute_IntValue
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
mi := &file_app_router_config_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Domain_Attribute) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_app_router_config_proto_msgTypes[12]
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 Domain_Attribute.ProtoReflect.Descriptor instead.
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
return file_app_router_config_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Domain_Attribute) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
if x != nil {
return x.TypedValue
}
return nil
}
func (x *Domain_Attribute) GetBoolValue() bool {
if x != nil {
if x, ok := x.TypedValue.(*Domain_Attribute_BoolValue); ok {
return x.BoolValue
}
}
return false
}
func (x *Domain_Attribute) GetIntValue() int64 {
if x != nil {
if x, ok := x.TypedValue.(*Domain_Attribute_IntValue); ok {
return x.IntValue
}
}
return 0
}
type isDomain_Attribute_TypedValue interface {
isDomain_Attribute_TypedValue()
}
type Domain_Attribute_BoolValue struct {
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
}
type Domain_Attribute_IntValue struct {
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
}
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
var File_app_router_config_proto protoreflect.FileDescriptor var File_app_router_config_proto protoreflect.FileDescriptor
const file_app_router_config_proto_rawDesc = "" + const file_app_router_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\"\xb3\x02\n" + "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" +
"\x06Domain\x120\n" +
"\x04type\x18\x01 \x01(\x0e2\x1c.xray.app.router.Domain.TypeR\x04type\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value\x12?\n" +
"\tattribute\x18\x03 \x03(\v2!.xray.app.router.Domain.AttributeR\tattribute\x1al\n" +
"\tAttribute\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x1f\n" +
"\n" +
"bool_value\x18\x02 \x01(\bH\x00R\tboolValue\x12\x1d\n" +
"\tint_value\x18\x03 \x01(\x03H\x00R\bintValueB\r\n" +
"\vtyped_value\"2\n" +
"\x04Type\x12\t\n" +
"\x05Plain\x10\x00\x12\t\n" +
"\x05Regex\x10\x01\x12\n" +
"\n" +
"\x06Domain\x10\x02\x12\b\n" +
"\x04Full\x10\x03\".\n" +
"\x04CIDR\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"z\n" +
"\x05GeoIP\x12!\n" +
"\fcountry_code\x18\x01 \x01(\tR\vcountryCode\x12)\n" +
"\x04cidr\x18\x02 \x03(\v2\x15.xray.app.router.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"9\n" +
"\tGeoIPList\x12,\n" +
"\x05entry\x18\x01 \x03(\v2\x16.xray.app.router.GeoIPR\x05entry\"]\n" +
"\aGeoSite\x12!\n" +
"\fcountry_code\x18\x01 \x01(\tR\vcountryCode\x12/\n" +
"\x06domain\x18\x02 \x03(\v2\x17.xray.app.router.DomainR\x06domain\"=\n" +
"\vGeoSiteList\x12.\n" +
"\x05entry\x18\x01 \x03(\v2\x18.xray.app.router.GeoSiteR\x05entry\"\xbc\a\n" +
"\vRoutingRule\x12\x12\n" + "\vRoutingRule\x12\x12\n" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" + "\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" + "\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" +
"\brule_tag\x18\x13 \x01(\tR\aruleTag\x12/\n" + "\brule_tag\x18\x13 \x01(\tR\aruleTag\x127\n" +
"\x06domain\x18\x02 \x03(\v2\x17.xray.app.router.DomainR\x06domain\x12,\n" + "\x06domain\x18\x02 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12+\n" +
"\x05geoip\x18\n" + "\x02ip\x18\n" +
" \x03(\v2\x16.xray.app.router.GeoIPR\x05geoip\x126\n" + " \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\x126\n" +
"\tport_list\x18\x0e \x01(\v2\x19.xray.common.net.PortListR\bportList\x124\n" + "\tport_list\x18\x0e \x01(\v2\x19.xray.common.net.PortListR\bportList\x124\n" +
"\bnetworks\x18\r \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x129\n" + "\bnetworks\x18\r \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x128\n" +
"\fsource_geoip\x18\v \x03(\v2\x16.xray.app.router.GeoIPR\vsourceGeoip\x12C\n" + "\tsource_ip\x18\v \x03(\v2\x1b.xray.common.geodata.IPRuleR\bsourceIp\x12C\n" +
"\x10source_port_list\x18\x10 \x01(\v2\x19.xray.common.net.PortListR\x0esourcePortList\x12\x1d\n" + "\x10source_port_list\x18\x10 \x01(\v2\x19.xray.common.net.PortListR\x0esourcePortList\x12\x1d\n" +
"\n" + "\n" +
"user_email\x18\a \x03(\tR\tuserEmail\x12\x1f\n" + "user_email\x18\a \x03(\tR\tuserEmail\x12\x1f\n" +
@@ -1153,9 +656,8 @@ const file_app_router_config_proto_rawDesc = "" +
"\bprotocol\x18\t \x03(\tR\bprotocol\x12L\n" + "\bprotocol\x18\t \x03(\tR\bprotocol\x12L\n" +
"\n" + "\n" +
"attributes\x18\x0f \x03(\v2,.xray.app.router.RoutingRule.AttributesEntryR\n" + "attributes\x18\x0f \x03(\v2,.xray.app.router.RoutingRule.AttributesEntryR\n" +
"attributes\x127\n" + "attributes\x126\n" +
"\vlocal_geoip\x18\x11 \x03(\v2\x16.xray.app.router.GeoIPR\n" + "\blocal_ip\x18\x11 \x03(\v2\x1b.xray.common.geodata.IPRuleR\alocalIp\x12A\n" +
"localGeoip\x12A\n" +
"\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" + "\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" +
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" + "\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" +
"\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" + "\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" +
@@ -1187,16 +689,16 @@ const file_app_router_config_proto_rawDesc = "" +
"\tbaselines\x18\x03 \x03(\x03R\tbaselines\x12\x1a\n" + "\tbaselines\x18\x03 \x03(\x03R\tbaselines\x12\x1a\n" +
"\bexpected\x18\x04 \x01(\x05R\bexpected\x12\x16\n" + "\bexpected\x18\x04 \x01(\x05R\bexpected\x12\x16\n" +
"\x06maxRTT\x18\x05 \x01(\x03R\x06maxRTT\x12\x1c\n" + "\x06maxRTT\x18\x05 \x01(\x03R\x06maxRTT\x12\x1c\n" +
"\ttolerance\x18\x06 \x01(\x02R\ttolerance\"\x90\x02\n" + "\ttolerance\x18\x06 \x01(\x02R\ttolerance\"\x96\x02\n" +
"\x06Config\x12O\n" + "\x06Config\x12O\n" +
"\x0fdomain_strategy\x18\x01 \x01(\x0e2&.xray.app.router.Config.DomainStrategyR\x0edomainStrategy\x120\n" + "\x0fdomain_strategy\x18\x01 \x01(\x0e2&.xray.app.router.Config.DomainStrategyR\x0edomainStrategy\x120\n" +
"\x04rule\x18\x02 \x03(\v2\x1c.xray.app.router.RoutingRuleR\x04rule\x12E\n" + "\x04rule\x18\x02 \x03(\v2\x1c.xray.app.router.RoutingRuleR\x04rule\x12E\n" +
"\x0ebalancing_rule\x18\x03 \x03(\v2\x1e.xray.app.router.BalancingRuleR\rbalancingRule\"<\n" + "\x0ebalancing_rule\x18\x03 \x03(\v2\x1e.xray.app.router.BalancingRuleR\rbalancingRule\"B\n" +
"\x0eDomainStrategy\x12\b\n" + "\x0eDomainStrategy\x12\b\n" +
"\x04AsIs\x10\x00\x12\x10\n" + "\x04AsIs\x10\x00\x12\x10\n" +
"\fIpIfNonMatch\x10\x02\x12\x0e\n" + "\fIpIfNonMatch\x10\x02\x12\x0e\n" +
"\n" + "\n" +
"IpOnDemand\x10\x03BO\n" + "IpOnDemand\x10\x03\"\x04\b\x01\x10\x01BO\n" +
"\x13com.xray.app.routerP\x01Z$github.com/xtls/xray-core/app/router\xaa\x02\x0fXray.App.Routerb\x06proto3" "\x13com.xray.app.routerP\x01Z$github.com/xtls/xray-core/app/router\xaa\x02\x0fXray.App.Routerb\x06proto3"
var ( var (
@@ -1211,59 +713,47 @@ func file_app_router_config_proto_rawDescGZIP() []byte {
return file_app_router_config_proto_rawDescData return file_app_router_config_proto_rawDescData
} }
var file_app_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_app_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_app_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_app_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_app_router_config_proto_goTypes = []any{ var file_app_router_config_proto_goTypes = []any{
(Domain_Type)(0), // 0: xray.app.router.Domain.Type (Config_DomainStrategy)(0), // 0: xray.app.router.Config.DomainStrategy
(Config_DomainStrategy)(0), // 1: xray.app.router.Config.DomainStrategy (*RoutingRule)(nil), // 1: xray.app.router.RoutingRule
(*Domain)(nil), // 2: xray.app.router.Domain (*WebhookConfig)(nil), // 2: xray.app.router.WebhookConfig
(*CIDR)(nil), // 3: xray.app.router.CIDR (*BalancingRule)(nil), // 3: xray.app.router.BalancingRule
(*GeoIP)(nil), // 4: xray.app.router.GeoIP (*StrategyWeight)(nil), // 4: xray.app.router.StrategyWeight
(*GeoIPList)(nil), // 5: xray.app.router.GeoIPList (*StrategyLeastLoadConfig)(nil), // 5: xray.app.router.StrategyLeastLoadConfig
(*GeoSite)(nil), // 6: xray.app.router.GeoSite (*Config)(nil), // 6: xray.app.router.Config
(*GeoSiteList)(nil), // 7: xray.app.router.GeoSiteList nil, // 7: xray.app.router.RoutingRule.AttributesEntry
(*RoutingRule)(nil), // 8: xray.app.router.RoutingRule nil, // 8: xray.app.router.WebhookConfig.HeadersEntry
(*WebhookConfig)(nil), // 9: xray.app.router.WebhookConfig (*geodata.DomainRule)(nil), // 9: xray.common.geodata.DomainRule
(*BalancingRule)(nil), // 10: xray.app.router.BalancingRule (*geodata.IPRule)(nil), // 10: xray.common.geodata.IPRule
(*StrategyWeight)(nil), // 11: xray.app.router.StrategyWeight (*net.PortList)(nil), // 11: xray.common.net.PortList
(*StrategyLeastLoadConfig)(nil), // 12: xray.app.router.StrategyLeastLoadConfig (net.Network)(0), // 12: xray.common.net.Network
(*Config)(nil), // 13: xray.app.router.Config (*serial.TypedMessage)(nil), // 13: xray.common.serial.TypedMessage
(*Domain_Attribute)(nil), // 14: xray.app.router.Domain.Attribute
nil, // 15: xray.app.router.RoutingRule.AttributesEntry
nil, // 16: xray.app.router.WebhookConfig.HeadersEntry
(*net.PortList)(nil), // 17: xray.common.net.PortList
(net.Network)(0), // 18: xray.common.net.Network
(*serial.TypedMessage)(nil), // 19: xray.common.serial.TypedMessage
} }
var file_app_router_config_proto_depIdxs = []int32{ var file_app_router_config_proto_depIdxs = []int32{
0, // 0: xray.app.router.Domain.type:type_name -> xray.app.router.Domain.Type 9, // 0: xray.app.router.RoutingRule.domain:type_name -> xray.common.geodata.DomainRule
14, // 1: xray.app.router.Domain.attribute:type_name -> xray.app.router.Domain.Attribute 10, // 1: xray.app.router.RoutingRule.ip:type_name -> xray.common.geodata.IPRule
3, // 2: xray.app.router.GeoIP.cidr:type_name -> xray.app.router.CIDR 11, // 2: xray.app.router.RoutingRule.port_list:type_name -> xray.common.net.PortList
4, // 3: xray.app.router.GeoIPList.entry:type_name -> xray.app.router.GeoIP 12, // 3: xray.app.router.RoutingRule.networks:type_name -> xray.common.net.Network
2, // 4: xray.app.router.GeoSite.domain:type_name -> xray.app.router.Domain 10, // 4: xray.app.router.RoutingRule.source_ip:type_name -> xray.common.geodata.IPRule
6, // 5: xray.app.router.GeoSiteList.entry:type_name -> xray.app.router.GeoSite 11, // 5: xray.app.router.RoutingRule.source_port_list:type_name -> xray.common.net.PortList
2, // 6: xray.app.router.RoutingRule.domain:type_name -> xray.app.router.Domain 7, // 6: xray.app.router.RoutingRule.attributes:type_name -> xray.app.router.RoutingRule.AttributesEntry
4, // 7: xray.app.router.RoutingRule.geoip:type_name -> xray.app.router.GeoIP 10, // 7: xray.app.router.RoutingRule.local_ip:type_name -> xray.common.geodata.IPRule
17, // 8: xray.app.router.RoutingRule.port_list:type_name -> xray.common.net.PortList 11, // 8: xray.app.router.RoutingRule.local_port_list:type_name -> xray.common.net.PortList
18, // 9: xray.app.router.RoutingRule.networks:type_name -> xray.common.net.Network 11, // 9: xray.app.router.RoutingRule.vless_route_list:type_name -> xray.common.net.PortList
4, // 10: xray.app.router.RoutingRule.source_geoip:type_name -> xray.app.router.GeoIP 2, // 10: xray.app.router.RoutingRule.webhook:type_name -> xray.app.router.WebhookConfig
17, // 11: xray.app.router.RoutingRule.source_port_list:type_name -> xray.common.net.PortList 8, // 11: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry
15, // 12: xray.app.router.RoutingRule.attributes:type_name -> xray.app.router.RoutingRule.AttributesEntry 13, // 12: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage
4, // 13: xray.app.router.RoutingRule.local_geoip:type_name -> xray.app.router.GeoIP 4, // 13: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight
17, // 14: xray.app.router.RoutingRule.local_port_list:type_name -> xray.common.net.PortList 0, // 14: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy
17, // 15: xray.app.router.RoutingRule.vless_route_list:type_name -> xray.common.net.PortList 1, // 15: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule
9, // 16: xray.app.router.RoutingRule.webhook:type_name -> xray.app.router.WebhookConfig 3, // 16: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule
16, // 17: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry 17, // [17:17] is the sub-list for method output_type
19, // 18: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage 17, // [17:17] is the sub-list for method input_type
11, // 19: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight 17, // [17:17] is the sub-list for extension type_name
1, // 20: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy 17, // [17:17] is the sub-list for extension extendee
8, // 21: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule 0, // [0:17] is the sub-list for field type_name
10, // 22: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule
23, // [23:23] is the sub-list for method output_type
23, // [23:23] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
} }
func init() { file_app_router_config_proto_init() } func init() { file_app_router_config_proto_init() }
@@ -1271,21 +761,17 @@ func file_app_router_config_proto_init() {
if File_app_router_config_proto != nil { if File_app_router_config_proto != nil {
return return
} }
file_app_router_config_proto_msgTypes[6].OneofWrappers = []any{ file_app_router_config_proto_msgTypes[0].OneofWrappers = []any{
(*RoutingRule_Tag)(nil), (*RoutingRule_Tag)(nil),
(*RoutingRule_BalancingTag)(nil), (*RoutingRule_BalancingTag)(nil),
} }
file_app_router_config_proto_msgTypes[12].OneofWrappers = []any{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_router_config_proto_rawDesc), len(file_app_router_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_router_config_proto_rawDesc), len(file_app_router_config_proto_rawDesc)),
NumEnums: 2, NumEnums: 1,
NumMessages: 15, NumMessages: 8,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+15 -75
View File
@@ -9,67 +9,7 @@ option java_multiple_files = true;
import "common/serial/typed_message.proto"; import "common/serial/typed_message.proto";
import "common/net/port.proto"; import "common/net/port.proto";
import "common/net/network.proto"; import "common/net/network.proto";
import "common/geodata/geodat.proto";
// Domain for routing decision.
message Domain {
// Type of domain value.
enum Type {
// The value is used as is.
Plain = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a root domain.
Domain = 2;
// The value is a domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
// IP for routing decision, in CIDR form.
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
bool reverse_match = 3;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoSite {
string country_code = 1;
repeated Domain domain = 2;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
message RoutingRule { message RoutingRule {
oneof target_tag { oneof target_tag {
@@ -79,26 +19,23 @@ message RoutingRule {
// Tag of routing balancer. // Tag of routing balancer.
string balancing_tag = 12; string balancing_tag = 12;
} }
string rule_tag = 19;
string rule_tag = 19;
// List of domains for target domain matching. // List of domains for target domain matching.
repeated Domain domain = 2; repeated xray.common.geodata.DomainRule domain = 2;
// List of GeoIPs for target IP address matching. If this entry exists, the // List of IPs for target IP address matching.
// cidr above will have no effect. GeoIP fields with the same country code are repeated xray.common.geodata.IPRule ip = 10;
// supposed to contain exactly same content. They will be merged during
// runtime. For customized GeoIPs, please leave country code empty.
repeated GeoIP geoip = 10;
// List of ports. // List of ports for target port matching.
xray.common.net.PortList port_list = 14; xray.common.net.PortList port_list = 14;
// List of networks for matching. // List of networks for matching.
repeated xray.common.net.Network networks = 13; repeated xray.common.net.Network networks = 13;
// List of GeoIPs for source IP address matching. If this entry exists, the // List of IPs for source IP address matching.
// source_cidr above will have no effect. repeated xray.common.geodata.IPRule source_ip = 11;
repeated GeoIP source_geoip = 11;
// List of ports for source port matching. // List of ports for source port matching.
xray.common.net.PortList source_port_list = 16; xray.common.net.PortList source_port_list = 16;
@@ -109,10 +46,14 @@ message RoutingRule {
map<string, string> attributes = 15; map<string, string> attributes = 15;
repeated GeoIP local_geoip = 17; // List of IPs for local IP address matching.
repeated xray.common.geodata.IPRule local_ip = 17;
// List of ports for local port matching.
xray.common.net.PortList local_port_list = 18; xray.common.net.PortList local_port_list = 18;
xray.common.net.PortList vless_route_list = 20; xray.common.net.PortList vless_route_list = 20;
repeated string process = 21; repeated string process = 21;
WebhookConfig webhook = 22; WebhookConfig webhook = 22;
} }
@@ -155,8 +96,7 @@ message Config {
// Use domain as is. // Use domain as is.
AsIs = 0; AsIs = 0;
// [Deprecated] Always resolve IP for domains. reserved 1;
// UseIp = 1;
// Resolve to IP if the domain doesn't match any rules. // Resolve to IP if the domain doesn't match any rules.
IpIfNonMatch = 2; IpIfNonMatch = 2;
-100
View File
@@ -1,100 +0,0 @@
package router
import (
"encoding/gob"
"errors"
"io"
"runtime"
"github.com/xtls/xray-core/common/strmatcher"
)
type geoSiteListGob struct {
Sites map[string][]byte
Deps map[string][]string
Hosts map[string][]string
}
func SerializeGeoSiteList(sites []*GeoSite, deps map[string][]string, hosts map[string][]string, w io.Writer) error {
data := geoSiteListGob{
Sites: make(map[string][]byte),
Deps: deps,
Hosts: hosts,
}
for _, site := range sites {
if site == nil {
continue
}
var buf bytesWriter
if err := SerializeDomainMatcher(site.Domain, &buf); err != nil {
return err
}
data.Sites[site.CountryCode] = buf.Bytes()
}
return gob.NewEncoder(w).Encode(data)
}
type bytesWriter struct {
data []byte
}
func (w *bytesWriter) Write(p []byte) (n int, err error) {
w.data = append(w.data, p...)
return len(p), nil
}
func (w *bytesWriter) Bytes() []byte {
return w.data
}
func LoadGeoSiteMatcher(r io.Reader, countryCode string) (strmatcher.IndexMatcher, error) {
var data geoSiteListGob
if err := gob.NewDecoder(r).Decode(&data); err != nil {
return nil, err
}
return loadWithDeps(&data, countryCode, make(map[string]bool))
}
func loadWithDeps(data *geoSiteListGob, code string, visited map[string]bool) (strmatcher.IndexMatcher, error) {
if visited[code] {
return nil, errors.New("cyclic dependency")
}
visited[code] = true
var matchers []strmatcher.IndexMatcher
if siteData, ok := data.Sites[code]; ok {
m, err := NewDomainMatcherFromBuffer(siteData)
if err == nil {
matchers = append(matchers, m)
}
}
if deps, ok := data.Deps[code]; ok {
for _, dep := range deps {
m, err := loadWithDeps(data, dep, visited)
if err == nil {
matchers = append(matchers, m)
}
}
}
if len(matchers) == 0 {
return nil, errors.New("matcher not found for: " + code)
}
if len(matchers) == 1 {
return matchers[0], nil
}
runtime.GC()
return &strmatcher.IndexMatcherGroup{Matchers: matchers}, nil
}
func LoadGeoSiteHosts(r io.Reader) (map[string][]string, error) {
var data geoSiteListGob
if err := gob.NewDecoder(r).Decode(&data); err != nil {
return nil, err
}
return data.Hosts, nil
}
+13 -15
View File
@@ -7,6 +7,7 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
. "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
@@ -155,12 +156,11 @@ func TestIPOnDemand(t *testing.T) {
TargetTag: &RoutingRule_Tag{ TargetTag: &RoutingRule_Tag{
Tag: "test", Tag: "test",
}, },
Geoip: []*GeoIP{ Ip: []*geodata.IPRule{
{ {
Cidr: []*CIDR{ Value: &geodata.IPRule_Custom{
{ Custom: &geodata.CIDRRule{
Ip: []byte{192, 168, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
Prefix: 16,
}, },
}, },
}, },
@@ -200,12 +200,11 @@ func TestIPIfNonMatchDomain(t *testing.T) {
TargetTag: &RoutingRule_Tag{ TargetTag: &RoutingRule_Tag{
Tag: "test", Tag: "test",
}, },
Geoip: []*GeoIP{ Ip: []*geodata.IPRule{
{ {
Cidr: []*CIDR{ Value: &geodata.IPRule_Custom{
{ Custom: &geodata.CIDRRule{
Ip: []byte{192, 168, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
Prefix: 16,
}, },
}, },
}, },
@@ -245,12 +244,11 @@ func TestIPIfNonMatchIP(t *testing.T) {
TargetTag: &RoutingRule_Tag{ TargetTag: &RoutingRule_Tag{
Tag: "test", Tag: "test",
}, },
Geoip: []*GeoIP{ Ip: []*geodata.IPRule{
{ {
Cidr: []*CIDR{ Value: &geodata.IPRule_Custom{
{ Custom: &geodata.CIDRRule{
Ip: []byte{127, 0, 0, 0}, Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
Prefix: 8,
}, },
}, },
}, },
+82 -17
View File
@@ -3,12 +3,10 @@ package command
import ( import (
"context" "context"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
feature_stats "github.com/xtls/xray-core/features/stats" feature_stats "github.com/xtls/xray-core/features/stats"
grpc "google.golang.org/grpc" grpc "google.golang.org/grpc"
@@ -70,9 +68,10 @@ func (s *statsServer) GetStatsOnlineIpList(ctx context.Context, request *GetStat
} }
ips := make(map[string]int64) ips := make(map[string]int64)
for ip, t := range c.IPTimeMap() { c.ForEach(func(ip string, lastSeen int64) bool {
ips[ip] = t.Unix() ips[ip] = lastSeen
} return true
})
return &GetStatsOnlineIpListResponse{ return &GetStatsOnlineIpListResponse{
Name: request.Name, Name: request.Name,
@@ -86,21 +85,87 @@ func (s *statsServer) GetAllOnlineUsers(ctx context.Context, request *GetAllOnli
}, nil }, nil
} }
func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { func (s *statsServer) GetUsersStats(ctx context.Context, request *GetUsersStatsRequest) (*GetUsersStatsResponse, error) {
matcher, err := strmatcher.Substr.New(request.Pattern) userMap := make(map[string]*UserStat)
if err != nil {
return nil, err s.stats.VisitOnlineMaps(func(name string, om feature_stats.OnlineMap) bool {
if om.Count() == 0 {
return true
}
_, rest, _ := strings.Cut(name, ">>>")
email, _, _ := strings.Cut(rest, ">>>")
user := &UserStat{Email: email}
om.ForEach(func(ip string, lastSeen int64) bool {
user.Ips = append(user.Ips, &OnlineIPEntry{
Ip: ip,
LastSeen: lastSeen,
})
return true
})
if len(user.Ips) > 0 {
userMap[email] = user
}
return true
})
if request.IncludeTraffic {
for _, u := range userMap {
u.Traffic = &TrafficUserStat{}
}
const (
prefixUser = "user>>>"
suffixUplink = ">>>traffic>>>uplink"
suffixDownlink = ">>>traffic>>>downlink"
)
s.stats.VisitCounters(func(name string, c feature_stats.Counter) bool {
var email string
var isUplink bool
if strings.HasSuffix(name, suffixUplink) {
email = name[len(prefixUser) : len(name)-len(suffixUplink)]
isUplink = true
} else if strings.HasSuffix(name, suffixDownlink) {
email = name[len(prefixUser) : len(name)-len(suffixDownlink)]
} else {
return true
}
u, ok := userMap[email]
if !ok {
return true
}
var value int64
if request.Reset_ {
value = c.Set(0)
} else {
value = c.Value()
}
if isUplink {
u.Traffic.Uplink = value
} else {
u.Traffic.Downlink = value
}
return true
})
} }
resp := &GetUsersStatsResponse{}
for _, u := range userMap {
resp.Users = append(resp.Users, u)
}
return resp, nil
}
func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) {
response := &QueryStatsResponse{} response := &QueryStatsResponse{}
manager, ok := s.stats.(*stats.Manager) s.stats.VisitCounters(func(name string, c feature_stats.Counter) bool {
if !ok { if strings.Contains(name, request.Pattern) {
return nil, errors.New("QueryStats only works its own stats.Manager.")
}
manager.VisitCounters(func(name string, c feature_stats.Counter) bool {
if matcher.Match(name) {
var value int64 var value int64
if request.Reset_ { if request.Reset_ {
value = c.Set(0) value = c.Set(0)
+314 -28
View File
@@ -551,6 +551,266 @@ func (x *GetAllOnlineUsersResponse) GetUsers() []string {
return nil return nil
} }
type OnlineIPEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
LastSeen int64 `protobuf:"varint,2,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OnlineIPEntry) Reset() {
*x = OnlineIPEntry{}
mi := &file_app_stats_command_command_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OnlineIPEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OnlineIPEntry) ProtoMessage() {}
func (x *OnlineIPEntry) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[10]
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 OnlineIPEntry.ProtoReflect.Descriptor instead.
func (*OnlineIPEntry) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{10}
}
func (x *OnlineIPEntry) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *OnlineIPEntry) GetLastSeen() int64 {
if x != nil {
return x.LastSeen
}
return 0
}
type TrafficUserStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Uplink int64 `protobuf:"varint,1,opt,name=uplink,proto3" json:"uplink,omitempty"`
Downlink int64 `protobuf:"varint,2,opt,name=downlink,proto3" json:"downlink,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TrafficUserStat) Reset() {
*x = TrafficUserStat{}
mi := &file_app_stats_command_command_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TrafficUserStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TrafficUserStat) ProtoMessage() {}
func (x *TrafficUserStat) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[11]
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 TrafficUserStat.ProtoReflect.Descriptor instead.
func (*TrafficUserStat) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{11}
}
func (x *TrafficUserStat) GetUplink() int64 {
if x != nil {
return x.Uplink
}
return 0
}
func (x *TrafficUserStat) GetDownlink() int64 {
if x != nil {
return x.Downlink
}
return 0
}
type UserStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
Ips []*OnlineIPEntry `protobuf:"bytes,2,rep,name=ips,proto3" json:"ips,omitempty"`
Traffic *TrafficUserStat `protobuf:"bytes,3,opt,name=traffic,proto3" json:"traffic,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserStat) Reset() {
*x = UserStat{}
mi := &file_app_stats_command_command_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserStat) ProtoMessage() {}
func (x *UserStat) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[12]
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 UserStat.ProtoReflect.Descriptor instead.
func (*UserStat) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{12}
}
func (x *UserStat) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *UserStat) GetIps() []*OnlineIPEntry {
if x != nil {
return x.Ips
}
return nil
}
func (x *UserStat) GetTraffic() *TrafficUserStat {
if x != nil {
return x.Traffic
}
return nil
}
type GetUsersStatsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
IncludeTraffic bool `protobuf:"varint,1,opt,name=include_traffic,json=includeTraffic,proto3" json:"include_traffic,omitempty"`
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetUsersStatsRequest) Reset() {
*x = GetUsersStatsRequest{}
mi := &file_app_stats_command_command_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetUsersStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUsersStatsRequest) ProtoMessage() {}
func (x *GetUsersStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[13]
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 GetUsersStatsRequest.ProtoReflect.Descriptor instead.
func (*GetUsersStatsRequest) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{13}
}
func (x *GetUsersStatsRequest) GetIncludeTraffic() bool {
if x != nil {
return x.IncludeTraffic
}
return false
}
func (x *GetUsersStatsRequest) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type GetUsersStatsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Users []*UserStat `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetUsersStatsResponse) Reset() {
*x = GetUsersStatsResponse{}
mi := &file_app_stats_command_command_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetUsersStatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUsersStatsResponse) ProtoMessage() {}
func (x *GetUsersStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[14]
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 GetUsersStatsResponse.ProtoReflect.Descriptor instead.
func (*GetUsersStatsResponse) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{14}
}
func (x *GetUsersStatsResponse) GetUsers() []*UserStat {
if x != nil {
return x.Users
}
return nil
}
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -559,7 +819,7 @@ type Config struct {
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_app_stats_command_command_proto_msgTypes[10] mi := &file_app_stats_command_command_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -571,7 +831,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[10] mi := &file_app_stats_command_command_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -584,7 +844,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{10} return file_app_stats_command_command_proto_rawDescGZIP(), []int{15}
} }
var File_app_stats_command_command_proto protoreflect.FileDescriptor var File_app_stats_command_command_proto protoreflect.FileDescriptor
@@ -628,8 +888,23 @@ const file_app_stats_command_command_proto_rawDesc = "" +
"\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x1a\n" + "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x1a\n" +
"\x18GetAllOnlineUsersRequest\"1\n" + "\x18GetAllOnlineUsersRequest\"1\n" +
"\x19GetAllOnlineUsersResponse\x12\x14\n" + "\x19GetAllOnlineUsersResponse\x12\x14\n" +
"\x05users\x18\x01 \x03(\tR\x05users\"\b\n" + "\x05users\x18\x01 \x03(\tR\x05users\"<\n" +
"\x06Config2\x96\x05\n" + "\rOnlineIPEntry\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\tR\x02ip\x12\x1b\n" +
"\tlast_seen\x18\x02 \x01(\x03R\blastSeen\"E\n" +
"\x0fTrafficUserStat\x12\x16\n" +
"\x06uplink\x18\x01 \x01(\x03R\x06uplink\x12\x1a\n" +
"\bdownlink\x18\x02 \x01(\x03R\bdownlink\"\x9c\x01\n" +
"\bUserStat\x12\x14\n" +
"\x05email\x18\x01 \x01(\tR\x05email\x127\n" +
"\x03ips\x18\x02 \x03(\v2%.xray.app.stats.command.OnlineIPEntryR\x03ips\x12A\n" +
"\atraffic\x18\x03 \x01(\v2'.xray.app.stats.command.TrafficUserStatR\atraffic\"U\n" +
"\x14GetUsersStatsRequest\x12'\n" +
"\x0finclude_traffic\x18\x01 \x01(\bR\x0eincludeTraffic\x12\x14\n" +
"\x05reset\x18\x02 \x01(\bR\x05reset\"O\n" +
"\x15GetUsersStatsResponse\x126\n" +
"\x05users\x18\x01 \x03(\v2 .xray.app.stats.command.UserStatR\x05users\"\b\n" +
"\x06Config2\x86\x06\n" +
"\fStatsService\x12_\n" + "\fStatsService\x12_\n" +
"\bGetStats\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + "\bGetStats\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" +
"\x0eGetStatsOnline\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + "\x0eGetStatsOnline\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" +
@@ -637,7 +912,8 @@ const file_app_stats_command_command_proto_rawDesc = "" +
"QueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12b\n" + "QueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12b\n" +
"\vGetSysStats\x12'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n" + "\vGetSysStats\x12'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n" +
"\x14GetStatsOnlineIpList\x12'.xray.app.stats.command.GetStatsRequest\x1a4.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x12z\n" + "\x14GetStatsOnlineIpList\x12'.xray.app.stats.command.GetStatsRequest\x1a4.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x12z\n" +
"\x11GetAllOnlineUsers\x120.xray.app.stats.command.GetAllOnlineUsersRequest\x1a1.xray.app.stats.command.GetAllOnlineUsersResponse\"\x00Bd\n" + "\x11GetAllOnlineUsers\x120.xray.app.stats.command.GetAllOnlineUsersRequest\x1a1.xray.app.stats.command.GetAllOnlineUsersResponse\"\x00\x12n\n" +
"\rGetUsersStats\x12,.xray.app.stats.command.GetUsersStatsRequest\x1a-.xray.app.stats.command.GetUsersStatsResponse\"\x00Bd\n" +
"\x1acom.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3" "\x1acom.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3"
var ( var (
@@ -652,7 +928,7 @@ func file_app_stats_command_command_proto_rawDescGZIP() []byte {
return file_app_stats_command_command_proto_rawDescData return file_app_stats_command_command_proto_rawDescData
} }
var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
var file_app_stats_command_command_proto_goTypes = []any{ var file_app_stats_command_command_proto_goTypes = []any{
(*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest (*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest
(*Stat)(nil), // 1: xray.app.stats.command.Stat (*Stat)(nil), // 1: xray.app.stats.command.Stat
@@ -664,30 +940,40 @@ var file_app_stats_command_command_proto_goTypes = []any{
(*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse (*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse
(*GetAllOnlineUsersRequest)(nil), // 8: xray.app.stats.command.GetAllOnlineUsersRequest (*GetAllOnlineUsersRequest)(nil), // 8: xray.app.stats.command.GetAllOnlineUsersRequest
(*GetAllOnlineUsersResponse)(nil), // 9: xray.app.stats.command.GetAllOnlineUsersResponse (*GetAllOnlineUsersResponse)(nil), // 9: xray.app.stats.command.GetAllOnlineUsersResponse
(*Config)(nil), // 10: xray.app.stats.command.Config (*OnlineIPEntry)(nil), // 10: xray.app.stats.command.OnlineIPEntry
nil, // 11: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry (*TrafficUserStat)(nil), // 11: xray.app.stats.command.TrafficUserStat
(*UserStat)(nil), // 12: xray.app.stats.command.UserStat
(*GetUsersStatsRequest)(nil), // 13: xray.app.stats.command.GetUsersStatsRequest
(*GetUsersStatsResponse)(nil), // 14: xray.app.stats.command.GetUsersStatsResponse
(*Config)(nil), // 15: xray.app.stats.command.Config
nil, // 16: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry
} }
var file_app_stats_command_command_proto_depIdxs = []int32{ var file_app_stats_command_command_proto_depIdxs = []int32{
1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat 1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat
1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat 1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat
11, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry 16, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry
0, // 3: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest 10, // 3: xray.app.stats.command.UserStat.ips:type_name -> xray.app.stats.command.OnlineIPEntry
0, // 4: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest 11, // 4: xray.app.stats.command.UserStat.traffic:type_name -> xray.app.stats.command.TrafficUserStat
3, // 5: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest 12, // 5: xray.app.stats.command.GetUsersStatsResponse.users:type_name -> xray.app.stats.command.UserStat
5, // 6: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest 0, // 6: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest
0, // 7: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest 0, // 7: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest
8, // 8: xray.app.stats.command.StatsService.GetAllOnlineUsers:input_type -> xray.app.stats.command.GetAllOnlineUsersRequest 3, // 8: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest
2, // 9: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse 5, // 9: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest
2, // 10: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse 0, // 10: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest
4, // 11: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse 8, // 11: xray.app.stats.command.StatsService.GetAllOnlineUsers:input_type -> xray.app.stats.command.GetAllOnlineUsersRequest
6, // 12: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse 13, // 12: xray.app.stats.command.StatsService.GetUsersStats:input_type -> xray.app.stats.command.GetUsersStatsRequest
7, // 13: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse 2, // 13: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse
9, // 14: xray.app.stats.command.StatsService.GetAllOnlineUsers:output_type -> xray.app.stats.command.GetAllOnlineUsersResponse 2, // 14: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse
9, // [9:15] is the sub-list for method output_type 4, // 15: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse
3, // [3:9] is the sub-list for method input_type 6, // 16: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse
3, // [3:3] is the sub-list for extension type_name 7, // 17: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse
3, // [3:3] is the sub-list for extension extendee 9, // 18: xray.app.stats.command.StatsService.GetAllOnlineUsers:output_type -> xray.app.stats.command.GetAllOnlineUsersResponse
0, // [0:3] is the sub-list for field type_name 14, // 19: xray.app.stats.command.StatsService.GetUsersStats:output_type -> xray.app.stats.command.GetUsersStatsResponse
13, // [13:20] is the sub-list for method output_type
6, // [6:13] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
} }
func init() { file_app_stats_command_command_proto_init() } func init() { file_app_stats_command_command_proto_init() }
@@ -701,7 +987,7 @@ func file_app_stats_command_command_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_stats_command_command_proto_rawDesc), len(file_app_stats_command_command_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_stats_command_command_proto_rawDesc), len(file_app_stats_command_command_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 12, NumMessages: 17,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+26
View File
@@ -57,6 +57,31 @@ message GetAllOnlineUsersResponse {
repeated string users = 1; repeated string users = 1;
} }
message OnlineIPEntry {
string ip = 1;
int64 last_seen = 2;
}
message TrafficUserStat {
int64 uplink = 1;
int64 downlink = 2;
}
message UserStat {
string email = 1;
repeated OnlineIPEntry ips = 2;
TrafficUserStat traffic = 3;
}
message GetUsersStatsRequest {
bool include_traffic = 1;
bool reset = 2;
}
message GetUsersStatsResponse {
repeated UserStat users = 1;
}
service StatsService { service StatsService {
rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {}
rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {}
@@ -64,6 +89,7 @@ service StatsService {
rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {}
rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {} rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {}
rpc GetAllOnlineUsers(GetAllOnlineUsersRequest) returns (GetAllOnlineUsersResponse) {} rpc GetAllOnlineUsers(GetAllOnlineUsersRequest) returns (GetAllOnlineUsersResponse) {}
rpc GetUsersStats(GetUsersStatsRequest) returns (GetUsersStatsResponse) {}
} }
message Config {} message Config {}
+38
View File
@@ -25,6 +25,7 @@ const (
StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats" StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats"
StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList" StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList"
StatsService_GetAllOnlineUsers_FullMethodName = "/xray.app.stats.command.StatsService/GetAllOnlineUsers" StatsService_GetAllOnlineUsers_FullMethodName = "/xray.app.stats.command.StatsService/GetAllOnlineUsers"
StatsService_GetUsersStats_FullMethodName = "/xray.app.stats.command.StatsService/GetUsersStats"
) )
// StatsServiceClient is the client API for StatsService service. // StatsServiceClient is the client API for StatsService service.
@@ -37,6 +38,7 @@ type StatsServiceClient interface {
GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error)
GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error) GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error)
GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error) GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error)
GetUsersStats(ctx context.Context, in *GetUsersStatsRequest, opts ...grpc.CallOption) (*GetUsersStatsResponse, error)
} }
type statsServiceClient struct { type statsServiceClient struct {
@@ -107,6 +109,16 @@ func (c *statsServiceClient) GetAllOnlineUsers(ctx context.Context, in *GetAllOn
return out, nil return out, nil
} }
func (c *statsServiceClient) GetUsersStats(ctx context.Context, in *GetUsersStatsRequest, opts ...grpc.CallOption) (*GetUsersStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUsersStatsResponse)
err := c.cc.Invoke(ctx, StatsService_GetUsersStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// StatsServiceServer is the server API for StatsService service. // StatsServiceServer is the server API for StatsService service.
// All implementations must embed UnimplementedStatsServiceServer // All implementations must embed UnimplementedStatsServiceServer
// for forward compatibility. // for forward compatibility.
@@ -117,6 +129,7 @@ type StatsServiceServer interface {
GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error)
GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error)
GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error)
GetUsersStats(context.Context, *GetUsersStatsRequest) (*GetUsersStatsResponse, error)
mustEmbedUnimplementedStatsServiceServer() mustEmbedUnimplementedStatsServiceServer()
} }
@@ -145,6 +158,9 @@ func (UnimplementedStatsServiceServer) GetStatsOnlineIpList(context.Context, *Ge
func (UnimplementedStatsServiceServer) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) { func (UnimplementedStatsServiceServer) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAllOnlineUsers not implemented") return nil, status.Error(codes.Unimplemented, "method GetAllOnlineUsers not implemented")
} }
func (UnimplementedStatsServiceServer) GetUsersStats(context.Context, *GetUsersStatsRequest) (*GetUsersStatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUsersStats not implemented")
}
func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {}
func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} func (UnimplementedStatsServiceServer) testEmbeddedByValue() {}
@@ -274,6 +290,24 @@ func _StatsService_GetAllOnlineUsers_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StatsService_GetUsersStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUsersStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StatsServiceServer).GetUsersStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StatsService_GetUsersStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StatsServiceServer).GetUsersStats(ctx, req.(*GetUsersStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
// StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. // StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -305,6 +339,10 @@ var StatsService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetAllOnlineUsers", MethodName: "GetAllOnlineUsers",
Handler: _StatsService_GetAllOnlineUsers_Handler, Handler: _StatsService_GetAllOnlineUsers_Handler,
}, },
{
MethodName: "GetUsersStats",
Handler: _StatsService_GetUsersStats_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "app/stats/command/command.proto", Metadata: "app/stats/command/command.proto",
+15 -27
View File
@@ -13,14 +13,14 @@ const (
type ipEntry struct { type ipEntry struct {
refCount int refCount int
lastSeen time.Time lastSeen int64
} }
// OnlineMap is a refcount-based implementation of stats.OnlineMap. // OnlineMap is a refcount-based implementation of stats.OnlineMap.
// IPs are tracked by reference counting: AddIP increments, RemoveIP decrements. // IPs are tracked by reference counting: AddIP increments, RemoveIP decrements.
// An IP is removed from the map when its reference count reaches zero. // An IP is removed from the map when its reference count reaches zero.
type OnlineMap struct { type OnlineMap struct {
entries map[string]*ipEntry entries map[string]ipEntry
access sync.Mutex access sync.Mutex
count atomic.Int64 count atomic.Int64
} }
@@ -28,7 +28,7 @@ type OnlineMap struct {
// NewOnlineMap creates a new OnlineMap instance. // NewOnlineMap creates a new OnlineMap instance.
func NewOnlineMap() *OnlineMap { func NewOnlineMap() *OnlineMap {
return &OnlineMap{ return &OnlineMap{
entries: make(map[string]*ipEntry), entries: make(map[string]ipEntry),
} }
} }
@@ -37,17 +37,17 @@ func (om *OnlineMap) AddIP(ip string) {
if ip == localhostIPv4 || ip == localhostIPv6 { if ip == localhostIPv4 || ip == localhostIPv6 {
return return
} }
now := time.Now().Unix()
om.access.Lock() om.access.Lock()
defer om.access.Unlock() defer om.access.Unlock()
if e, ok := om.entries[ip]; ok { if e, ok := om.entries[ip]; ok {
e.refCount++ e.refCount++
e.lastSeen = time.Now() e.lastSeen = now
om.entries[ip] = e
} else { } else {
om.entries[ip] = &ipEntry{ om.entries[ip] = ipEntry{
refCount: 1, refCount: 1,
lastSeen: time.Now(), lastSeen: now,
} }
om.count.Add(1) om.count.Add(1)
} }
@@ -57,7 +57,6 @@ func (om *OnlineMap) AddIP(ip string) {
func (om *OnlineMap) RemoveIP(ip string) { func (om *OnlineMap) RemoveIP(ip string) {
om.access.Lock() om.access.Lock()
defer om.access.Unlock() defer om.access.Unlock()
e, ok := om.entries[ip] e, ok := om.entries[ip]
if !ok { if !ok {
return return
@@ -66,6 +65,8 @@ func (om *OnlineMap) RemoveIP(ip string) {
if e.refCount <= 0 { if e.refCount <= 0 {
delete(om.entries, ip) delete(om.entries, ip)
om.count.Add(-1) om.count.Add(-1)
} else {
om.entries[ip] = e
} }
} }
@@ -74,26 +75,13 @@ func (om *OnlineMap) Count() int {
return int(om.count.Load()) return int(om.count.Load())
} }
// List implements stats.OnlineMap. // ForEach calls fn for each online IP. If fn returns false, iteration stops.
func (om *OnlineMap) List() []string { func (om *OnlineMap) ForEach(fn func(string, int64) bool) {
om.access.Lock() om.access.Lock()
defer om.access.Unlock() defer om.access.Unlock()
keys := make([]string, 0, len(om.entries))
for ip := range om.entries {
keys = append(keys, ip)
}
return keys
}
// IPTimeMap implements stats.OnlineMap.
func (om *OnlineMap) IPTimeMap() map[string]time.Time {
om.access.Lock()
defer om.access.Unlock()
result := make(map[string]time.Time, len(om.entries))
for ip, e := range om.entries { for ip, e := range om.entries {
result[ip] = e.lastSeen if !fn(ip, e.lastSeen) {
break
}
} }
return result
} }
+35 -19
View File
@@ -11,19 +11,19 @@ import (
// Manager is an implementation of stats.Manager. // Manager is an implementation of stats.Manager.
type Manager struct { type Manager struct {
access sync.RWMutex access sync.RWMutex
counters map[string]*Counter counters map[string]*Counter
onlineMap map[string]*OnlineMap onlineMaps map[string]*OnlineMap
channels map[string]*Channel channels map[string]*Channel
running bool running bool
} }
// NewManager creates an instance of Statistics Manager. // NewManager creates an instance of Statistics Manager.
func NewManager(ctx context.Context, config *Config) (*Manager, error) { func NewManager(ctx context.Context, config *Config) (*Manager, error) {
m := &Manager{ m := &Manager{
counters: make(map[string]*Counter), counters: make(map[string]*Counter),
onlineMap: make(map[string]*OnlineMap), onlineMaps: make(map[string]*OnlineMap),
channels: make(map[string]*Channel), channels: make(map[string]*Channel),
} }
return m, nil return m, nil
@@ -88,12 +88,12 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if _, found := m.onlineMap[name]; found { if _, found := m.onlineMaps[name]; found {
return nil, errors.New("onlineMap ", name, " already registered.") return nil, errors.New("OnlineMap ", name, " already registered.")
} }
errors.LogDebug(context.Background(), "create new onlineMap ", name) errors.LogDebug(context.Background(), "create new OnlineMap ", name)
om := NewOnlineMap() om := NewOnlineMap()
m.onlineMap[name] = om m.onlineMaps[name] = om
return om, nil return om, nil
} }
@@ -102,9 +102,9 @@ func (m *Manager) UnregisterOnlineMap(name string) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if _, found := m.onlineMap[name]; found { if _, found := m.onlineMaps[name]; found {
errors.LogDebug(context.Background(), "remove onlineMap ", name) errors.LogDebug(context.Background(), "remove OnlineMap ", name)
delete(m.onlineMap, name) delete(m.onlineMaps, name)
} }
return nil return nil
} }
@@ -114,12 +114,24 @@ func (m *Manager) GetOnlineMap(name string) stats.OnlineMap {
m.access.RLock() m.access.RLock()
defer m.access.RUnlock() defer m.access.RUnlock()
if om, found := m.onlineMap[name]; found { if om, found := m.onlineMaps[name]; found {
return om return om
} }
return nil return nil
} }
// VisitOnlineMaps calls visitor function on all managed online maps.
// The visitor runs under a read lock; it must not call RegisterOnlineMap or UnregisterOnlineMap (would deadlock).
func (m *Manager) VisitOnlineMaps(visitor func(string, stats.OnlineMap) bool) {
m.access.RLock()
defer m.access.RUnlock()
for name, om := range m.onlineMaps {
if !visitor(name, om) {
break
}
}
}
// RegisterChannel implements stats.Manager. // RegisterChannel implements stats.Manager.
func (m *Manager) RegisterChannel(name string) (stats.Channel, error) { func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
m.access.Lock() m.access.Lock()
@@ -166,9 +178,9 @@ func (m *Manager) GetAllOnlineUsers() []string {
m.access.RLock() m.access.RLock()
defer m.access.RUnlock() defer m.access.RUnlock()
usersOnline := make([]string, 0, len(m.onlineMap)) usersOnline := make([]string, 0, len(m.onlineMaps))
for user, onlineMap := range m.onlineMap { for user, om := range m.onlineMaps {
if onlineMap.Count() > 0 { if om.Count() > 0 {
usersOnline = append(usersOnline, user) usersOnline = append(usersOnline, user)
} }
} }
@@ -198,6 +210,10 @@ func (m *Manager) Close() error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
m.running = false m.running = false
for name := range m.onlineMaps {
errors.LogDebug(context.Background(), "remove OnlineMap ", name)
delete(m.onlineMaps, name)
}
errs := []error{} errs := []error{}
for name, channel := range m.channels { for name, channel := range m.channels {
errors.LogDebug(context.Background(), "remove channel ", name) errors.LogDebug(context.Background(), "remove channel ", name)
+237
View File
@@ -0,0 +1,237 @@
package geodata
import (
"context"
"runtime"
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata/strmatcher"
)
type DomainMatcher interface {
// Match returns the indices of all rules that match the input domain.
// The returned slice is owned by the caller and may be safely modified.
// Note: the slice may contain duplicates and the order is unspecified.
Match(input string) []uint32
MatchAny(input string) bool
}
type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}
type MphDomainMatcherFactory struct {
sync.Mutex
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
}
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 := f.shared[key]; g != nil {
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) {
case *DomainRule_Custom:
m, err := parseDomain(v.Custom)
if err != nil {
return nil, err
}
g.Add(m, uint32(i))
case *DomainRule_Geosite:
domains, err := loadSiteWithAttrs(v.Geosite.File, v.Geosite.Code, v.Geosite.Attrs)
if err != nil {
return nil, err
}
for j, d := range domains {
domains[j] = nil // peak mem
m, err := parseDomain(d)
if err != nil {
errors.LogError(context.Background(), "ignore invalid geosite entry in ", v.Geosite.File, ":", v.Geosite.Code, " at index ", j, ", ", err)
continue
}
g.Add(m, uint32(i))
}
default:
panic("unknown domain rule type")
}
}
if err := g.Build(); err != nil {
return nil, err
}
if key != "" {
f.shared[key] = g
}
return g, nil
}
type CompactDomainMatcherFactory struct {
sync.Mutex
shared map[string]strmatcher.MatcherSet // TODO: cleanup
}
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
f.Lock()
defer f.Unlock()
if s := f.shared[key]; s != nil {
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil
}
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
s := strmatcher.NewLinearAnyMatcher()
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
if err != nil {
return nil, err
}
for i, d := range domains {
domains[i] = nil // peak mem
m, err := parseDomain(d)
if err != nil {
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
s.Add(m)
}
f.shared[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.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
m, err := parseDomain(v.Custom)
if err != nil {
return nil, err
}
if compact.custom == nil {
compact.custom = strmatcher.NewLinearValueMatcher()
}
compact.custom.Add(m, uint32(i))
case *DomainRule_Geosite:
m, err := f.getOrCreateFrom(v.Geosite)
if err != nil {
return nil, err
}
compact.matchers = append(compact.matchers, m)
compact.values = append(compact.values, uint32(i))
default:
panic("unknown domain rule type")
}
}
return compact, nil
}
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherSet
values []uint32
}
// Match implements DomainMatcher.
func (c *CompactDomainMatcher) Match(input string) []uint32 {
var result []uint32
if c.custom != nil {
result = append(result, c.custom.Match(input)...)
}
for i, m := range c.matchers {
if m.MatchAny(input) {
result = append(result, c.values[i])
}
}
return result
}
// MatchAny implements DomainMatcher.
func (c *CompactDomainMatcher) MatchAny(input string) bool {
if c.custom != nil && c.custom.MatchAny(input) {
return true
}
for _, m := range c.matchers {
if m.MatchAny(input) {
return true
}
}
return false
}
func parseDomain(d *Domain) (strmatcher.Matcher, error) {
if d == nil {
return nil, errors.New("domain must not be nil")
}
switch d.Type {
case Domain_Substr:
return strmatcher.Substr.New(strings.ToLower(d.Value))
case Domain_Regex:
return strmatcher.Regex.New(d.Value)
case Domain_Domain:
return strmatcher.Domain.New(d.Value)
case Domain_Full:
return strmatcher.Full.New(strings.ToLower(d.Value))
default:
return nil, errors.New("unknown domain type: ", d.Type)
}
}
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
default:
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
}
}
+72
View File
@@ -0,0 +1,72 @@
package geodata
import (
"path/filepath"
"reflect"
"slices"
"testing"
"github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("example.com")
slices.Sort(got)
want := []uint32{0, 1}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("163.com")
slices.Sort(got)
want := []uint32{0, 1}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("example.com")
if !reflect.DeepEqual(got, []uint32{0, 1}) {
t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1})
}
got[0] = 1
gotAgain := matcher.Match("example.com")
if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) {
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
}
}
+94
View File
@@ -0,0 +1,94 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
)
type DomainRegistry struct {
mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
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 {
return &DomainRegistry{
factory: newDomainMatcherFactory(),
}
}
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
}
+965
View File
@@ -0,0 +1,965 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: common/geodata/geodat.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 of domain value.
type Domain_Type int32
const (
// The value is used as a sub string.
Domain_Substr Domain_Type = 0
// The value is used as a regular expression.
Domain_Regex Domain_Type = 1
// The value is a domain.
Domain_Domain Domain_Type = 2
// The value is a full domain.
Domain_Full Domain_Type = 3
)
// Enum value maps for Domain_Type.
var (
Domain_Type_name = map[int32]string{
0: "Substr",
1: "Regex",
2: "Domain",
3: "Full",
}
Domain_Type_value = map[string]int32{
"Substr": 0,
"Regex": 1,
"Domain": 2,
"Full": 3,
}
)
func (x Domain_Type) Enum() *Domain_Type {
p := new(Domain_Type)
*p = x
return p
}
func (x Domain_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
return file_common_geodata_geodat_proto_enumTypes[0].Descriptor()
}
func (Domain_Type) Type() protoreflect.EnumType {
return &file_common_geodata_geodat_proto_enumTypes[0]
}
func (x Domain_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Domain_Type.Descriptor instead.
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{0, 0}
}
type Domain struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Domain matching type.
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.common.geodata.Domain_Type" json:"type,omitempty"`
// Domain value.
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
// Attributes of this domain. May be used for filtering.
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Domain) Reset() {
*x = Domain{}
mi := &file_common_geodata_geodat_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_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 Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{0}
}
func (x *Domain) GetType() Domain_Type {
if x != nil {
return x.Type
}
return Domain_Substr
}
func (x *Domain) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Domain) GetAttribute() []*Domain_Attribute {
if x != nil {
return x.Attribute
}
return nil
}
type GeoSite struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSite) Reset() {
*x = GeoSite{}
mi := &file_common_geodata_geodat_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSite) ProtoMessage() {}
func (x *GeoSite) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_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 GeoSite.ProtoReflect.Descriptor instead.
func (*GeoSite) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{1}
}
func (x *GeoSite) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoSite) GetDomain() []*Domain {
if x != nil {
return x.Domain
}
return nil
}
type GeoSiteList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSiteList) Reset() {
*x = GeoSiteList{}
mi := &file_common_geodata_geodat_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSiteList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteList) ProtoMessage() {}
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[2]
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 GeoSiteList.ProtoReflect.Descriptor instead.
func (*GeoSiteList) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{2}
}
func (x *GeoSiteList) GetEntry() []*GeoSite {
if x != nil {
return x.Entry
}
return nil
}
type GeoSiteRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"`
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
Attrs string `protobuf:"bytes,3,opt,name=attrs,proto3" json:"attrs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSiteRule) Reset() {
*x = GeoSiteRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSiteRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteRule) ProtoMessage() {}
func (x *GeoSiteRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSiteRule.ProtoReflect.Descriptor instead.
func (*GeoSiteRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{3}
}
func (x *GeoSiteRule) GetFile() string {
if x != nil {
return x.File
}
return ""
}
func (x *GeoSiteRule) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoSiteRule) GetAttrs() string {
if x != nil {
return x.Attrs
}
return ""
}
type DomainRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Value:
//
// *DomainRule_Geosite
// *DomainRule_Custom
Value isDomainRule_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DomainRule) Reset() {
*x = DomainRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DomainRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DomainRule) ProtoMessage() {}
func (x *DomainRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DomainRule.ProtoReflect.Descriptor instead.
func (*DomainRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{4}
}
func (x *DomainRule) GetValue() isDomainRule_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *DomainRule) GetGeosite() *GeoSiteRule {
if x != nil {
if x, ok := x.Value.(*DomainRule_Geosite); ok {
return x.Geosite
}
}
return nil
}
func (x *DomainRule) GetCustom() *Domain {
if x != nil {
if x, ok := x.Value.(*DomainRule_Custom); ok {
return x.Custom
}
}
return nil
}
type isDomainRule_Value interface {
isDomainRule_Value()
}
type DomainRule_Geosite struct {
Geosite *GeoSiteRule `protobuf:"bytes,1,opt,name=geosite,proto3,oneof"`
}
type DomainRule_Custom struct {
Custom *Domain `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
}
func (*DomainRule_Geosite) isDomainRule_Value() {}
func (*DomainRule_Custom) isDomainRule_Value() {}
type CIDR struct {
state protoimpl.MessageState `protogen:"open.v1"`
// IP address, should be either 4 or 16 bytes.
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
// Number of leading ones in the network mask.
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CIDR) Reset() {
*x = CIDR{}
mi := &file_common_geodata_geodat_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CIDR) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDR) ProtoMessage() {}
func (x *CIDR) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[5]
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 CIDR.ProtoReflect.Descriptor instead.
func (*CIDR) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{5}
}
func (x *CIDR) GetIp() []byte {
if x != nil {
return x.Ip
}
return nil
}
func (x *CIDR) GetPrefix() uint32 {
if x != nil {
return x.Prefix
}
return 0
}
type CIDRRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cidr *CIDR `protobuf:"bytes,1,opt,name=cidr,proto3" json:"cidr,omitempty"`
ReverseMatch bool `protobuf:"varint,2,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CIDRRule) Reset() {
*x = CIDRRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CIDRRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDRRule) ProtoMessage() {}
func (x *CIDRRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CIDRRule.ProtoReflect.Descriptor instead.
func (*CIDRRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
}
func (x *CIDRRule) GetCidr() *CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *CIDRRule) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type GeoIP struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
mi := &file_common_geodata_geodat_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[7]
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 GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
}
func (x *GeoIP) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoIP) GetCidr() []*CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *GeoIP) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type GeoIPList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
mi := &file_common_geodata_geodat_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIPList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[8]
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 GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
if x != nil {
return x.Entry
}
return nil
}
type GeoIPRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"`
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIPRule) Reset() {
*x = GeoIPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIPRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPRule) ProtoMessage() {}
func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[9]
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 GeoIPRule.ProtoReflect.Descriptor instead.
func (*GeoIPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
}
func (x *GeoIPRule) GetFile() string {
if x != nil {
return x.File
}
return ""
}
func (x *GeoIPRule) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoIPRule) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type IPRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Value:
//
// *IPRule_Geoip
// *IPRule_Custom
Value isIPRule_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IPRule) Reset() {
*x = IPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IPRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IPRule) ProtoMessage() {}
func (x *IPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[10]
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 IPRule.ProtoReflect.Descriptor instead.
func (*IPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10}
}
func (x *IPRule) GetValue() isIPRule_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *IPRule) GetGeoip() *GeoIPRule {
if x != nil {
if x, ok := x.Value.(*IPRule_Geoip); ok {
return x.Geoip
}
}
return nil
}
func (x *IPRule) GetCustom() *CIDRRule {
if x != nil {
if x, ok := x.Value.(*IPRule_Custom); ok {
return x.Custom
}
}
return nil
}
type isIPRule_Value interface {
isIPRule_Value()
}
type IPRule_Geoip struct {
Geoip *GeoIPRule `protobuf:"bytes,1,opt,name=geoip,proto3,oneof"`
}
type IPRule_Custom struct {
Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
}
func (*IPRule_Geoip) isIPRule_Value() {}
func (*IPRule_Custom) isIPRule_Value() {}
type Domain_Attribute struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Types that are valid to be assigned to TypedValue:
//
// *Domain_Attribute_BoolValue
// *Domain_Attribute_IntValue
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
mi := &file_common_geodata_geodat_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Domain_Attribute) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[11]
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 Domain_Attribute.ProtoReflect.Descriptor instead.
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Domain_Attribute) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
if x != nil {
return x.TypedValue
}
return nil
}
func (x *Domain_Attribute) GetBoolValue() bool {
if x != nil {
if x, ok := x.TypedValue.(*Domain_Attribute_BoolValue); ok {
return x.BoolValue
}
}
return false
}
func (x *Domain_Attribute) GetIntValue() int64 {
if x != nil {
if x, ok := x.TypedValue.(*Domain_Attribute_IntValue); ok {
return x.IntValue
}
}
return 0
}
type isDomain_Attribute_TypedValue interface {
isDomain_Attribute_TypedValue()
}
type Domain_Attribute_BoolValue struct {
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
}
type Domain_Attribute_IntValue struct {
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
}
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
var File_common_geodata_geodat_proto protoreflect.FileDescriptor
const file_common_geodata_geodat_proto_rawDesc = "" +
"\n" +
"\x1bcommon/geodata/geodat.proto\x12\x13xray.common.geodata\"\xbc\x02\n" +
"\x06Domain\x124\n" +
"\x04type\x18\x01 \x01(\x0e2 .xray.common.geodata.Domain.TypeR\x04type\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value\x12C\n" +
"\tattribute\x18\x03 \x03(\v2%.xray.common.geodata.Domain.AttributeR\tattribute\x1al\n" +
"\tAttribute\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x1f\n" +
"\n" +
"bool_value\x18\x02 \x01(\bH\x00R\tboolValue\x12\x1d\n" +
"\tint_value\x18\x03 \x01(\x03H\x00R\bintValueB\r\n" +
"\vtyped_value\"3\n" +
"\x04Type\x12\n" +
"\n" +
"\x06Substr\x10\x00\x12\t\n" +
"\x05Regex\x10\x01\x12\n" +
"\n" +
"\x06Domain\x10\x02\x12\b\n" +
"\x04Full\x10\x03\"R\n" +
"\aGeoSite\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x123\n" +
"\x06domain\x18\x02 \x03(\v2\x1b.xray.common.geodata.DomainR\x06domain\"A\n" +
"\vGeoSiteList\x122\n" +
"\x05entry\x18\x01 \x03(\v2\x1c.xray.common.geodata.GeoSiteR\x05entry\"K\n" +
"\vGeoSiteRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12\x14\n" +
"\x05attrs\x18\x03 \x01(\tR\x05attrs\"\x8a\x01\n" +
"\n" +
"DomainRule\x12<\n" +
"\ageosite\x18\x01 \x01(\v2 .xray.common.geodata.GeoSiteRuleH\x00R\ageosite\x125\n" +
"\x06custom\x18\x02 \x01(\v2\x1b.xray.common.geodata.DomainH\x00R\x06customB\a\n" +
"\x05value\".\n" +
"\x04CIDR\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" +
"\bCIDRRule\x12-\n" +
"\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" +
"\x05GeoIP\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"=\n" +
"\tGeoIPList\x120\n" +
"\x05entry\x18\x01 \x03(\v2\x1a.xray.common.geodata.GeoIPR\x05entry\"X\n" +
"\tGeoIPRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" +
"\x06IPRule\x126\n" +
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" +
"\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" +
"\x05valueB[\n" +
"\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3"
var (
file_common_geodata_geodat_proto_rawDescOnce sync.Once
file_common_geodata_geodat_proto_rawDescData []byte
)
func file_common_geodata_geodat_proto_rawDescGZIP() []byte {
file_common_geodata_geodat_proto_rawDescOnce.Do(func() {
file_common_geodata_geodat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)))
})
return file_common_geodata_geodat_proto_rawDescData
}
var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_common_geodata_geodat_proto_goTypes = []any{
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
(*Domain)(nil), // 1: xray.common.geodata.Domain
(*GeoSite)(nil), // 2: xray.common.geodata.GeoSite
(*GeoSiteList)(nil), // 3: xray.common.geodata.GeoSiteList
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
(*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule
(*GeoIP)(nil), // 8: xray.common.geodata.GeoIP
(*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList
(*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule
(*IPRule)(nil), // 11: xray.common.geodata.IPRule
(*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute
}
var file_common_geodata_geodat_proto_depIdxs = []int32{
0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type
12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain
2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite
4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule
1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain
6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR
6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_common_geodata_geodat_proto_init() }
func file_common_geodata_geodat_proto_init() {
if File_common_geodata_geodat_proto != nil {
return
}
file_common_geodata_geodat_proto_msgTypes[4].OneofWrappers = []any{
(*DomainRule_Geosite)(nil),
(*DomainRule_Custom)(nil),
}
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
(*IPRule_Geoip)(nil),
(*IPRule_Custom)(nil),
}
file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
NumEnums: 1,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_common_geodata_geodat_proto_goTypes,
DependencyIndexes: file_common_geodata_geodat_proto_depIdxs,
EnumInfos: file_common_geodata_geodat_proto_enumTypes,
MessageInfos: file_common_geodata_geodat_proto_msgTypes,
}.Build()
File_common_geodata_geodat_proto = out.File
file_common_geodata_geodat_proto_goTypes = nil
file_common_geodata_geodat_proto_depIdxs = nil
}
+95
View File
@@ -0,0 +1,95 @@
syntax = "proto3";
package xray.common.geodata;
option csharp_namespace = "Xray.Common.Geodata";
option go_package = "github.com/xtls/xray-core/common/geodata";
option java_package = "com.xray.common.geodata";
option java_multiple_files = true;
message Domain {
// Type of domain value.
enum Type {
// The value is used as a sub string.
Substr = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a domain.
Domain = 2;
// The value is a full domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
message GeoSite {
string code = 1;
repeated Domain domain = 2;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
message GeoSiteRule {
string file = 1;
string code = 2;
string attrs = 3;
}
message DomainRule {
oneof value {
GeoSiteRule geosite = 1;
Domain custom = 2;
}
}
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message CIDRRule {
CIDR cidr = 1;
bool reverse_match = 2;
}
message GeoIP {
string code = 1;
repeated CIDR cidr = 2;
bool reverse_match = 3;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoIPRule {
string file = 1;
string code = 2;
bool reverse_match = 3;
}
message IPRule {
oneof value {
GeoIPRule geoip = 1;
CIDRRule custom = 2;
}
}
+207
View File
@@ -0,0 +1,207 @@
package geodata
import (
"bufio"
"bytes"
"io"
"runtime"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
func checkFile(file, code string) error {
r, err := filesystem.OpenAsset(file)
if err != nil {
return errors.New("failed to open ", file).Base(err)
}
defer r.Close()
if _, err := find(r, []byte(code), false); err != nil {
return errors.New("failed to check code ", code, " from ", file).Base(err)
}
return nil
}
func loadFile(file, code string) ([]byte, error) {
runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file)
if err != nil {
return nil, errors.New("failed to open ", file).Base(err)
}
defer r.Close()
bs, err := find(r, []byte(code), true)
if err != nil {
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
}
return bs, nil
}
func loadIP(file, code string) ([]*CIDR, error) {
bs, err := loadFile(file, code)
if err != nil {
return nil, err
}
defer runtime.GC() // peak mem
var geoip GeoIP
if err := proto.Unmarshal(bs, &geoip); err != nil {
return nil, errors.New("error unmarshal IP in ", file, ":", code).Base(err)
}
return geoip.Cidr, nil
}
func loadSite(file, code string) ([]*Domain, error) {
bs, err := loadFile(file, code)
if err != nil {
return nil, err
}
defer runtime.GC() // peak mem
var geosite GeoSite
if err := proto.Unmarshal(bs, &geosite); err != nil {
return nil, errors.New("error unmarshal Site in ", file, ":", code).Base(err)
}
return geosite.Domain, nil
}
func decodeVarint(br *bufio.Reader) (uint64, error) {
var x uint64
for shift := uint(0); shift < 64; shift += 7 {
b, err := br.ReadByte()
if err != nil {
return 0, err
}
x |= (uint64(b) & 0x7F) << shift
if (b & 0x80) == 0 {
return x, nil
}
}
// The number is too large to represent in a 64-bit value.
return 0, errors.New("varint overflow")
}
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
codeL := len(code)
if codeL == 0 {
return nil, errors.New("empty code")
}
br := bufio.NewReaderSize(r, 64*1024)
need := 2 + codeL // TODO: if code too long
prefixBuf := make([]byte, need)
for {
if _, err := br.ReadByte(); err != nil {
return nil, err
}
x, err := decodeVarint(br)
if err != nil {
return nil, err
}
bodyL := int(x)
if bodyL <= 0 {
return nil, errors.New("invalid body length: ", bodyL)
}
prefixL := bodyL
if prefixL > need {
prefixL = need
}
prefix := prefixBuf[:prefixL]
if _, err := io.ReadFull(br, prefix); err != nil {
return nil, err
}
match := false
if bodyL >= need {
if int(prefix[1]) == codeL && bytes.Equal(prefix[2:need], code) {
if !readBody {
return nil, nil
}
match = true
}
}
remain := bodyL - prefixL
if match {
out := make([]byte, bodyL)
copy(out, prefix)
if remain > 0 {
if _, err := io.ReadFull(br, out[prefixL:]); err != nil {
return nil, err
}
}
return out, nil
}
if remain > 0 {
if _, err := br.Discard(remain); err != nil {
return nil, err
}
}
}
}
type AttributeMatcher interface {
Match(*Domain) bool
}
type HasAttrMatcher string
// Match reports whether this matcher matches any attribute on the domain.
func (m HasAttrMatcher) Match(domain *Domain) bool {
for _, attr := range domain.Attribute {
if attr.Key == string(m) {
return true
}
}
return false
}
type AllAttrsMatcher struct {
matchers []AttributeMatcher
}
// Match reports whether the domain matches every matcher in the list.
func (m *AllAttrsMatcher) Match(domain *Domain) bool {
for _, matcher := range m.matchers {
if !matcher.Match(domain) {
return false
}
}
return true
}
func NewAllAttrsMatcher(attrs string) AttributeMatcher {
if attrs == "" {
return nil
}
m := new(AllAttrsMatcher)
for _, attr := range strings.Split(attrs, "@") {
m.matchers = append(m.matchers, HasAttrMatcher(attr))
}
return m
}
func loadSiteWithAttrs(file, code, attrs string) ([]*Domain, error) {
domains, err := loadSite(file, code)
if err != nil {
return nil, err
}
matcher := NewAllAttrsMatcher(attrs)
if matcher == nil {
return domains, nil
}
filtered := make([]*Domain, 0, len(domains))
for _, d := range domains {
if matcher.Match(d) {
filtered = append(filtered, d)
}
}
return filtered, nil
}
@@ -1,8 +1,10 @@
package router package geodata
import ( import (
"context" "context"
"net/netip" "net/netip"
"runtime"
"slices"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -13,7 +15,7 @@ import (
"go4.org/netipx" "go4.org/netipx"
) )
type GeoIPMatcher interface { type IPMatcher interface {
// TODO: (PERF) all net.IP -> netipx.Addr // TODO: (PERF) all net.IP -> netipx.Addr
// Invalid IP always return false. // Invalid IP always return false.
@@ -33,13 +35,13 @@ type GeoIPMatcher interface {
SetReverse(reverse bool) SetReverse(reverse bool)
} }
type GeoIPSet struct { type IPSet struct {
ipv4, ipv6 *netipx.IPSet ipv4, ipv6 *netipx.IPSet
max4, max6 uint8 max4, max6 uint8
} }
type HeuristicGeoIPMatcher struct { type HeuristicIPMatcher struct {
ipset *GeoIPSet ipset *IPSet
reverse bool reverse bool
} }
@@ -48,8 +50,8 @@ type ipBucket struct {
ips []net.IP ips []net.IP
} }
// Match implements GeoIPMatcher. // Match implements IPMatcher.
func (m *HeuristicGeoIPMatcher) Match(ip net.IP) bool { func (m *HeuristicIPMatcher) Match(ip net.IP) bool {
ipx, ok := netipx.FromStdIP(ip) ipx, ok := netipx.FromStdIP(ip)
if !ok { if !ok {
return false return false
@@ -57,18 +59,24 @@ func (m *HeuristicGeoIPMatcher) Match(ip net.IP) bool {
return m.matchAddr(ipx) return m.matchAddr(ipx)
} }
func (m *HeuristicGeoIPMatcher) matchAddr(ipx netip.Addr) bool { func (m *HeuristicIPMatcher) matchAddr(ipx netip.Addr) bool {
if ipx.Is4() { if ipx.Is4() {
if m.ipset.max4 == 0xff {
return false
}
return m.ipset.ipv4.Contains(ipx) != m.reverse return m.ipset.ipv4.Contains(ipx) != m.reverse
} }
if ipx.Is6() { if ipx.Is6() {
if m.ipset.max6 == 0xff {
return false
}
return m.ipset.ipv6.Contains(ipx) != m.reverse return m.ipset.ipv6.Contains(ipx) != m.reverse
} }
return false return false
} }
// AnyMatch implements GeoIPMatcher. // AnyMatch implements IPMatcher.
func (m *HeuristicGeoIPMatcher) AnyMatch(ips []net.IP) bool { func (m *HeuristicIPMatcher) AnyMatch(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -117,8 +125,8 @@ func (m *HeuristicGeoIPMatcher) AnyMatch(ips []net.IP) bool {
return false return false
} }
// Matches implements GeoIPMatcher. // Matches implements IPMatcher.
func (m *HeuristicGeoIPMatcher) Matches(ips []net.IP) bool { func (m *HeuristicIPMatcher) Matches(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -205,8 +213,8 @@ func prefixKeyFromIP(ip net.IP) (key [9]byte, ok bool) {
return key, false // illegal return key, false // illegal
} }
// FilterIPs implements GeoIPMatcher. // FilterIPs implements IPMatcher.
func (m *HeuristicGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { func (m *HeuristicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return []net.IP{}, []net.IP{} return []net.IP{}, []net.IP{}
@@ -295,22 +303,22 @@ func (m *HeuristicGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmat
return return
} }
// ToggleReverse implements GeoIPMatcher. // ToggleReverse implements IPMatcher.
func (m *HeuristicGeoIPMatcher) ToggleReverse() { func (m *HeuristicIPMatcher) ToggleReverse() {
m.reverse = !m.reverse m.reverse = !m.reverse
} }
// SetReverse implements GeoIPMatcher. // SetReverse implements IPMatcher.
func (m *HeuristicGeoIPMatcher) SetReverse(reverse bool) { func (m *HeuristicIPMatcher) SetReverse(reverse bool) {
m.reverse = reverse m.reverse = reverse
} }
type GeneralMultiGeoIPMatcher struct { type GeneralMultiIPMatcher struct {
matchers []GeoIPMatcher matchers []IPMatcher
} }
// Match implements GeoIPMatcher. // Match implements IPMatcher.
func (mm *GeneralMultiGeoIPMatcher) Match(ip net.IP) bool { func (mm *GeneralMultiIPMatcher) Match(ip net.IP) bool {
for _, m := range mm.matchers { for _, m := range mm.matchers {
if m.Match(ip) { if m.Match(ip) {
return true return true
@@ -319,8 +327,8 @@ func (mm *GeneralMultiGeoIPMatcher) Match(ip net.IP) bool {
return false return false
} }
// AnyMatch implements GeoIPMatcher. // AnyMatch implements IPMatcher.
func (mm *GeneralMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool { func (mm *GeneralMultiIPMatcher) AnyMatch(ips []net.IP) bool {
for _, m := range mm.matchers { for _, m := range mm.matchers {
if m.AnyMatch(ips) { if m.AnyMatch(ips) {
return true return true
@@ -329,8 +337,8 @@ func (mm *GeneralMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool {
return false return false
} }
// Matches implements GeoIPMatcher. // Matches implements IPMatcher.
func (mm *GeneralMultiGeoIPMatcher) Matches(ips []net.IP) bool { func (mm *GeneralMultiIPMatcher) Matches(ips []net.IP) bool {
for _, m := range mm.matchers { for _, m := range mm.matchers {
if m.Matches(ips) { if m.Matches(ips) {
return true return true
@@ -339,8 +347,8 @@ func (mm *GeneralMultiGeoIPMatcher) Matches(ips []net.IP) bool {
return false return false
} }
// FilterIPs implements GeoIPMatcher. // FilterIPs implements IPMatcher.
func (mm *GeneralMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { func (mm *GeneralMultiIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
matched = make([]net.IP, 0, len(ips)) matched = make([]net.IP, 0, len(ips))
unmatched = ips unmatched = ips
for _, m := range mm.matchers { for _, m := range mm.matchers {
@@ -356,26 +364,26 @@ func (mm *GeneralMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, u
return return
} }
// ToggleReverse implements GeoIPMatcher. // ToggleReverse implements IPMatcher.
func (mm *GeneralMultiGeoIPMatcher) ToggleReverse() { func (mm *GeneralMultiIPMatcher) ToggleReverse() {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.ToggleReverse() m.ToggleReverse()
} }
} }
// SetReverse implements GeoIPMatcher. // SetReverse implements IPMatcher.
func (mm *GeneralMultiGeoIPMatcher) SetReverse(reverse bool) { func (mm *GeneralMultiIPMatcher) SetReverse(reverse bool) {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.SetReverse(reverse) m.SetReverse(reverse)
} }
} }
type HeuristicMultiGeoIPMatcher struct { type HeuristicMultiIPMatcher struct {
matchers []*HeuristicGeoIPMatcher matchers []*HeuristicIPMatcher
} }
// Match implements GeoIPMatcher. // Match implements IPMatcher.
func (mm *HeuristicMultiGeoIPMatcher) Match(ip net.IP) bool { func (mm *HeuristicMultiIPMatcher) Match(ip net.IP) bool {
ipx, ok := netipx.FromStdIP(ip) ipx, ok := netipx.FromStdIP(ip)
if !ok { if !ok {
return false return false
@@ -389,8 +397,8 @@ func (mm *HeuristicMultiGeoIPMatcher) Match(ip net.IP) bool {
return false return false
} }
// AnyMatch implements GeoIPMatcher. // AnyMatch implements IPMatcher.
func (mm *HeuristicMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool { func (mm *HeuristicMultiIPMatcher) AnyMatch(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -439,8 +447,8 @@ func (mm *HeuristicMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool {
return false return false
} }
// Matches implements GeoIPMatcher. // Matches implements IPMatcher.
func (mm *HeuristicMultiGeoIPMatcher) Matches(ips []net.IP) bool { func (mm *HeuristicMultiIPMatcher) Matches(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -503,7 +511,7 @@ type ipViews struct {
precise4, precise6 []netip.Addr precise4, precise6 []netip.Addr
} }
func (v *ipViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) bool { func (v *ipViews) ensureForMatcher(m *HeuristicIPMatcher, ips []net.IP) bool {
needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil
needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil
needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil
@@ -581,8 +589,8 @@ func (v *ipViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) bool
return true return true
} }
// FilterIPs implements GeoIPMatcher. // FilterIPs implements IPMatcher.
func (mm *HeuristicMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { func (mm *HeuristicMultiIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return []net.IP{}, []net.IP{} return []net.IP{}, []net.IP{}
@@ -694,7 +702,7 @@ type ipBucketViews struct {
precise4, precise6 map[netip.Addr]net.IP precise4, precise6 map[netip.Addr]net.IP
} }
func (v *ipBucketViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) { func (v *ipBucketViews) ensureForMatcher(m *HeuristicIPMatcher, ips []net.IP) {
needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil
needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil
needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil
@@ -782,70 +790,123 @@ func (v *ipBucketViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP)
} }
} }
// ToggleReverse implements GeoIPMatcher. // ToggleReverse implements IPMatcher.
func (mm *HeuristicMultiGeoIPMatcher) ToggleReverse() { func (mm *HeuristicMultiIPMatcher) ToggleReverse() {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.ToggleReverse() m.ToggleReverse()
} }
} }
// SetReverse implements GeoIPMatcher. // SetReverse implements IPMatcher.
func (mm *HeuristicMultiGeoIPMatcher) SetReverse(reverse bool) { func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.SetReverse(reverse) m.SetReverse(reverse)
} }
} }
type GeoIPSetFactory struct { type IPSetFactory struct {
sync.Mutex sync.Mutex
shared map[string]*GeoIPSet // TODO: cleanup shared map[string]*IPSet // TODO: cleanup
} }
var ipsetFactory = GeoIPSetFactory{shared: make(map[string]*GeoIPSet)} func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) {
key := buildGeoIPRulesKey(rules)
func (f *GeoIPSetFactory) GetOrCreate(key string, cidrGroups [][]*CIDR) (*GeoIPSet, error) {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
if ipset := f.shared[key]; ipset != nil { if ipset := f.shared[key]; ipset != nil {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil return ipset, nil
} }
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.Create(cidrGroups...) ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules {
cidrs, err := loadIP(r.File, r.Code)
if err != nil {
return err
}
for i, c := range cidrs {
add(c)
cidrs[i] = nil // peak mem
}
}
return nil
})
if err == nil { if err == nil {
f.shared[key] = ipset f.shared[key] = ipset
} }
return ipset, err return ipset, err
} }
func (f *GeoIPSetFactory) Create(cidrGroups ...[]*CIDR) (*GeoIPSet, error) { func buildGeoIPRulesKey(rules []*GeoIPRule) string {
var ipv4Builder, ipv6Builder netipx.IPSetBuilder rules = slices.Clone(rules)
for _, cidrGroup := range cidrGroups { sort.Slice(rules, func(i, j int) bool {
for i, cidrEntry := range cidrGroup { ri, rj := rules[i], rules[j]
cidrGroup[i] = nil if ri.File != rj.File {
ipBytes := cidrEntry.GetIp() return ri.File < rj.File
prefixLen := int(cidrEntry.GetPrefix()) }
return ri.Code < rj.Code
})
addr, ok := netip.AddrFromSlice(ipBytes) var sb strings.Builder
if !ok { sb.Grow(len(rules) * 20) // geoip.dat:xx,
errors.LogError(context.Background(), "ignore invalid IP byte slice: ", ipBytes) var last *GeoIPRule
continue for i, r := range rules {
} if i == 0 || (r.File != last.File || r.Code != last.Code) {
last = r
prefix := netip.PrefixFrom(addr, prefixLen) sb.WriteString(r.File)
if !prefix.IsValid() { sb.WriteString(":")
errors.LogError(context.Background(), "ignore created invalid prefix from addr ", addr, " and length ", prefixLen) sb.WriteString(r.Code)
continue sb.WriteString(",")
}
if addr.Is4() {
ipv4Builder.AddPrefix(prefix)
} else if addr.Is6() {
ipv6Builder.AddPrefix(prefix)
}
} }
} }
return sb.String()
}
func (f *IPSetFactory) CreateFromCIDRs(cidrs []*CIDR) (*IPSet, error) {
return f.createFrom(func(add func(*CIDR)) error {
for _, c := range cidrs {
add(c)
}
return nil
})
}
func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error) {
var ipv4Builder, ipv6Builder netipx.IPSetBuilder
err := yield(func(c *CIDR) {
ipBytes := c.GetIp()
prefixLen := int(c.GetPrefix())
addr, ok := netip.AddrFromSlice(ipBytes)
if !ok {
errors.LogError(context.Background(), "ignore invalid IP byte slice: ", ipBytes)
return
}
prefix := netip.PrefixFrom(addr, prefixLen)
if !prefix.IsValid() {
errors.LogError(context.Background(), "ignore created invalid prefix from addr ", addr, " and length ", prefixLen)
return
}
if addr.Is4() {
ipv4Builder.AddPrefix(prefix)
} else if addr.Is6() {
ipv6Builder.AddPrefix(prefix)
}
})
if err != nil {
return nil, err
}
// peak mem
runtime.GC()
defer runtime.GC()
ipv4, err := ipv4Builder.IPSet() ipv4, err := ipv4Builder.IPSet()
if err != nil { if err != nil {
@@ -856,107 +917,106 @@ func (f *GeoIPSetFactory) Create(cidrGroups ...[]*CIDR) (*GeoIPSet, error) {
return nil, errors.New("failed to build IPv6 set").Base(err) return nil, errors.New("failed to build IPv6 set").Base(err)
} }
var has4, has6 bool
var max4, max6 int var max4, max6 int
for _, p := range ipv4.Prefixes() { for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 { if b := p.Bits(); b > max4 {
max4 = b max4 = b
} }
} }
for _, p := range ipv6.Prefixes() { for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 { if b := p.Bits(); b > max6 {
max6 = b max6 = b
} }
} }
if max4 == 0 { if !has4 {
max4 = 0xff max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
} }
if max6 == 0 { if !has6 {
max6 = 0xff max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
} }
return &GeoIPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
} }
func BuildOptimizedGeoIPMatcher(geoips ...*GeoIP) (GeoIPMatcher, error) { func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) {
n := len(geoips) n := len(rules)
if n == 0 { posCustom := make([]*CIDR, 0, n)
return nil, errors.New("no geoip configs provided") negCustom := make([]*CIDR, 0, n)
} posGeoip := make([]*GeoIPRule, 0, n)
negGeoip := make([]*GeoIPRule, 0, n)
var subs []*HeuristicGeoIPMatcher for _, r := range rules {
pos := make([]*GeoIP, 0, n) switch v := r.Value.(type) {
neg := make([]*GeoIP, 0, n/2) case *IPRule_Custom:
if !v.Custom.ReverseMatch {
for _, geoip := range geoips { posCustom = append(posCustom, v.Custom.Cidr)
if geoip == nil { } else {
return nil, errors.New("geoip entry is nil") negCustom = append(negCustom, v.Custom.Cidr)
}
if geoip.CountryCode == "" {
ipset, err := ipsetFactory.Create(geoip.Cidr)
if err != nil {
return nil, err
} }
subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: geoip.ReverseMatch}) case *IPRule_Geoip:
continue if !v.Geoip.ReverseMatch {
} posGeoip = append(posGeoip, v.Geoip)
if !geoip.ReverseMatch { } else {
pos = append(pos, geoip) negGeoip = append(negGeoip, v.Geoip)
} else {
neg = append(neg, geoip)
}
}
buildIPSet := func(mergeables []*GeoIP) (*GeoIPSet, error) {
n := len(mergeables)
if n == 0 {
return nil, nil
}
sort.Slice(mergeables, func(i, j int) bool {
gi, gj := mergeables[i], mergeables[j]
return gi.CountryCode < gj.CountryCode
})
var sb strings.Builder
sb.Grow(n * 3) // xx,
cidrGroups := make([][]*CIDR, 0, n)
var last *GeoIP
for i, geoip := range mergeables {
if i == 0 || (geoip.CountryCode != last.CountryCode) {
last = geoip
sb.WriteString(geoip.CountryCode)
sb.WriteString(",")
cidrGroups = append(cidrGroups, geoip.Cidr)
} }
default:
panic("unknown ip rule type")
} }
return ipsetFactory.GetOrCreate(sb.String(), cidrGroups)
} }
ipset, err := buildIPSet(pos) subs := make([]*HeuristicIPMatcher, 0, 4)
if err != nil {
return nil, err if len(posCustom) > 0 {
} ipset, err := f.CreateFromCIDRs(posCustom)
if ipset != nil { if err != nil {
subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: false}) return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
} }
ipset, err = buildIPSet(neg) if len(negCustom) > 0 {
if err != nil { ipset, err := f.CreateFromCIDRs(negCustom)
return nil, err if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
} }
if ipset != nil {
subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: true}) if len(posGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
}
if len(negGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
} }
switch len(subs) { switch len(subs) {
case 0: case 0:
return nil, errors.New("no valid geoip matcher") return nil, errors.New("no valid ip matcher")
case 1: case 1:
return subs[0], nil return subs[0], nil
default: default:
return &HeuristicMultiGeoIPMatcher{matchers: subs}, nil return &HeuristicMultiIPMatcher{matchers: subs}, nil
} }
} }
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: make(map[string]*IPSet)}
}
+437
View File
@@ -0,0 +1,437 @@
package geodata
import (
"net"
"path/filepath"
"reflect"
"slices"
"testing"
"github.com/xtls/xray-core/common"
xnet "github.com/xtls/xray-core/common/net"
)
func buildIPMatcher(rawRules ...string) IPMatcher {
rules, err := ParseIPRules(rawRules)
common.Must(err)
matcher, err := newIPRegistry().BuildIPMatcher(rules)
common.Must(err)
return matcher
}
func sortIPStrings(ips []net.IP) []string {
output := make([]string, 0, len(ips))
for _, ip := range ips {
output = append(output, ip.String())
}
slices.Sort(output)
return output
}
func TestIPMatcher(t *testing.T) {
matcher := buildIPMatcher(
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.168.0.0/16",
"192.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"8.8.8.8/32",
"91.108.4.0/16",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "192.0.0.0",
Output: true,
},
{
Input: "192.0.1.0",
Output: false,
},
{
Input: "0.1.0.0",
Output: true,
},
{
Input: "1.0.0.1",
Output: false,
},
{
Input: "8.8.8.7",
Output: false,
},
{
Input: "8.8.8.8",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
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 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",
"98.108.20.0/23",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "98.108.22.11",
Output: true,
},
{
Input: "98.108.25.0",
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 TestIPReverseMatcher(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"91.108.4.0/16",
)
matcher.SetReverse(true)
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
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 TestIPReverseMatcher2(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"91.108.4.0/16",
"fe80::", // Keep IPv6 family non-empty so reverse matching can evaluate IPv6 input.
)
matcher.SetReverse(true)
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "91.108.255.254",
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 TestIPCustomReverseMatcher(t *testing.T) {
matcher := buildIPMatcher("!8.8.8.8/32")
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "1.1.1.1",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"2001:4860:4860::8888/128",
)
ip := func(raw string) net.IP {
return xnet.ParseAddress(raw).IP()
}
if matcher.AnyMatch(nil) {
t.Fatal("expect AnyMatch(nil) to be false")
}
if !matcher.AnyMatch([]net.IP{
net.IP{},
ip("1.1.1.1"),
ip("8.8.8.8"),
}) {
t.Fatal("expect AnyMatch to ignore invalid IPs and return true when one valid IP matches")
}
if matcher.AnyMatch([]net.IP{
ip("1.1.1.1"),
ip("2001:db8::1"),
}) {
t.Fatal("expect AnyMatch to be false when no valid IP matches")
}
if !matcher.Matches([]net.IP{
ip("8.8.8.8"),
ip("2001:4860:4860::8888"),
}) {
t.Fatal("expect Matches to be true when all valid IPs match")
}
if matcher.Matches([]net.IP{
ip("8.8.8.8"),
ip("1.1.1.1"),
}) {
t.Fatal("expect Matches to be false when one valid IP does not match")
}
if matcher.Matches([]net.IP{
ip("8.8.8.8"),
net.IP{},
}) {
t.Fatal("expect Matches to be false when any IP is invalid")
}
}
func TestIPMatcherFilterIPs(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"91.108.4.0/16",
"2001:4860:4860::8888/128",
)
ip := func(raw string) net.IP {
return xnet.ParseAddress(raw).IP()
}
matched, unmatched := matcher.FilterIPs([]net.IP{
net.IP{},
ip("8.8.8.8"),
ip("91.108.255.254"),
ip("1.1.1.1"),
ip("2001:4860:4860::8888"),
ip("2001:db8::1"),
})
wantMatched := []string{
"2001:4860:4860::8888",
"8.8.8.8",
"91.108.255.254",
}
slices.Sort(wantMatched)
if v := sortIPStrings(matched); !reflect.DeepEqual(v, wantMatched) {
t.Error("unexpected output: ", v, " want ", wantMatched)
}
wantUnmatched := []string{
"1.1.1.1",
"2001:db8::1",
}
slices.Sort(wantUnmatched)
if v := sortIPStrings(unmatched); !reflect.DeepEqual(v, wantUnmatched) {
t.Error("unexpected output: ", v, " want ", wantUnmatched)
}
}
func TestIPMatcher4CN(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:cn")
if matcher.Match([]byte{8, 8, 8, 8}) {
t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does")
}
}
func TestIPMatcher6US(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:us")
if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
}
}
func BenchmarkIPMatcher4CN(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:cn")
ip := net.IP{8, 8, 8, 8}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(ip)
}
}
func BenchmarkIPMatcher6US(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:us")
ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(ip)
}
}
+135
View File
@@ -0,0 +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) {
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: 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
}
+265
View File
@@ -0,0 +1,265 @@
package geodata
import (
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
const (
DefaultGeoIPDat = "geoip.dat"
DefaultGeoSiteDat = "geosite.dat"
)
func ParseIPRules(rules []string) ([]*IPRule, error) {
var ipRules []*IPRule
for i, r := range rules {
r, reverse := cutReversePrefix(r)
if strings.HasPrefix(r, "geoip:") {
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-ip:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
}
}
var rule isIPRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoIPRule(r[prefix:], reverse)
} else {
rule, err = parseCustomIPRule(r, reverse)
}
if err != nil {
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
}
ipRules = append(ipRules, &IPRule{Value: rule})
}
return ipRules, nil
}
func cutReversePrefix(s string) (string, bool) {
reverse := false
for strings.HasPrefix(s, "!") {
s = s[1:]
reverse = !reverse
}
return s, reverse
}
func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
file, code, ok := strings.Cut(rule, ":")
if !ok {
return nil, errors.New("syntax error")
}
if file == "" {
return nil, errors.New("empty file")
}
code, codeReverse := cutReversePrefix(code)
reverse = reverse != codeReverse
if code == "" {
return nil, errors.New("empty code")
}
code = strings.ToUpper(code)
if err := checkFile(file, code); err != nil {
return nil, err
}
return &IPRule_Geoip{
Geoip: &GeoIPRule{
File: file,
Code: code,
ReverseMatch: reverse,
},
}, nil
}
func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
cidr, err := parseCIDR(rule)
if err != nil {
return nil, err
}
return &IPRule_Custom{
Custom: &CIDRRule{
Cidr: cidr,
ReverseMatch: reverse,
},
}, nil
}
func parseCIDR(s string) (*CIDR, error) {
ipStr, prefixStr, _ := strings.Cut(s, "/")
ipAddr := net.ParseAddress(ipStr)
var maxPrefix uint32
switch ipAddr.Family() {
case net.AddressFamilyIPv4:
maxPrefix = 32
case net.AddressFamilyIPv6:
maxPrefix = 128
default:
return nil, errors.New("unsupported address family")
}
prefixBits := maxPrefix
if prefixStr != "" {
parsedPrefix, err := strconv.ParseUint(prefixStr, 10, 32)
if err != nil {
return nil, errors.New("invalid CIDR prefix length: ", prefixStr).Base(err)
}
prefixBits = uint32(parsedPrefix)
}
if prefixBits > maxPrefix {
return nil, errors.New("CIDR prefix length ", prefixBits, " exceeds max ", maxPrefix)
}
return &CIDR{
Ip: []byte(ipAddr.IP()),
Prefix: prefixBits,
}, nil
}
func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
if strings.HasPrefix(r, "geosite:") {
r = "ext:" + DefaultGeoSiteDat + ":" + r[len("geosite:"):]
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
}
}
var rule isDomainRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoSiteRule(r[prefix:])
} else {
rule, err = parseCustomDomainRule(r, defaultType)
}
if err != nil {
return nil, errors.New("illegal domain rule: ", r).Base(err)
}
return &DomainRule{Value: rule}, nil
}
func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, error) {
var domainRules []*DomainRule
for i, r := range rules {
if strings.HasPrefix(r, "geosite:") {
r = "ext:" + DefaultGeoSiteDat + ":" + r[len("geosite:"):]
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
}
}
var rule isDomainRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoSiteRule(r[prefix:])
} else {
rule, err = parseCustomDomainRule(r, defaultType)
}
if err != nil {
return nil, errors.New("illegal domain rule: ", rules[i]).Base(err)
}
domainRules = append(domainRules, &DomainRule{Value: rule})
}
return domainRules, nil
}
func parseGeoSiteRule(rule string) (*DomainRule_Geosite, error) {
file, codeWithAttrs, ok := strings.Cut(rule, ":")
if !ok {
return nil, errors.New("syntax error")
}
if file == "" {
return nil, errors.New("empty file")
}
if strings.HasSuffix(codeWithAttrs, "@") || strings.Contains(codeWithAttrs, "@@") {
return nil, errors.New("empty attr")
}
code, attrs, _ := strings.Cut(codeWithAttrs, "@")
if code == "" {
return nil, errors.New("empty code")
}
code = strings.ToUpper(code)
if err := checkFile(file, code); err != nil {
return nil, err
}
return &DomainRule_Geosite{
Geosite: &GeoSiteRule{
File: file,
Code: code,
Attrs: strings.ToLower(attrs),
},
}, nil
}
func parseCustomDomainRule(rule string, defaultType Domain_Type) (*DomainRule_Custom, error) {
domain := new(Domain)
switch {
case strings.HasPrefix(rule, "regexp:"):
domain.Type = Domain_Regex
domain.Value = rule[7:]
case strings.HasPrefix(rule, "domain:"):
domain.Type = Domain_Domain
domain.Value = rule[7:]
case strings.HasPrefix(rule, "full:"):
domain.Type = Domain_Full
domain.Value = rule[5:]
case strings.HasPrefix(rule, "keyword:"):
domain.Type = Domain_Substr
domain.Value = rule[8:]
case strings.HasPrefix(rule, "dotless:"):
domain.Type = Domain_Regex
switch substr := rule[8:]; {
case substr == "":
domain.Value = "^[^.]*$"
case !strings.Contains(substr, "."):
domain.Value = "^[^.]*" + substr + "[^.]*$"
default:
return nil, errors.New("substr in dotless rule should not contain a dot")
}
default:
domain.Type = defaultType
domain.Value = rule
}
return &DomainRule_Custom{
Custom: domain,
}, nil
}
+106
View File
@@ -0,0 +1,106 @@
package geodata_test
import (
"path/filepath"
"testing"
"github.com/xtls/xray-core/common/geodata"
)
func TestParseIPRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{
"geoip:us",
"geoip:cn",
"!geoip:cn",
"!!geoip:cn",
"geoip:!cn",
"geoip:!!cn",
"!geoip:!cn",
"ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn",
"ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ca",
"192.168.0.0/24",
"!192.168.0.0/24",
"!!192.168.0.0/24",
"!!!192.168.0.0/24",
"192.168.0.1",
"fe80::/64",
"fe80::",
}
_, err := geodata.ParseIPRules(rules)
if err != nil {
t.Fatalf("Failed to parse ip rules, got %s", err)
}
}
func TestParseIPRuleReverse(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, tt := range []struct {
rule string
reverse bool
}{
{rule: "!192.168.0.0/24", reverse: true},
{rule: "!!192.168.0.0/24", reverse: false},
{rule: "!!!192.168.0.0/24", reverse: true},
{rule: "!!!!192.168.0.0/24", reverse: false},
{rule: "geoip:cn", reverse: false},
{rule: "!geoip:cn", reverse: true},
{rule: "!!geoip:cn", reverse: false},
{rule: "geoip:!cn", reverse: true},
{rule: "geoip:!!cn", reverse: false},
{rule: "!geoip:!cn", reverse: false},
{rule: "!!geoip:!cn", reverse: true},
{rule: "!geoip:!!cn", reverse: true},
{rule: "ext:geoip.dat:!!!cn", reverse: true},
} {
t.Run(tt.rule, func(t *testing.T) {
rules, err := geodata.ParseIPRules([]string{tt.rule})
if err != nil {
t.Fatalf("Failed to parse ip rules, got %s", err)
}
if len(rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(rules))
}
switch rule := rules[0]; {
case rule.GetGeoip() != nil:
if rule.GetGeoip().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected geoip reverse match to be %t", tt.reverse)
}
case rule.GetCustom() != nil:
if rule.GetCustom().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected custom reverse match to be %t", tt.reverse)
}
default:
t.Fatal("Expected ip rule")
}
})
}
}
func TestParseDomainRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{
"geosite:cn",
"geosite:geolocation-!cn",
"geosite:cn@!cn",
"ext:geosite.dat:geolocation-!cn",
"ext:geosite.dat:cn@!cn",
"ext-site:geosite.dat:geolocation-!cn",
"ext-site:geosite.dat:cn@!cn",
"domain:google.com",
}
_, err := geodata.ParseDomainRules(rules, geodata.Domain_Domain)
if err != nil {
t.Fatalf("Failed to parse domain rules, got %s", err)
}
}
@@ -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)
}
@@ -0,0 +1,58 @@
package strmatcher_test
import (
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func BenchmarkLinearIndexMatcher(b *testing.B) {
benchmarkIndexMatcher(b, func() IndexMatcher {
return NewLinearIndexMatcher()
})
}
func BenchmarkMphIndexMatcher(b *testing.B) {
benchmarkIndexMatcher(b, func() IndexMatcher {
return NewMphIndexMatcher()
})
}
func benchmarkIndexMatcher(b *testing.B, ctor func() IndexMatcher) {
b.Run("Match", func(b *testing.B) {
b.Run("Domain------------", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true})
})
b.Run("Domain+Full-------", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true})
})
b.Run("Domain+Full+Substr", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true})
})
b.Run("All-Fail----------", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false})
})
})
b.Run("Match/Dotless", func(b *testing.B) { // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used.
b.Run("All-Succ", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true, Regex: true})
})
b.Run("All-Fail", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false, Regex: false})
})
})
b.Run("MatchAny", func(b *testing.B) {
b.Run("First-Full--", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: true, Domain: true, Substr: true})
})
b.Run("First-Domain", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: true, Substr: true})
})
b.Run("First-Substr", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: true})
})
b.Run("All-Fail----", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: false})
})
})
}
@@ -0,0 +1,149 @@
package strmatcher_test
import (
"strconv"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func BenchmarkFullMatcher(b *testing.B) {
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return new(SimpleMatcherGroup)
})
})
b.Run("FullMatcherGroup--------", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return NewFullMatcherGroup()
})
})
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return NewACAutomatonMatcherGroup()
})
})
b.Run("MphMatcherGroup---------", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return NewMphMatcherGroup()
})
})
}
func BenchmarkDomainMatcher(b *testing.B) {
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return new(SimpleMatcherGroup)
})
})
b.Run("DomainMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return NewDomainMatcherGroup()
})
})
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return NewACAutomatonMatcherGroup()
})
})
b.Run("MphMatcherGroup---------", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return NewMphMatcherGroup()
})
})
}
func BenchmarkSubstrMatcher(b *testing.B) {
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Substr, func() MatcherGroup {
return new(SimpleMatcherGroup)
})
})
b.Run("SubstrMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Substr, func() MatcherGroup {
return new(SubstrMatcherGroup)
})
})
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
benchmarkMatcherType(b, Substr, func() MatcherGroup {
return NewACAutomatonMatcherGroup()
})
})
}
// Utility functions for benchmark
func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) {
b.Run("Match", func(b *testing.B) {
b.Run("Succ", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{t: true})
})
b.Run("Fail", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{t: false})
})
})
b.Run("MatchAny", func(b *testing.B) {
b.Run("Succ", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{t: true})
})
b.Run("Fail", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{t: false})
})
})
}
func benchmarkMatch(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) {
prepareMatchers(g, enabledTypes)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.Match("0.example.com")
}
}
func benchmarkMatchAny(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) {
prepareMatchers(g, enabledTypes)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.MatchAny("0.example.com")
}
}
func prepareMatchers(g MatcherGroup, enabledTypes map[Type]bool) {
for matcherType, hasMatch := range enabledTypes {
switch matcherType {
case Domain:
if hasMatch {
AddMatcherToGroup(g, DomainMatcher("example.com"), 0)
}
for i := 1; i < 1024; i++ {
AddMatcherToGroup(g, DomainMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
}
case Full:
if hasMatch {
AddMatcherToGroup(g, FullMatcher("0.example.com"), 0)
}
for i := 1; i < 64; i++ {
AddMatcherToGroup(g, FullMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
}
case Substr:
if hasMatch {
AddMatcherToGroup(g, SubstrMatcher("example.com"), 0)
}
for i := 1; i < 4; i++ {
AddMatcherToGroup(g, SubstrMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
}
case Regex:
matcher, err := Regex.New("^[^.]*$") // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used.
common.Must(err)
AddMatcherToGroup(g, matcher, 0)
}
}
if g, ok := g.(buildable); ok {
common.Must(g.Build())
}
}
type buildable interface {
Build() error
}
@@ -0,0 +1,96 @@
package strmatcher
// LinearIndexMatcher is an implementation of IndexMatcher.
type LinearIndexMatcher struct {
count uint32
full *FullMatcherGroup
domain *DomainMatcherGroup
substr *SubstrMatcherGroup
regex *SimpleMatcherGroup
}
func NewLinearIndexMatcher() *LinearIndexMatcher {
return new(LinearIndexMatcher)
}
// Add implements IndexMatcher.Add.
func (g *LinearIndexMatcher) Add(matcher Matcher) uint32 {
g.count++
index := g.count
switch matcher := matcher.(type) {
case FullMatcher:
if g.full == nil {
g.full = NewFullMatcherGroup()
}
g.full.AddFullMatcher(matcher, index)
case DomainMatcher:
if g.domain == nil {
g.domain = NewDomainMatcherGroup()
}
g.domain.AddDomainMatcher(matcher, index)
case SubstrMatcher:
if g.substr == nil {
g.substr = new(SubstrMatcherGroup)
}
g.substr.AddSubstrMatcher(matcher, index)
default:
if g.regex == nil {
g.regex = new(SimpleMatcherGroup)
}
g.regex.AddMatcher(matcher, index)
}
return index
}
// Build implements IndexMatcher.Build.
func (*LinearIndexMatcher) Build() error {
return nil
}
// Match implements IndexMatcher.Match.
func (g *LinearIndexMatcher) Match(input string) []uint32 {
// Allocate capacity to prevent matches escaping to heap
result := make([][]uint32, 0, 5)
if g.full != nil {
if matches := g.full.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.domain != nil {
if matches := g.domain.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.substr != nil {
if matches := g.substr.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.regex != nil {
if matches := g.regex.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
return CompositeMatches(result)
}
// MatchAny implements IndexMatcher.MatchAny.
func (g *LinearIndexMatcher) MatchAny(input string) bool {
if g.full != nil && g.full.MatchAny(input) {
return true
}
if g.domain != nil && g.domain.MatchAny(input) {
return true
}
if g.substr != nil && g.substr.MatchAny(input) {
return true
}
return g.regex != nil && g.regex.MatchAny(input)
}
// Size implements IndexMatcher.Size.
func (g *LinearIndexMatcher) Size() uint32 {
return g.count
}
@@ -0,0 +1,95 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
func TestLinearIndexMatcher(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
{
Type: Regex,
Domain: "apis\\.us$",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4, 2, 6},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4, 2, 6},
},
{
Input: "testapis.us",
Output: []uint32{2, 6, 1},
},
{
Input: "example.com",
Output: []uint32{10, 4},
},
}
matcherGroup := NewLinearIndexMatcher()
for _, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
matcherGroup.Add(matcher)
}
matcherGroup.Build()
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
@@ -0,0 +1,100 @@
package strmatcher
import "runtime"
// A MphIndexMatcher is divided into three parts:
// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table;
// 2. `substr` patterns are matched by ac automaton;
// 3. `regex` patterns are matched with the regex library.
type MphIndexMatcher struct {
count uint32
mph *MphMatcherGroup
ac *ACAutomatonMatcherGroup
regex *SimpleMatcherGroup
}
func NewMphIndexMatcher() *MphIndexMatcher {
return new(MphIndexMatcher)
}
// Add implements IndexMatcher.Add.
func (g *MphIndexMatcher) Add(matcher Matcher) uint32 {
g.count++
index := g.count
switch matcher := matcher.(type) {
case FullMatcher:
if g.mph == nil {
g.mph = NewMphMatcherGroup()
}
g.mph.AddFullMatcher(matcher, index)
case DomainMatcher:
if g.mph == nil {
g.mph = NewMphMatcherGroup()
}
g.mph.AddDomainMatcher(matcher, index)
case SubstrMatcher:
if g.ac == nil {
g.ac = NewACAutomatonMatcherGroup()
}
g.ac.AddSubstrMatcher(matcher, index)
case *RegexMatcher:
if g.regex == nil {
g.regex = &SimpleMatcherGroup{}
}
g.regex.AddMatcher(matcher, index)
}
return index
}
// Build implements IndexMatcher.Build.
func (g *MphIndexMatcher) Build() error {
if g.mph != nil {
runtime.GC() // peak mem
g.mph.Build()
}
runtime.GC() // peak mem
if g.ac != nil {
g.ac.Build()
runtime.GC() // peak mem
}
return nil
}
// Match implements IndexMatcher.Match.
func (g *MphIndexMatcher) Match(input string) []uint32 {
result := make([][]uint32, 0, 5)
if g.mph != nil {
if matches := g.mph.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.ac != nil {
if matches := g.ac.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.regex != nil {
if matches := g.regex.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
return CompositeMatches(result)
}
// MatchAny implements IndexMatcher.MatchAny.
func (g *MphIndexMatcher) MatchAny(input string) bool {
if g.mph != nil && g.mph.MatchAny(input) {
return true
}
if g.ac != nil && g.ac.MatchAny(input) {
return true
}
return g.regex != nil && g.regex.MatchAny(input)
}
// Size implements IndexMatcher.Size.
func (g *MphIndexMatcher) Size() uint32 {
return g.count
}
@@ -0,0 +1,94 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestMphIndexMatcher(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
{
Type: Regex,
Domain: "apis\\.us$",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4, 2, 6},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4, 2, 6},
},
{
Input: "testapis.us",
Output: []uint32{2, 6, 1},
},
{
Input: "example.com",
Output: []uint32{10, 4},
},
}
matcherGroup := NewMphIndexMatcher()
for _, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
matcherGroup.Add(matcher)
}
matcherGroup.Build()
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
@@ -0,0 +1,282 @@
package strmatcher
import (
"container/list"
)
const (
acValidCharCount = 39 // aA-zZ (26), 0-9 (10), - (1), . (1), invalid(1)
acMatchTypeCount = 3 // Full, Domain and Substr
)
type acEdge byte
const (
acTrieEdge acEdge = 1
acFailEdge acEdge = 0
)
type acNode struct {
next [acValidCharCount]uint32 // EdgeIdx -> Next NodeIdx (Next trie node or fail node)
edge [acValidCharCount]acEdge // EdgeIdx -> Trie Edge / Fail Edge
fail uint32 // NodeIdx of *next matched* Substr Pattern on its fail path
match uint32 // MatchIdx of matchers registered on this node, 0 indicates no match
} // Sizeof acNode: (4+1)*acValidCharCount + <padding> + 4 + 4
type acValue [acMatchTypeCount][]uint32 // MatcherType -> Registered Matcher Values
// ACAutoMationMatcherGroup is an implementation of MatcherGroup.
// It uses an AC Automata to provide support for Full, Domain and Substr matcher. Trie node is char based.
//
// NOTICE: ACAutomatonMatcherGroup currently uses a restricted charset (LDH Subset),
// upstream should manually in a way to ensure all patterns and inputs passed to it to be in this charset.
type ACAutomatonMatcherGroup struct {
nodes []acNode // NodeIdx -> acNode
values []acValue // MatchIdx -> acValue
}
func NewACAutomatonMatcherGroup() *ACAutomatonMatcherGroup {
ac := new(ACAutomatonMatcherGroup)
ac.addNode() // Create root node (NodeIdx 0)
ac.addMatchEntry() // Create sentinel match entry (MatchIdx 0)
return ac
}
// AddFullMatcher implements MatcherGroupForFull.AddFullMatcher.
func (ac *ACAutomatonMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
ac.addPattern(0, matcher.Pattern(), matcher.Type(), value)
}
// AddDomainMatcher implements MatcherGroupForDomain.AddDomainMatcher.
func (ac *ACAutomatonMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
node := ac.addPattern(0, matcher.Pattern(), matcher.Type(), value) // For full domain match
ac.addPattern(node, ".", matcher.Type(), value) // For partial domain match
}
// AddSubstrMatcher implements MatcherGroupForSubstr.AddSubstrMatcher.
func (ac *ACAutomatonMatcherGroup) AddSubstrMatcher(matcher SubstrMatcher, value uint32) {
ac.addPattern(0, matcher.Pattern(), matcher.Type(), value)
}
func (ac *ACAutomatonMatcherGroup) addPattern(nodeIdx uint32, pattern string, matcherType Type, value uint32) uint32 {
node := &ac.nodes[nodeIdx]
for i := len(pattern) - 1; i >= 0; i-- {
edgeIdx := acCharset[pattern[i]]
nextIdx := node.next[edgeIdx]
if nextIdx == 0 { // Add new Trie Edge
nextIdx = ac.addNode()
ac.nodes[nodeIdx].next[edgeIdx] = nextIdx
ac.nodes[nodeIdx].edge[edgeIdx] = acTrieEdge
}
nodeIdx = nextIdx
node = &ac.nodes[nodeIdx]
}
if node.match == 0 { // Add new match entry
node.match = ac.addMatchEntry()
}
ac.values[node.match][matcherType] = append(ac.values[node.match][matcherType], value)
return nodeIdx
}
func (ac *ACAutomatonMatcherGroup) addNode() uint32 {
ac.nodes = append(ac.nodes, acNode{})
return uint32(len(ac.nodes) - 1)
}
func (ac *ACAutomatonMatcherGroup) addMatchEntry() uint32 {
ac.values = append(ac.values, acValue{})
return uint32(len(ac.values) - 1)
}
func (ac *ACAutomatonMatcherGroup) Build() error {
fail := make([]uint32, len(ac.nodes))
queue := list.New()
for edgeIdx := 0; edgeIdx < acValidCharCount; edgeIdx++ {
if nextIdx := ac.nodes[0].next[edgeIdx]; nextIdx != 0 {
queue.PushBack(nextIdx)
}
}
for {
front := queue.Front()
if front == nil {
break
}
queue.Remove(front)
nodeIdx := front.Value.(uint32)
node := &ac.nodes[nodeIdx] // Current node
failNode := &ac.nodes[fail[nodeIdx]] // Fail node of currrent node
for edgeIdx := 0; edgeIdx < acValidCharCount; edgeIdx++ {
nodeIdx := node.next[edgeIdx] // Next node through trie edge
failIdx := failNode.next[edgeIdx] // Next node through fail edge
if nodeIdx != 0 {
queue.PushBack(nodeIdx)
fail[nodeIdx] = failIdx
if match := ac.nodes[failIdx].match; match != 0 && len(ac.values[match][Substr]) > 0 { // Fail node is a Substr match node
ac.nodes[nodeIdx].fail = failIdx
} else { // Use path compression to reduce fail path to only contain match nodes
ac.nodes[nodeIdx].fail = ac.nodes[failIdx].fail
}
} else { // Add new fail edge
node.next[edgeIdx] = failIdx
node.edge[edgeIdx] = acFailEdge
}
}
}
return nil
}
// Match implements MatcherGroup.Match.
func (ac *ACAutomatonMatcherGroup) Match(input string) []uint32 {
suffixMatches := make([][]uint32, 0, 5)
substrMatches := make([][]uint32, 0, 5)
fullMatch := true // fullMatch indicates no fail edge traversed so far.
node := &ac.nodes[0] // start from root node.
// 1. the match string is all through trie edge. FULL MATCH or DOMAIN
// 2. the match string is through a fail edge. NOT FULL MATCH
// 2.1 Through a fail edge, but there exists a valid node. SUBSTR
for i := len(input) - 1; i >= 0; i-- {
edge := acCharset[input[i]]
fullMatch = fullMatch && (node.edge[edge] == acTrieEdge)
node = &ac.nodes[node.next[edge]] // Advance to next node
// When entering a new node, traverse the fail path to find all possible Substr patterns:
// 1. The fail path is compressed to only contains match nodes and root node (for terminate condition).
// 2. node.fail != 0 is added here for better performance (as shown by benchmark), possibly it helps branch prediction.
if node.fail != 0 {
for failIdx, failNode := node.fail, &ac.nodes[node.fail]; failIdx != 0; failIdx, failNode = failNode.fail, &ac.nodes[failIdx] {
substrMatches = append(substrMatches, ac.values[failNode.match][Substr])
}
}
// When entering a new node, check whether this node is a match.
// For Substr matchers:
// 1. Matched in any situation, whether a failNode edge is traversed or not.
// For Domain matchers:
// 1. Should not traverse any fail edge (fullMatch).
// 2. Only check on dot separator (input[i] == '.').
if node.match != 0 {
values := ac.values[node.match]
if len(values[Substr]) > 0 {
substrMatches = append(substrMatches, values[Substr])
}
if fullMatch && input[i] == '.' && len(values[Domain]) > 0 {
suffixMatches = append(suffixMatches, values[Domain])
}
}
}
// At the end of input, check if the whole string matches a pattern.
// For Domain matchers:
// 1. Exact match on Domain Matcher works like Full Match. e.g. foo.com is a full match for domain:foo.com.
// For Full matchers:
// 1. Only when no fail edge is traversed (fullMatch).
// 2. Takes the highest priority (added at last).
if fullMatch && node.match != 0 {
values := ac.values[node.match]
if len(values[Domain]) > 0 {
suffixMatches = append(suffixMatches, values[Domain])
}
if len(values[Full]) > 0 {
suffixMatches = append(suffixMatches, values[Full])
}
}
if len(substrMatches) == 0 {
return CompositeMatchesReverse(suffixMatches)
}
return CompositeMatchesReverse(append(substrMatches, suffixMatches...))
}
// MatchAny implements MatcherGroup.MatchAny.
func (ac *ACAutomatonMatcherGroup) MatchAny(input string) bool {
fullMatch := true
node := &ac.nodes[0]
for i := len(input) - 1; i >= 0; i-- {
edge := acCharset[input[i]]
fullMatch = fullMatch && (node.edge[edge] == acTrieEdge)
node = &ac.nodes[node.next[edge]]
if node.fail != 0 { // There is a match on this node's fail path
return true
}
if node.match != 0 { // There is a match on this node
values := ac.values[node.match]
if len(values[Substr]) > 0 { // Substr match succeeds unconditionally
return true
}
if fullMatch && input[i] == '.' && len(values[Domain]) > 0 { // Domain match only succeeds with dot separator on trie path
return true
}
}
}
return fullMatch && node.match != 0 // At the end of input, Domain and Full match will succeed if no fail edge is traversed
}
// Letter-Digit-Hyphen (LDH) subset (https://tools.ietf.org/html/rfc952):
// - Letters A to Z (no distinction is made between uppercase and lowercase)
// - Digits 0 to 9
// - Hyphens(-) and Periods(.)
//
// If for future the strmatcher are used for other scenarios than domain,
// we could add a new Charset interface to represent variable charsets.
var acCharset = [256]int{
'A': 1,
'a': 1,
'B': 2,
'b': 2,
'C': 3,
'c': 3,
'D': 4,
'd': 4,
'E': 5,
'e': 5,
'F': 6,
'f': 6,
'G': 7,
'g': 7,
'H': 8,
'h': 8,
'I': 9,
'i': 9,
'J': 10,
'j': 10,
'K': 11,
'k': 11,
'L': 12,
'l': 12,
'M': 13,
'm': 13,
'N': 14,
'n': 14,
'O': 15,
'o': 15,
'P': 16,
'p': 16,
'Q': 17,
'q': 17,
'R': 18,
'r': 18,
'S': 19,
's': 19,
'T': 20,
't': 20,
'U': 21,
'u': 21,
'V': 22,
'v': 22,
'W': 23,
'w': 23,
'X': 24,
'x': 24,
'Y': 25,
'y': 25,
'Z': 26,
'z': 26,
'-': 27,
'.': 28,
'0': 29,
'1': 30,
'2': 31,
'3': 32,
'4': 33,
'5': 34,
'6': 35,
'7': 36,
'8': 37,
'9': 38,
}
@@ -0,0 +1,365 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestACAutomatonMatcherGroup(t *testing.T) {
cases1 := []struct {
pattern string
mType Type
input string
output bool
}{
{
pattern: "example.com",
mType: Domain,
input: "www.example.com",
output: true,
},
{
pattern: "example.com",
mType: Domain,
input: "example.com",
output: true,
},
{
pattern: "example.com",
mType: Domain,
input: "www.e3ample.com",
output: false,
},
{
pattern: "example.com",
mType: Domain,
input: "xample.com",
output: false,
},
{
pattern: "example.com",
mType: Domain,
input: "xexample.com",
output: false,
},
{
pattern: "example.com",
mType: Full,
input: "example.com",
output: true,
},
{
pattern: "example.com",
mType: Full,
input: "xexample.com",
output: false,
},
}
for _, test := range cases1 {
ac := NewACAutomatonMatcherGroup()
matcher, err := test.mType.New(test.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(ac, matcher, 0))
ac.Build()
if m := ac.MatchAny(test.input); m != test.output {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
{
cases2Input := []struct {
pattern string
mType Type
}{
{
pattern: "163.com",
mType: Domain,
},
{
pattern: "m.126.com",
mType: Full,
},
{
pattern: "3.com",
mType: Full,
},
{
pattern: "google.com",
mType: Substr,
},
{
pattern: "vgoogle.com",
mType: Substr,
},
}
ac := NewACAutomatonMatcherGroup()
for _, test := range cases2Input {
matcher, err := test.mType.New(test.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(ac, matcher, 0))
}
ac.Build()
cases2Output := []struct {
pattern string
res bool
}{
{
pattern: "126.com",
res: false,
},
{
pattern: "m.163.com",
res: true,
},
{
pattern: "mm163.com",
res: false,
},
{
pattern: "m.126.com",
res: true,
},
{
pattern: "163.com",
res: true,
},
{
pattern: "63.com",
res: false,
},
{
pattern: "oogle.com",
res: false,
},
{
pattern: "vvgoogle.com",
res: true,
},
}
for _, test := range cases2Output {
if m := ac.MatchAny(test.pattern); m != test.res {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
{
cases3Input := []struct {
pattern string
mType Type
}{
{
pattern: "video.google.com",
mType: Domain,
},
{
pattern: "gle.com",
mType: Domain,
},
}
ac := NewACAutomatonMatcherGroup()
for _, test := range cases3Input {
matcher, err := test.mType.New(test.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(ac, matcher, 0))
}
ac.Build()
cases3Output := []struct {
pattern string
res bool
}{
{
pattern: "google.com",
res: false,
},
}
for _, test := range cases3Output {
if m := ac.MatchAny(test.pattern); m != test.res {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
{
cases4Input := []struct {
pattern string
mType Type
}{
{
pattern: "apis",
mType: Substr,
},
{
pattern: "googleapis.com",
mType: Domain,
},
}
ac := NewACAutomatonMatcherGroup()
for _, test := range cases4Input {
matcher, err := test.mType.New(test.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(ac, matcher, 0))
}
ac.Build()
cases4Output := []struct {
pattern string
res bool
}{
{
pattern: "gapis.com",
res: true,
},
}
for _, test := range cases4Output {
if m := ac.MatchAny(test.pattern); m != test.res {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
}
func TestACAutomatonMatcherGroupSubstr(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 []uint32
}{
{
input: "google.com",
output: []uint32{1},
},
{
input: "apis.com",
output: []uint32{0, 2},
},
{
input: "googleapis.com",
output: []uint32{1, 0, 2},
},
{
input: "fonts.googleapis.com",
output: []uint32{1, 0, 2},
},
{
input: "apis.googleapis.com",
output: []uint32{0, 2, 1, 0, 2},
},
}
matcherGroup := NewACAutomatonMatcherGroup()
for id, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(id)))
}
matcherGroup.Build()
for _, test := range cases {
if r := matcherGroup.Match(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
func TestACAutomatonMatcherGroupAsIndexMatcher(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
// Regex not supported by ACAutomationMatcherGroup
// {
// Type: Regex,
// Domain: "apis\\.us$",
// },
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4, 2, 6},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4, 2, 6},
},
{
Input: "testapis.us",
Output: []uint32{2, 6 /*, 1*/},
},
{
Input: "example.com",
Output: []uint32{10, 4},
},
}
matcherGroup := NewACAutomatonMatcherGroup()
for i, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(i+2)))
}
matcherGroup.Build()
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
@@ -0,0 +1,109 @@
package strmatcher
type trieNode struct {
values []uint32
children map[string]*trieNode
}
// DomainMatcherGroup is an implementation of MatcherGroup.
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
type DomainMatcherGroup struct {
root *trieNode
}
func NewDomainMatcherGroup() *DomainMatcherGroup {
return &DomainMatcherGroup{
root: new(trieNode),
}
}
// AddDomainMatcher implements MatcherGroupForDomain.AddDomainMatcher.
func (g *DomainMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
node := g.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]*trieNode)
}
next := node.children[part]
if next == nil {
next = new(trieNode)
node.children[part] = next
}
node = next
}
node.values = append(node.values, value)
}
// Match implements MatcherGroup.Match.
func (g *DomainMatcherGroup) Match(input string) []uint32 {
matches := make([][]uint32, 0, 5)
node := g.root
for i := len(input); i > 0; {
for j := i - 1; ; j-- {
if input[j] == '.' { // Domain label found
node = node.children[input[j+1:i]]
i = j
break
}
if j == 0 { // The last part of domain label
node = node.children[input[j:i]]
i = j
break
}
}
if node == nil { // No more match if no trie edge transition
break
}
if len(node.values) > 0 { // Found matched matchers
matches = append(matches, node.values)
}
if node.children == nil { // No more match if leaf node reached
break
}
}
return CompositeMatchesReverse(matches)
}
// MatchAny implements MatcherGroup.MatchAny.
func (g *DomainMatcherGroup) MatchAny(input string) bool {
node := g.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 len(node.values) > 0 {
return true
}
if node.children == nil {
return false
}
}
return false
}
@@ -4,19 +4,43 @@ import (
"reflect" "reflect"
"testing" "testing"
. "github.com/xtls/xray-core/common/strmatcher" . "github.com/xtls/xray-core/common/geodata/strmatcher"
) )
func TestDomainMatcherGroup(t *testing.T) { func TestDomainMatcherGroup(t *testing.T) {
g := new(DomainMatcherGroup) patterns := []struct {
g.Add("example.com", 1) Pattern string
g.Add("google.com", 2) Value uint32
g.Add("x.a.com", 3) }{
g.Add("a.b.com", 4) {
g.Add("c.a.b.com", 5) Pattern: "example.com",
g.Add("x.y.com", 4) Value: 1,
g.Add("x.y.com", 6) },
{
Pattern: "google.com",
Value: 2,
},
{
Pattern: "x.a.com",
Value: 3,
},
{
Pattern: "a.b.com",
Value: 4,
},
{
Pattern: "c.a.b.com",
Value: 5,
},
{
Pattern: "x.y.com",
Value: 4,
},
{
Pattern: "x.y.com",
Value: 6,
},
}
testCases := []struct { testCases := []struct {
Domain string Domain string
Result []uint32 Result []uint32
@@ -58,7 +82,10 @@ func TestDomainMatcherGroup(t *testing.T) {
Result: []uint32{4, 6}, Result: []uint32{4, 6},
}, },
} }
g := NewDomainMatcherGroup()
for _, pattern := range patterns {
AddMatcherToGroup(g, DomainMatcher(pattern.Pattern), pattern.Value)
}
for _, testCase := range testCases { for _, testCase := range testCases {
r := g.Match(testCase.Domain) r := g.Match(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) { if !reflect.DeepEqual(r, testCase.Result) {
@@ -68,7 +95,7 @@ func TestDomainMatcherGroup(t *testing.T) {
} }
func TestEmptyDomainMatcherGroup(t *testing.T) { func TestEmptyDomainMatcherGroup(t *testing.T) {
g := new(DomainMatcherGroup) g := NewDomainMatcherGroup()
r := g.Match("example.com") r := g.Match("example.com")
if len(r) != 0 { if len(r) != 0 {
t.Error("Expect [], but ", r) t.Error("Expect [], but ", r)
@@ -0,0 +1,30 @@
package strmatcher
// FullMatcherGroup is an implementation of MatcherGroup.
// It uses a hash table to facilitate exact match lookup.
type FullMatcherGroup struct {
matchers map[string][]uint32
}
func NewFullMatcherGroup() *FullMatcherGroup {
return &FullMatcherGroup{
matchers: make(map[string][]uint32),
}
}
// AddFullMatcher implements MatcherGroupForFull.AddFullMatcher.
func (g *FullMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
domain := matcher.Pattern()
g.matchers[domain] = append(g.matchers[domain], value)
}
// Match implements MatcherGroup.Match.
func (g *FullMatcherGroup) Match(input string) []uint32 {
return g.matchers[input]
}
// MatchAny implements MatcherGroup.Any.
func (g *FullMatcherGroup) MatchAny(input string) bool {
_, found := g.matchers[input]
return found
}
@@ -4,17 +4,35 @@ import (
"reflect" "reflect"
"testing" "testing"
. "github.com/xtls/xray-core/common/strmatcher" . "github.com/xtls/xray-core/common/geodata/strmatcher"
) )
func TestFullMatcherGroup(t *testing.T) { func TestFullMatcherGroup(t *testing.T) {
g := new(FullMatcherGroup) patterns := []struct {
g.Add("example.com", 1) Pattern string
g.Add("google.com", 2) Value uint32
g.Add("x.a.com", 3) }{
g.Add("x.y.com", 4) {
g.Add("x.y.com", 6) Pattern: "example.com",
Value: 1,
},
{
Pattern: "google.com",
Value: 2,
},
{
Pattern: "x.a.com",
Value: 3,
},
{
Pattern: "x.y.com",
Value: 4,
},
{
Pattern: "x.y.com",
Value: 6,
},
}
testCases := []struct { testCases := []struct {
Domain string Domain string
Result []uint32 Result []uint32
@@ -32,7 +50,10 @@ func TestFullMatcherGroup(t *testing.T) {
Result: []uint32{4, 6}, Result: []uint32{4, 6},
}, },
} }
g := NewFullMatcherGroup()
for _, pattern := range patterns {
AddMatcherToGroup(g, FullMatcher(pattern.Pattern), pattern.Value)
}
for _, testCase := range testCases { for _, testCase := range testCases {
r := g.Match(testCase.Domain) r := g.Match(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) { if !reflect.DeepEqual(r, testCase.Result) {
@@ -42,7 +63,7 @@ func TestFullMatcherGroup(t *testing.T) {
} }
func TestEmptyFullMatcherGroup(t *testing.T) { func TestEmptyFullMatcherGroup(t *testing.T) {
g := new(FullMatcherGroup) g := NewFullMatcherGroup()
r := g.Match("example.com") r := g.Match("example.com")
if len(r) != 0 { if len(r) != 0 {
t.Error("Expect [], but ", r) t.Error("Expect [], but ", r)
@@ -0,0 +1,198 @@
package strmatcher
import (
"math/bits"
"runtime"
"sort"
"strings"
"unsafe"
)
// PrimeRK is the prime base used in Rabin-Karp algorithm.
const PrimeRK = 16777619
// RollingHash calculates the rolling murmurHash of given string based on a provided suffix hash.
func RollingHash(hash uint32, input string) uint32 {
for i := len(input) - 1; i >= 0; i-- {
hash = hash*PrimeRK + uint32(input[i])
}
return hash
}
// MemHash is the hash function used by go map, it utilizes available hardware instructions(behaves
// as aeshash if aes instruction is available).
// With different seed, each MemHash<seed> performs as distinct hash functions.
func MemHash(seed uint32, input string) uint32 {
return uint32(strhash(unsafe.Pointer(&input), uintptr(seed))) // nosemgrep
}
const (
mphMatchTypeCount = 2 // Full and Domain
)
type mphRuleInfo struct {
rollingHash uint32
matchers [mphMatchTypeCount][]uint32
}
// MphMatcherGroup is an implementation of MatcherGroup.
// It implements Rabin-Karp algorithm and minimal perfect hash table for Full and Domain matcher.
type MphMatcherGroup struct {
rules []string // RuleIdx -> pattern string, index 0 reserved for failed lookup
values [][]uint32 // RuleIdx -> registered matcher values for the pattern (Full Matcher takes precedence)
level0 []uint32 // RollingHash & Mask -> seed for Memhash
level0Mask uint32 // Mask restricting RollingHash to 0 ~ len(level0)
level1 []uint32 // Memhash<seed> & Mask -> stored index for rules
level1Mask uint32 // Mask for restricting Memhash<seed> to 0 ~ len(level1)
ruleInfos *map[string]mphRuleInfo
}
func NewMphMatcherGroup() *MphMatcherGroup {
return &MphMatcherGroup{
rules: []string{""},
values: [][]uint32{nil},
level0: nil,
level0Mask: 0,
level1: nil,
level1Mask: 0,
ruleInfos: &map[string]mphRuleInfo{}, // Only used for building, destroyed after build complete
}
}
// AddFullMatcher implements MatcherGroupForFull.
func (g *MphMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
pattern := strings.ToLower(matcher.Pattern())
g.addPattern(0, "", pattern, matcher.Type(), value)
}
// AddDomainMatcher implements MatcherGroupForDomain.
func (g *MphMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
pattern := strings.ToLower(matcher.Pattern())
hash := g.addPattern(0, "", pattern, matcher.Type(), value) // For full domain match
g.addPattern(hash, pattern, ".", matcher.Type(), value) // For partial domain match
}
func (g *MphMatcherGroup) addPattern(suffixHash uint32, suffixPattern string, pattern string, matcherType Type, value uint32) uint32 {
fullPattern := pattern + suffixPattern
info, found := (*g.ruleInfos)[fullPattern]
if !found {
info = mphRuleInfo{rollingHash: RollingHash(suffixHash, pattern)}
g.rules = append(g.rules, fullPattern)
g.values = append(g.values, nil)
}
info.matchers[matcherType] = append(info.matchers[matcherType], value)
(*g.ruleInfos)[fullPattern] = info
return info.rollingHash
}
// Build builds a minimal perfect hash table for insert rules.
// Algorithm used: Hash, displace, and compress. See http://cmph.sourceforge.net/papers/esa09.pdf
func (g *MphMatcherGroup) Build() error {
ruleCount := len(*g.ruleInfos)
g.level0 = make([]uint32, nextPow2(ruleCount/4))
g.level0Mask = uint32(len(g.level0) - 1)
g.level1 = make([]uint32, nextPow2(ruleCount))
g.level1Mask = uint32(len(g.level1) - 1)
// Create buckets based on all rule's rolling hash
buckets := make([][]uint32, len(g.level0))
for ruleIdx := 1; ruleIdx < len(g.rules); ruleIdx++ { // Traverse rules starting from index 1 (0 reserved for failed lookup)
ruleInfo := (*g.ruleInfos)[g.rules[ruleIdx]]
bucketIdx := ruleInfo.rollingHash & g.level0Mask
buckets[bucketIdx] = append(buckets[bucketIdx], uint32(ruleIdx))
g.values[ruleIdx] = append(ruleInfo.matchers[Full], ruleInfo.matchers[Domain]...) // nolint:gocritic
}
g.ruleInfos = nil // Set ruleInfos nil to release memory
runtime.GC() // peak mem
// Sort buckets in descending order with respect to each bucket's size
bucketIdxs := make([]int, len(buckets))
for bucketIdx := range buckets {
bucketIdxs[bucketIdx] = bucketIdx
}
sort.Slice(bucketIdxs, func(i, j int) bool { return len(buckets[bucketIdxs[i]]) > len(buckets[bucketIdxs[j]]) })
// Exercise Hash, Displace, and Compress algorithm to construct minimal perfect hash table
occupied := make([]bool, len(g.level1)) // Whether a second-level hash has been already used
hashedBucket := make([]uint32, 0, 4) // Second-level hashes for each rule in a specific bucket
for _, bucketIdx := range bucketIdxs {
bucket := buckets[bucketIdx]
hashedBucket = hashedBucket[:0]
seed := uint32(0)
for len(hashedBucket) != len(bucket) {
for _, ruleIdx := range bucket {
memHash := MemHash(seed, g.rules[ruleIdx]) & g.level1Mask
if occupied[memHash] { // Collision occurred with this seed
for _, hash := range hashedBucket { // Revert all values in this hashed bucket
occupied[hash] = false
g.level1[hash] = 0
}
hashedBucket = hashedBucket[:0]
seed++ // Try next seed
break
}
occupied[memHash] = true
g.level1[memHash] = ruleIdx // The final value in the hash table
hashedBucket = append(hashedBucket, memHash)
}
}
g.level0[bucketIdx] = seed // Displacement value for this bucket
}
return nil
}
// Lookup searches for input in minimal perfect hash table and returns its index. 0 indicates not found.
func (g *MphMatcherGroup) Lookup(rollingHash uint32, input string) uint32 {
i0 := rollingHash & g.level0Mask
seed := g.level0[i0]
i1 := MemHash(seed, input) & g.level1Mask
if n := g.level1[i1]; g.rules[n] == input {
return n
}
return 0
}
// Match implements MatcherGroup.Match.
func (g *MphMatcherGroup) Match(input string) []uint32 {
matches := make([][]uint32, 0, 5)
hash := uint32(0)
for i := len(input) - 1; i >= 0; i-- {
hash = hash*PrimeRK + uint32(input[i])
if input[i] == '.' {
if mphIdx := g.Lookup(hash, input[i:]); mphIdx != 0 {
matches = append(matches, g.values[mphIdx])
}
}
}
if mphIdx := g.Lookup(hash, input); mphIdx != 0 {
matches = append(matches, g.values[mphIdx])
}
return CompositeMatchesReverse(matches)
}
// MatchAny implements MatcherGroup.MatchAny.
func (g *MphMatcherGroup) MatchAny(input string) bool {
hash := uint32(0)
for i := len(input) - 1; i >= 0; i-- {
hash = hash*PrimeRK + uint32(input[i])
if input[i] == '.' {
if g.Lookup(hash, input[i:]) != 0 {
return true
}
}
}
return g.Lookup(hash, input) != 0
}
func nextPow2(v int) int {
if v <= 1 {
return 1
}
const MaxUInt = ^uint(0)
n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1
return int(n)
}
//go:noescape
//go:linkname strhash runtime.strhash
func strhash(p unsafe.Pointer, h uintptr) uintptr
@@ -5,94 +5,10 @@ import (
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/strmatcher" . "github.com/xtls/xray-core/common/geodata/strmatcher"
) )
func TestMatcherGroup(t *testing.T) { func TestMphMatcherGroup(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
{
Type: Regex,
Domain: "apis\\.us$",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4, 2, 6},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4, 2, 6},
},
{
Input: "testapis.us",
Output: []uint32{1, 2, 6},
},
{
Input: "example.com",
Output: []uint32{10, 4},
},
}
matcherGroup := &MatcherGroup{}
for _, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
matcherGroup.Add(matcher)
}
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
func TestACAutomaton(t *testing.T) {
cases1 := []struct { cases1 := []struct {
pattern string pattern string
mType Type mType Type
@@ -100,53 +16,55 @@ func TestACAutomaton(t *testing.T) {
output bool output bool
}{ }{
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Domain, mType: Domain,
input: "www.xtls.github.io", input: "www.example.com",
output: true, output: true,
}, },
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Domain, mType: Domain,
input: "xtls.github.io", input: "example.com",
output: true, output: true,
}, },
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Domain, mType: Domain,
input: "www.xtis.github.io", input: "www.e3ample.com",
output: false, output: false,
}, },
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Domain, mType: Domain,
input: "tls.github.io", input: "xample.com",
output: false, output: false,
}, },
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Domain, mType: Domain,
input: "xxtls.github.io", input: "xexample.com",
output: false, output: false,
}, },
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Full, mType: Full,
input: "xtls.github.io", input: "example.com",
output: true, output: true,
}, },
{ {
pattern: "xtls.github.io", pattern: "example.com",
mType: Full, mType: Full,
input: "xxtls.github.io", input: "xexample.com",
output: false, output: false,
}, },
} }
for _, test := range cases1 { for _, test := range cases1 {
ac := NewACAutomaton() mph := NewMphMatcherGroup()
ac.Add(test.pattern, test.mType) matcher, err := test.mType.New(test.pattern)
ac.Build() common.Must(err)
if m := ac.Match(test.input); m != test.output { common.Must(AddMatcherToGroup(mph, matcher, 0))
mph.Build()
if m := mph.MatchAny(test.input); m != test.output {
t.Error("unexpected output: ", m, " for test case ", test) t.Error("unexpected output: ", m, " for test case ", test)
} }
} }
@@ -167,20 +85,14 @@ func TestACAutomaton(t *testing.T) {
pattern: "3.com", pattern: "3.com",
mType: Full, mType: Full,
}, },
{
pattern: "google.com",
mType: Substr,
},
{
pattern: "vgoogle.com",
mType: Substr,
},
} }
ac := NewACAutomaton() mph := NewMphMatcherGroup()
for _, test := range cases2Input { for _, test := range cases2Input {
ac.Add(test.pattern, test.mType) matcher, err := test.mType.New(test.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(mph, matcher, 0))
} }
ac.Build() mph.Build()
cases2Output := []struct { cases2Output := []struct {
pattern string pattern string
res bool res bool
@@ -215,15 +127,11 @@ func TestACAutomaton(t *testing.T) {
}, },
{ {
pattern: "vvgoogle.com", pattern: "vvgoogle.com",
res: true,
},
{
pattern: "½",
res: false, res: false,
}, },
} }
for _, test := range cases2Output { for _, test := range cases2Output {
if m := ac.Match(test.pattern); m != test.res { if m := mph.MatchAny(test.pattern); m != test.res {
t.Error("unexpected output: ", m, " for test case ", test) t.Error("unexpected output: ", m, " for test case ", test)
} }
} }
@@ -242,11 +150,13 @@ func TestACAutomaton(t *testing.T) {
mType: Domain, mType: Domain,
}, },
} }
ac := NewACAutomaton() mph := NewMphMatcherGroup()
for _, test := range cases3Input { for _, test := range cases3Input {
ac.Add(test.pattern, test.mType) matcher, err := test.mType.New(test.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(mph, matcher, 0))
} }
ac.Build() mph.Build()
cases3Output := []struct { cases3Output := []struct {
pattern string pattern string
res bool res bool
@@ -257,9 +167,112 @@ func TestACAutomaton(t *testing.T) {
}, },
} }
for _, test := range cases3Output { for _, test := range cases3Output {
if m := ac.Match(test.pattern); m != test.res { if m := mph.MatchAny(test.pattern); m != test.res {
t.Error("unexpected output: ", m, " for test case ", test) t.Error("unexpected output: ", m, " for test case ", test)
} }
} }
} }
} }
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
func TestMphMatcherGroupAsIndexMatcher(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
// Regex not supported by MphMatcherGroup
// {
// Type: Regex,
// Domain: "apis\\.us$",
// },
// Substr not supported by MphMatcherGroup
// {
// Type: Substr,
// Domain: "apis",
// },
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
// Substr not supported by MphMatcherGroup, We add another matcher to preserve index
{
Type: Domain, // Substr,
Domain: "example.com", // "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{ // This matcher (index 10) is swapped with matcher (index 6) to test that full matcher takes high priority.
Type: Full,
Domain: "example.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4 /*2, 6*/},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4 /*2, 6*/},
},
{
Input: "testapis.us",
// Output: []uint32{ /*2, 6*/ /*1,*/ },
Output: nil,
},
{
Input: "example.com",
Output: []uint32{10, 6, 11, 4},
},
}
matcherGroup := NewMphMatcherGroup()
for i, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(i+3)))
}
matcherGroup.Build()
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
func TestEmptyMphMatcherGroup(t *testing.T) {
g := NewMphMatcherGroup()
g.Build()
r := g.Match("example.com")
if len(r) != 0 {
t.Error("Expect [], but ", r)
}
}
@@ -0,0 +1,41 @@
package strmatcher
type matcherEntry struct {
matcher Matcher
value uint32
}
// SimpleMatcherGroup is an implementation of MatcherGroup.
// It simply stores all matchers in an array and sequentially matches them.
type SimpleMatcherGroup struct {
matchers []matcherEntry
}
// AddMatcher implements MatcherGroupForAll.AddMatcher.
func (g *SimpleMatcherGroup) AddMatcher(matcher Matcher, value uint32) {
g.matchers = append(g.matchers, matcherEntry{
matcher: matcher,
value: value,
})
}
// Match implements MatcherGroup.Match.
func (g *SimpleMatcherGroup) Match(input string) []uint32 {
result := []uint32{}
for _, e := range g.matchers {
if e.matcher.Match(input) {
result = append(result, e.value)
}
}
return result
}
// MatchAny implements MatcherGroup.MatchAny.
func (g *SimpleMatcherGroup) MatchAny(input string) bool {
for _, e := range g.matchers {
if e.matcher.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 TestSimpleMatcherGroup(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 []uint32
}{
{
input: "www.example.com",
output: []uint32{0, 2},
},
{
input: "example.com",
output: []uint32{0, 1, 2},
},
{
input: "www.e3ample.com",
output: []uint32{},
},
{
input: "xample.com",
output: []uint32{},
},
{
input: "xexample.com",
output: []uint32{2},
},
{
input: "examplexcom",
output: []uint32{2},
},
}
matcherGroup := &SimpleMatcherGroup{}
for id, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(id)))
}
for _, test := range cases {
if r := matcherGroup.Match(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
@@ -0,0 +1,61 @@
package strmatcher
import (
"sort"
"strings"
)
// SubstrMatcherGroup is implementation of MatcherGroup,
// It is simply implmeneted to comply with the priority specification of Substr matchers.
type SubstrMatcherGroup struct {
patterns []string
values []uint32
}
// AddSubstrMatcher implements MatcherGroupForSubstr.AddSubstrMatcher.
func (g *SubstrMatcherGroup) AddSubstrMatcher(matcher SubstrMatcher, value uint32) {
g.patterns = append(g.patterns, matcher.Pattern())
g.values = append(g.values, value)
}
// Match implements MatcherGroup.Match.
func (g *SubstrMatcherGroup) Match(input string) []uint32 {
var result []uint32
for i, pattern := range g.patterns {
for j := strings.LastIndex(input, pattern); j != -1; j = strings.LastIndex(input[:j], pattern) {
result = append(result, uint32(j)<<16|uint32(i)&0xffff) // uint32: position (higher 16 bit) | patternIdx (lower 16 bit)
}
}
// sort.Slice will trigger allocation no matter what input is. See https://github.com/golang/go/issues/17332
// We optimize the sorting by length to prevent memory allocation as possible.
switch len(result) {
case 0:
return nil
case 1:
// No need to sort
case 2:
// Do a simple swap if unsorted
if result[0] > result[1] {
result[0], result[1] = result[1], result[0]
}
default:
// Sort the match results in dictionary order, so that:
// 1. Pattern matched at smaller position (meaning matched further) takes precedence.
// 2. When patterns matched at same position, pattern with smaller index (meaning inserted early) takes precedence.
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
}
for i, entry := range result {
result[i] = g.values[entry&0xffff] // Get pattern value from its index (the lower 16 bit)
}
return result
}
// MatchAny implements MatcherGroup.MatchAny.
func (g *SubstrMatcherGroup) MatchAny(input string) bool {
for _, pattern := range g.patterns {
if strings.Contains(input, pattern) {
return true
}
}
return false
}
@@ -0,0 +1,65 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSubstrMatcherGroup(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 []uint32
}{
{
input: "google.com",
output: []uint32{1},
},
{
input: "apis.com",
output: []uint32{0, 2},
},
{
input: "googleapis.com",
output: []uint32{1, 0, 2},
},
{
input: "fonts.googleapis.com",
output: []uint32{1, 0, 2},
},
{
input: "apis.googleapis.com",
output: []uint32{0, 2, 1, 0, 2},
},
}
matcherGroup := &SubstrMatcherGroup{}
for id, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(id)))
}
for _, test := range cases {
if r := matcherGroup.Match(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
+348
View File
@@ -0,0 +1,348 @@
package strmatcher
import (
"errors"
"regexp"
"slices"
"strings"
"unicode/utf8"
"golang.org/x/net/idna"
)
// FullMatcher is an implementation of Matcher.
type FullMatcher string
func (FullMatcher) Type() Type {
return Full
}
func (m FullMatcher) Pattern() string {
return string(m)
}
func (m FullMatcher) String() string {
return "full:" + m.Pattern()
}
func (m FullMatcher) Match(s string) bool {
return string(m) == s
}
// DomainMatcher is an implementation of Matcher.
type DomainMatcher string
func (DomainMatcher) Type() Type {
return Domain
}
func (m DomainMatcher) Pattern() string {
return string(m)
}
func (m DomainMatcher) String() string {
return "domain:" + m.Pattern()
}
func (m DomainMatcher) Match(s string) bool {
pattern := m.Pattern()
if !strings.HasSuffix(s, pattern) {
return false
}
return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.'
}
// SubstrMatcher is an implementation of Matcher.
type SubstrMatcher string
func (SubstrMatcher) Type() Type {
return Substr
}
func (m SubstrMatcher) Pattern() string {
return string(m)
}
func (m SubstrMatcher) String() string {
return "keyword:" + m.Pattern()
}
func (m SubstrMatcher) Match(s string) bool {
return strings.Contains(s, m.Pattern())
}
// RegexMatcher is an implementation of Matcher.
type RegexMatcher struct {
pattern *regexp.Regexp
}
func (*RegexMatcher) Type() Type {
return Regex
}
func (m *RegexMatcher) Pattern() string {
return m.pattern.String()
}
func (m *RegexMatcher) String() string {
return "regexp:" + m.Pattern()
}
func (m *RegexMatcher) Match(s string) bool {
return m.pattern.MatchString(s)
}
// New creates a new Matcher based on the given pattern.
func (t Type) New(pattern string) (Matcher, error) {
switch t {
case Full:
return FullMatcher(pattern), nil
case Substr:
return SubstrMatcher(pattern), nil
case Domain:
return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
return &RegexMatcher{pattern: regex}, nil
default:
return nil, errors.New("unknown matcher type")
}
}
// NewDomainPattern creates a new Matcher based on the given domain pattern.
// It works like `Type.New`, but will do validation and conversion to ensure it's a valid domain pattern.
func (t Type) NewDomainPattern(pattern string) (Matcher, error) {
switch t {
case Full:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return FullMatcher(pattern), nil
case Substr:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return SubstrMatcher(pattern), nil
case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil
case Regex: // Regex's charset not in LDH subset
regex, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
return &RegexMatcher{pattern: regex}, nil
default:
return nil, errors.New("unknown matcher type")
}
}
// ToDomain converts input pattern to a domain string, and return error if such a conversion cannot be made.
// 1. Conforms to Letter-Digit-Hyphen (LDH) subset (https://tools.ietf.org/html/rfc952):
// * Letters A to Z (no distinction between uppercase and lowercase, we convert to lowers)
// * Digits 0 to 9
// * Hyphens(-) and Periods(.)
// 2. If any non-ASCII characters, domain are converted from Internationalized domain name to Punycode.
func ToDomain(pattern string) (string, error) {
for {
isASCII, hasUpper := true, false
for i := 0; i < len(pattern); i++ {
c := pattern[i]
if c >= utf8.RuneSelf {
isASCII = false
break
}
switch {
case 'A' <= c && c <= 'Z':
hasUpper = true
case 'a' <= c && c <= 'z':
case '0' <= c && c <= '9':
case c == '-':
case c == '.':
default:
return "", errors.New("pattern string does not conform to Letter-Digit-Hyphen (LDH) subset")
}
}
if !isASCII {
var err error
pattern, err = idna.Punycode.ToASCII(pattern)
if err != nil {
return "", err
}
continue
}
if hasUpper {
pattern = strings.ToLower(pattern)
}
break
}
return pattern, nil
}
// MatcherGroupForAll is an interface indicating a MatcherGroup could accept all types of matchers.
type MatcherGroupForAll interface {
AddMatcher(matcher Matcher, value uint32)
}
// MatcherGroupForFull is an interface indicating a MatcherGroup could accept FullMatchers.
type MatcherGroupForFull interface {
AddFullMatcher(matcher FullMatcher, value uint32)
}
// MatcherGroupForDomain is an interface indicating a MatcherGroup could accept DomainMatchers.
type MatcherGroupForDomain interface {
AddDomainMatcher(matcher DomainMatcher, value uint32)
}
// MatcherGroupForSubstr is an interface indicating a MatcherGroup could accept SubstrMatchers.
type MatcherGroupForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher, value uint32)
}
// MatcherGroupForRegex is an interface indicating a MatcherGroup could accept RegexMatchers.
type MatcherGroupForRegex interface {
AddRegexMatcher(matcher *RegexMatcher, value uint32)
}
// AddMatcherToGroup is a helper function to try to add a Matcher to any kind of MatcherGroup.
// It returns error if the MatcherGroup does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherGroup.
func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error {
if g, ok := g.(IndexMatcher); ok {
g.Add(matcher)
return nil
}
if g, ok := g.(MatcherGroupForAll); ok {
g.AddMatcher(matcher, value)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if g, ok := g.(MatcherGroupForFull); ok {
g.AddFullMatcher(matcher, value)
return nil
}
case DomainMatcher:
if g, ok := g.(MatcherGroupForDomain); ok {
g.AddDomainMatcher(matcher, value)
return nil
}
case SubstrMatcher:
if g, ok := g.(MatcherGroupForSubstr); ok {
g.AddSubstrMatcher(matcher, value)
return nil
}
case *RegexMatcher:
if g, ok := g.(MatcherGroupForRegex); ok {
g.AddRegexMatcher(matcher, value)
return nil
}
}
return errors.New("cannot add matcher to matcher group")
}
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
func CompositeMatches(matches [][]uint32) []uint32 {
switch len(matches) {
case 0:
return nil
case 1:
return slices.Clone(matches[0])
default:
result := make([]uint32, 0, 5)
for i := 0; i < len(matches); i++ {
result = append(result, matches[i]...)
}
return result
}
}
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
// It is designed that:
// 1. All matchers are concatenated in reverse order, so the matcher that matches further ranks higher.
// 2. Indices in the same matcher keeps their original order.
// 3. Avoid new memory allocation as possible.
func CompositeMatchesReverse(matches [][]uint32) []uint32 {
switch len(matches) {
case 0:
return nil
case 1:
return matches[0]
default:
result := make([]uint32, 0, 5)
for i := len(matches) - 1; i >= 0; i-- {
result = append(result, matches[i]...)
}
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")
}
+149
View File
@@ -0,0 +1,149 @@
package strmatcher_test
import (
"reflect"
"testing"
"unsafe"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestMatcher(t *testing.T) {
cases := []struct {
pattern string
mType Type
input string
output bool
}{
{
pattern: "example.com",
mType: Domain,
input: "www.example.com",
output: true,
},
{
pattern: "example.com",
mType: Domain,
input: "example.com",
output: true,
},
{
pattern: "example.com",
mType: Domain,
input: "www.e3ample.com",
output: false,
},
{
pattern: "example.com",
mType: Domain,
input: "xample.com",
output: false,
},
{
pattern: "example.com",
mType: Domain,
input: "xexample.com",
output: false,
},
{
pattern: "example.com",
mType: Full,
input: "example.com",
output: true,
},
{
pattern: "example.com",
mType: Full,
input: "xexample.com",
output: false,
},
{
pattern: "example.com",
mType: Regex,
input: "examplexcom",
output: true,
},
}
for _, test := range cases {
matcher, err := test.mType.New(test.pattern)
common.Must(err)
if m := matcher.Match(test.input); m != test.output {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
func TestToDomain(t *testing.T) {
{ // Test normal ASCII domain, which should not trigger new string data allocation
input := "example.com"
domain, err := ToDomain(input)
if err != nil {
t.Error("unexpected error: ", err)
}
if domain != input {
t.Error("unexpected output: ", domain, " for test case ", input)
}
if (*reflect.StringHeader)(unsafe.Pointer(&input)).Data != (*reflect.StringHeader)(unsafe.Pointer(&domain)).Data {
t.Error("different string data of output: ", domain, " and test case ", input)
}
}
{ // Test ASCII domain containing upper case letter, which should be converted to lower case
input := "eXAMPLE.cOm"
domain, err := ToDomain(input)
if err != nil {
t.Error("unexpected error: ", err)
}
if domain != "example.com" {
t.Error("unexpected output: ", domain, " for test case ", input)
}
}
{ // Test internationalized domain, which should be translated to ASCII punycode
input := "example.公益"
domain, err := ToDomain(input)
if err != nil {
t.Error("unexpected error: ", err)
}
if domain != "example.xn--55qw42g" {
t.Error("unexpected output: ", domain, " for test case ", input)
}
}
{ // Test internationalized domain containing upper case letter
input := "eXAMPLE.公益"
domain, err := ToDomain(input)
if err != nil {
t.Error("unexpected error: ", err)
}
if domain != "example.xn--55qw42g" {
t.Error("unexpected output: ", domain, " for test case ", input)
}
}
{ // Test domain name of invalid character, which should return with error
input := "{"
_, err := ToDomain(input)
if err == nil {
t.Error("unexpected non error for test case ", input)
}
}
{ // Test domain name containing a space, which should return with error
input := "Mijia Cloud"
_, err := ToDomain(input)
if err == nil {
t.Error("unexpected non error for test case ", input)
}
}
{ // Test domain name containing an underscore, which should return with error
input := "Mijia_Cloud.com"
_, err := ToDomain(input)
if err == nil {
t.Error("unexpected non error for test case ", input)
}
}
{ // Test internationalized domain containing invalid character
input := "Mijia Cloud.公司"
_, err := ToDomain(input)
if err == nil {
t.Error("unexpected non error for test case ", input)
}
}
}
@@ -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)
}
}
}
+121
View File
@@ -0,0 +1,121 @@
package strmatcher
// Type is the type of the matcher.
type Type byte
const (
// Full is the type of matcher that the input string must exactly equal to the pattern.
Full Type = 0
// Domain is the type of matcher that the input string must be a sub-domain or itself of the pattern.
Domain Type = 1
// Substr is the type of matcher that the input string must contain the pattern as a sub-string.
Substr Type = 2
// Regex is the type of matcher that the input string must matches the regular-expression pattern.
Regex Type = 3
)
// 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).
type Matcher interface {
// Type returns the matcher's type.
Type() Type
// Pattern returns the matcher's raw string representation.
Pattern() string
// String returns a string representation of the matcher containing its type and pattern.
String() string
// Match returns true if the given string matches a predefined pattern.
// * This method is seldom used for performance reason
// and is generally taken over by their corresponding MatcherGroup.
Match(input string) bool
}
// MatcherGroup is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherGroup accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherGroup accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherGroup interface {
// Match returns all matched matchers with their corresponding values.
Match(input string) []uint32
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// IndexMatcher is a general type of matcher thats accepts all kinds of basic matchers.
// It should:
// - Accept all Matcher types with no exception.
// - Optimize string matching with a combination of MatcherGroups.
// - Obey certain priority order specification when returning matched Matchers.
type IndexMatcher interface {
// Size returns number of matchers added to IndexMatcher.
Size() uint32
// Add adds a new Matcher to IndexMatcher, and returns its index. The index will never be 0.
Add(matcher Matcher) uint32
// Build builds the IndexMatcher to be ready for matching.
Build() error
// Match returns the indices of all matchers that matches the input.
// * Empty array is returned if no such matcher exists.
// * The order of returned matchers should follow priority specification.
// * The returned slice is owned by the caller and may be safely modified.
// Priority specification:
// 1. Priority between matcher types: full > domain > substr > regex.
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
// 3. Priority of domain matchers matching at different levels: the further matched domain takes precedence.
// 4. Priority of substr matchers matching at different positions: the further matched substr takes precedence.
Match(input string) []uint32
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// ValueMatcher is a general type of matcher that accepts all kinds of basic matchers.
// It should:
// - Accept all Matcher types with no exception.
// - Optimize string matching with a combination of MatcherGroups.
// - Obey certain priority order specification when returning matched values.
type ValueMatcher interface {
// Add adds a new Matcher to ValueMatcher, binding it to the provided value.
Add(matcher Matcher, value uint32)
// Build builds the ValueMatcher to be ready for matching.
Build() error
// Match returns the values of all matchers that matches the input.
// * Empty array is returned if no such matcher exists.
// * The order of returned values should follow priority specification.
// * Same value may appear multiple times if multiple matched matchers were added with that value.
// * The returned slice is owned by the caller and may be safely modified.
// Priority specification:
// 1. Priority between matcher types: full > domain > substr > regex.
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
// 3. Priority of domain matchers matching at different levels: the further matched domain takes precedence.
// 4. Priority of substr matchers matching at different positions: the further matched substr takes precedence.
Match(input string) []uint32
// 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
}
@@ -0,0 +1,85 @@
package strmatcher
// LinearValueMatcher is an implementation of ValueMatcher.
type LinearValueMatcher struct {
full *FullMatcherGroup
domain *DomainMatcherGroup
substr *SubstrMatcherGroup
regex *SimpleMatcherGroup
}
func NewLinearValueMatcher() *LinearValueMatcher {
return new(LinearValueMatcher)
}
// Add implements ValueMatcher.Add.
func (g *LinearValueMatcher) Add(matcher Matcher, value uint32) {
switch matcher := matcher.(type) {
case FullMatcher:
if g.full == nil {
g.full = NewFullMatcherGroup()
}
g.full.AddFullMatcher(matcher, value)
case DomainMatcher:
if g.domain == nil {
g.domain = NewDomainMatcherGroup()
}
g.domain.AddDomainMatcher(matcher, value)
case SubstrMatcher:
if g.substr == nil {
g.substr = new(SubstrMatcherGroup)
}
g.substr.AddSubstrMatcher(matcher, value)
default:
if g.regex == nil {
g.regex = new(SimpleMatcherGroup)
}
g.regex.AddMatcher(matcher, value)
}
}
// Build implements ValueMatcher.Build.
func (*LinearValueMatcher) Build() error {
return nil
}
// Match implements ValueMatcher.Match.
func (g *LinearValueMatcher) Match(input string) []uint32 {
// Allocate capacity to prevent matches escaping to heap
result := make([][]uint32, 0, 5)
if g.full != nil {
if matches := g.full.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.domain != nil {
if matches := g.domain.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.substr != nil {
if matches := g.substr.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.regex != nil {
if matches := g.regex.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
return CompositeMatches(result)
}
// MatchAny implements ValueMatcher.MatchAny.
func (g *LinearValueMatcher) MatchAny(input string) bool {
if g.full != nil && g.full.MatchAny(input) {
return true
}
if g.domain != nil && g.domain.MatchAny(input) {
return true
}
if g.substr != nil && g.substr.MatchAny(input) {
return true
}
return g.regex != nil && g.regex.MatchAny(input)
}
@@ -0,0 +1,89 @@
package strmatcher
import "runtime"
// A MphValueMatcher is divided into three parts:
// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table;
// 2. `substr` patterns are matched by ac automaton;
// 3. `regex` patterns are matched with the regex library.
type MphValueMatcher struct {
mph *MphMatcherGroup
ac *ACAutomatonMatcherGroup
regex *SimpleMatcherGroup
}
func NewMphValueMatcher() *MphValueMatcher {
return new(MphValueMatcher)
}
// Add implements ValueMatcher.Add.
func (g *MphValueMatcher) Add(matcher Matcher, value uint32) {
switch matcher := matcher.(type) {
case FullMatcher:
if g.mph == nil {
g.mph = NewMphMatcherGroup()
}
g.mph.AddFullMatcher(matcher, value)
case DomainMatcher:
if g.mph == nil {
g.mph = NewMphMatcherGroup()
}
g.mph.AddDomainMatcher(matcher, value)
case SubstrMatcher:
if g.ac == nil {
g.ac = NewACAutomatonMatcherGroup()
}
g.ac.AddSubstrMatcher(matcher, value)
case *RegexMatcher:
if g.regex == nil {
g.regex = &SimpleMatcherGroup{}
}
g.regex.AddMatcher(matcher, value)
}
}
// Build implements ValueMatcher.Build.
func (g *MphValueMatcher) Build() error {
if g.mph != nil {
runtime.GC() // peak mem
g.mph.Build()
}
runtime.GC() // peak mem
if g.ac != nil {
g.ac.Build()
runtime.GC() // peak mem
}
return nil
}
// Match implements ValueMatcher.Match.
func (g *MphValueMatcher) Match(input string) []uint32 {
result := make([][]uint32, 0, 5)
if g.mph != nil {
if matches := g.mph.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.ac != nil {
if matches := g.ac.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.regex != nil {
if matches := g.regex.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
return CompositeMatches(result)
}
// MatchAny implements ValueMatcher.MatchAny.
func (g *MphValueMatcher) MatchAny(input string) bool {
if g.mph != nil && g.mph.MatchAny(input) {
return true
}
if g.ac != nil && g.ac.MatchAny(input) {
return true
}
return g.regex != nil && g.regex.MatchAny(input)
}
+20
View File
@@ -0,0 +1,20 @@
//go:build android
package net
import (
"github.com/xtls/xray-core/common/errors"
)
var androidProcessFinder func(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error)
func RegisterAndroidProcessFinder(f func(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error)) {
androidProcessFinder = f
}
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
if androidProcessFinder != nil {
return androidProcessFinder(network, srcIP, srcPort, destIP, destPort)
}
return 0, "", "", errors.New("android process lookup must be registered before use")
}
+19 -23
View File
@@ -1,4 +1,4 @@
//go:build linux //go:build linux && !android
package net package net
@@ -6,6 +6,7 @@ import (
"bufio" "bufio"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -13,43 +14,38 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
) )
func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, err error) { func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
isLocal, err := IsLocal(dest.Address.IP()) isLocal, err := IsLocal(net.ParseIP(srcIP))
if err != nil { if err != nil {
return 0, "", "", errors.New("failed to determine if address is local: ", err) return 0, "", "", errors.New("failed to determine if address is local: ", err)
} }
if !isLocal { if !isLocal {
return 0, "", "", ErrNotLocal return 0, "", "", ErrNotLocal
} }
if dest.Network != Network_TCP && dest.Network != Network_UDP { if network != "tcp" && network != "udp" {
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
// the core should never has a domain as source(?
if dest.Address.Family() == AddressFamilyDomain {
panic("Domain addresses are not supported for process lookup.")
}
var procFile string var procFile string
switch dest.Network { switch network {
case Network_TCP: case "tcp":
if dest.Address.Family() == AddressFamilyIPv4 { if net.ParseIP(srcIP).To4() != nil {
procFile = "/proc/net/tcp" procFile = "/proc/net/tcp"
} } else {
if dest.Address.Family() == AddressFamilyIPv6 {
procFile = "/proc/net/tcp6" procFile = "/proc/net/tcp6"
} }
case Network_UDP: case "udp":
if dest.Address.Family() == AddressFamilyIPv4 { if net.ParseIP(srcIP).To4() != nil {
procFile = "/proc/net/udp" procFile = "/proc/net/udp"
} } else {
if dest.Address.Family() == AddressFamilyIPv6 {
procFile = "/proc/net/udp6" procFile = "/proc/net/udp6"
} }
default: default:
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
targetHexAddr, err := formatLittleEndianString(dest.Address, dest.Port) targetHexAddr, err := formatLittleEndianString(net.ParseIP(srcIP), Port(srcPort))
if err != nil { if err != nil {
return 0, "", "", errors.New("failed to format address: ", err) return 0, "", "", errors.New("failed to format address: ", err)
} }
@@ -59,7 +55,7 @@ func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, e
return 0, "", "", errors.New("could not search in ", procFile).Base(err) return 0, "", "", errors.New("could not search in ", procFile).Base(err)
} }
if inode == "" { if inode == "" {
return 0, "", "", errors.New("connection for ", dest.Address, ":", dest.Port, " not found in ", procFile) return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile)
} }
pidStr, err := findPidByInode(inode) pidStr, err := findPidByInode(inode)
@@ -86,16 +82,16 @@ func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, e
return pid, procName, absPath, nil return pid, procName, absPath, nil
} }
func formatLittleEndianString(addr Address, port Port) (string, error) { func formatLittleEndianString(addr net.IP, port Port) (string, error) {
ip := addr.IP() ip := addr
var ipBytes []byte var ipBytes []byte
if addr.Family() == AddressFamilyIPv4 { if ip.To4() != nil {
ipBytes = ip.To4() ipBytes = ip.To4()
} else { } else {
ipBytes = ip.To16() ipBytes = ip.To16()
} }
if ipBytes == nil { if ipBytes == nil {
return "", errors.New("invalid IP format for ", addr.Family(), ": ", ip) return "", errors.New("invalid IP format for ", addr, ": ", ip)
} }
for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 {
+2 -2
View File
@@ -1,4 +1,4 @@
//go:build !windows && !linux //go:build !windows && !linux && !android
package net package net
@@ -6,6 +6,6 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
) )
func FindProcess(dest Destination) (int, string, string, error) { func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
return 0, "", "", errors.New("process lookup is not supported on this platform") return 0, "", "", errors.New("process lookup is not supported on this platform")
} }
+18 -13
View File
@@ -3,6 +3,7 @@
package net package net
import ( import (
"net"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -49,41 +50,37 @@ func initWin32API() error {
return nil return nil
} }
func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, err error) { func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
once.Do(func() { once.Do(func() {
initErr = initWin32API() initErr = initWin32API()
}) })
if initErr != nil { if initErr != nil {
return 0, "", "", initErr return 0, "", "", initErr
} }
isLocal, err := IsLocal(dest.Address.IP()) isLocal, err := IsLocal(net.ParseIP(srcIP))
if err != nil { if err != nil {
return 0, "", "", errors.New("failed to determine if address is local: ", err) return 0, "", "", errors.New("failed to determine if address is local: ", err)
} }
if !isLocal { if !isLocal {
return 0, "", "", ErrNotLocal return 0, "", "", ErrNotLocal
} }
if dest.Network != Network_TCP && dest.Network != Network_UDP { if network != "tcp" && network != "udp" {
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
// the core should never has a domain as source(?
if dest.Address.Family() == AddressFamilyDomain {
panic("Domain addresses are not supported for process lookup.")
}
var class int var class int
var fn uintptr var fn uintptr
switch dest.Network { switch network {
case Network_TCP: case "tcp":
fn = getExTCPTable fn = getExTCPTable
class = tcpTablePidConn class = tcpTablePidConn
case Network_UDP: case "udp":
fn = getExUDPTable fn = getExUDPTable
class = udpTablePid class = udpTablePid
default: default:
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
ip := dest.Address.IP() ip := net.ParseIP(srcIP)
port := int(dest.Port) port := int(srcPort)
addr, ok := netip.AddrFromSlice(ip) addr, ok := netip.AddrFromSlice(ip)
if !ok { if !ok {
@@ -101,7 +98,15 @@ func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, e
return 0, "", "", err return 0, "", "", err
} }
s := newSearcher(dest.Network, dest.Address.Family()) networkType := Network_TCP
if network == "udp" {
networkType = Network_UDP
}
familyType := AddressFamilyIPv4
if addr.Is6() {
familyType = AddressFamilyIPv6
}
s := newSearcher(networkType, familyType)
pid, err := s.Search(buf, addr, uint16(port)) pid, err := s.Search(buf, addr, uint16(port))
if err != nil { if err != nil {
+40 -2
View File
@@ -1,6 +1,7 @@
package filesystem package filesystem
import ( import (
"errors"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -26,11 +27,48 @@ func ReadFile(path string) ([]byte, error) {
} }
func ReadAsset(file string) ([]byte, error) { func ReadAsset(file string) ([]byte, error) {
return ReadFile(platform.GetAssetLocation(file)) path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
} }
func OpenAsset(file string) (io.ReadCloser, error) { func OpenAsset(file string) (io.ReadCloser, error) {
return NewFileReader(platform.GetAssetLocation(file)) path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file)
return path, err
}
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
if !filepath.IsLocal(file) || file == "." {
return "", nil, errors.New("asset path must stay in asset directory")
}
local, err := filepath.Localize(file)
if err != nil {
return "", nil, err
}
path := platform.GetAssetLocation(local)
info, err := os.Stat(path)
if err != nil {
return "", nil, err
}
if !info.Mode().IsRegular() {
return "", nil, errors.New("asset is not a regular file")
}
return path, info, nil
} }
func ReadCert(file string) ([]byte, error) { func ReadCert(file string) ([]byte, error) {
+32
View File
@@ -0,0 +1,32 @@
package filesystem_test
import (
"path/filepath"
"testing"
. "github.com/xtls/xray-core/common/platform/filesystem"
)
func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{
"",
".",
"..",
"../geoip.dat",
"nested/..",
"nested/../geoip.dat",
"nested//geoip.dat",
"/geoip.dat",
"/tmp/geoip.dat",
`C:\geoip.dat`,
`C:geoip.dat`,
`\\server\share\geoip.dat`,
`nested\geoip.dat`,
`nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"),
} {
if _, err := StatAsset(file); err == nil {
t.Fatalf("expected error for %q", file)
}
}
}
-2
View File
@@ -24,8 +24,6 @@ const (
XUDPBaseKey = "xray.xudp.basekey" XUDPBaseKey = "xray.xudp.basekey"
TunFdKey = "xray.tun.fd" TunFdKey = "xray.tun.fd"
MphCachePath = "xray.mph.cache"
) )
type EnvFlag struct { type EnvFlag struct {
+35 -20
View File
@@ -100,17 +100,17 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
} }
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2 if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := quicvarint.Read(buffer) tokenLen, err := readShortQuicVarint(buffer)
if err != nil || tokenLen > uint64(len(b)) { if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQuic return nil, errNotQuic
} }
if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil { if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQuic return nil, errNotQuic
} }
} }
packetLen, err := quicvarint.Read(buffer) packetLen, err := readShortQuicVarint(buffer)
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQuic
} }
@@ -179,45 +179,45 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame case 0x00: // PADDING frame
case 0x01: // PING frame case 0x01: // PING frame
case 0x02, 0x03: // ACK frame case 0x02, 0x03: // ACK frame
if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
} }
if frameType == 0x03 { if frameType == 0x03 {
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
} }
case 0x06: // CRYPTO frame, we will use this frame case 0x06: // CRYPTO frame, we will use this frame
offset, err := quicvarint.Read(buffer) // Field: Offset offset, err := readShortQuicVarint(buffer) // Field: Offset
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
length, err := quicvarint.Read(buffer) // Field: Length length, err := readShortQuicVarint(buffer) // Field: Length
if err != nil || length > uint64(buffer.Len()) { if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
currentCryptoLen := int32(offset + length) currentCryptoLen := int32(offset + length)
@@ -228,17 +228,17 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
cryptoDataBuf.Extend(currentCryptoLen - cryptoLen) cryptoDataBuf.Extend(currentCryptoLen - cryptoLen)
cryptoLen = currentCryptoLen cryptoLen = currentCryptoLen
} }
if _, err := buffer.Read(cryptoDataBuf.BytesRange(int32(offset), currentCryptoLen)); err != nil { // Field: Crypto Data if _, err := buffer.Read(cryptoDataBuf.BytesRange(offset, currentCryptoLen)); err != nil { // Field: Crypto Data
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
@@ -283,3 +283,18 @@ func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, len
} }
return out return out
} }
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion
func readShortQuicVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader)
if err != nil {
return 0, err
}
if v > 65535 {
// not used(
return 0, errNotQuicInitial
}
return int32(v), nil
}
+29
View File
@@ -4,6 +4,7 @@ import (
"testing" "testing"
. "github.com/xtls/xray-core/common/serial" . "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
) )
func TestGetInstance(t *testing.T) { func TestGetInstance(t *testing.T) {
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
t.Error("expect nil, but actually not") t.Error("expect nil, but actually not")
} }
} }
func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) {
msg := &custom.UDPConfig{
Mode: "standalone",
Client: []*custom.UDPItem{
{Rand: 12, Save: "txid"},
},
}
tm := ToTypedMessage(msg)
if tm == nil {
t.Fatal("expected typed message")
}
roundTrip, err := tm.GetInstance()
if err != nil {
t.Fatalf("GetInstance() failed: %v", err)
}
udp, ok := roundTrip.(*custom.UDPConfig)
if !ok {
t.Fatalf("unexpected round-trip type: %T", roundTrip)
}
if udp.GetMode() != "standalone" {
t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode())
}
}
+3 -1
View File
@@ -7,6 +7,7 @@ import (
c "github.com/xtls/xray-core/common/ctx" c "github.com/xtls/xray-core/common/ctx"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/signal"
@@ -78,7 +79,8 @@ type Outbound struct {
// SniffingRequest controls the behavior of content sniffing. They are from inbound config. Read-only // SniffingRequest controls the behavior of content sniffing. They are from inbound config. Read-only
type SniffingRequest struct { type SniffingRequest struct {
ExcludeForDomain []string ExcludeForDomain geodata.DomainMatcher
ExcludeForIP geodata.IPMatcher
OverrideDestinationForProtocol []string OverrideDestinationForProtocol []string
Enabled bool Enabled bool
MetadataOnly bool MetadataOnly bool
-247
View File
@@ -1,247 +0,0 @@
package strmatcher
import (
"container/list"
)
const validCharCount = 53
type MatchType struct {
Type Type
Exist bool
}
const (
TrieEdge bool = true
FailEdge bool = false
)
type Edge struct {
Type bool
NextNode int
}
type ACAutomaton struct {
Trie [][validCharCount]Edge
Fail []int
Exists []MatchType
Count int
}
func newNode() [validCharCount]Edge {
var s [validCharCount]Edge
for i := range s {
s[i] = Edge{
Type: FailEdge,
NextNode: 0,
}
}
return s
}
var char2Index = []int{
'A': 0,
'a': 0,
'B': 1,
'b': 1,
'C': 2,
'c': 2,
'D': 3,
'd': 3,
'E': 4,
'e': 4,
'F': 5,
'f': 5,
'G': 6,
'g': 6,
'H': 7,
'h': 7,
'I': 8,
'i': 8,
'J': 9,
'j': 9,
'K': 10,
'k': 10,
'L': 11,
'l': 11,
'M': 12,
'm': 12,
'N': 13,
'n': 13,
'O': 14,
'o': 14,
'P': 15,
'p': 15,
'Q': 16,
'q': 16,
'R': 17,
'r': 17,
'S': 18,
's': 18,
'T': 19,
't': 19,
'U': 20,
'u': 20,
'V': 21,
'v': 21,
'W': 22,
'w': 22,
'X': 23,
'x': 23,
'Y': 24,
'y': 24,
'Z': 25,
'z': 25,
'!': 26,
'$': 27,
'&': 28,
'\'': 29,
'(': 30,
')': 31,
'*': 32,
'+': 33,
',': 34,
';': 35,
'=': 36,
':': 37,
'%': 38,
'-': 39,
'.': 40,
'_': 41,
'~': 42,
'0': 43,
'1': 44,
'2': 45,
'3': 46,
'4': 47,
'5': 48,
'6': 49,
'7': 50,
'8': 51,
'9': 52,
}
func NewACAutomaton() *ACAutomaton {
ac := new(ACAutomaton)
ac.Trie = append(ac.Trie, newNode())
ac.Fail = append(ac.Fail, 0)
ac.Exists = append(ac.Exists, MatchType{
Type: Full,
Exist: false,
})
return ac
}
func (ac *ACAutomaton) Add(domain string, t Type) {
node := 0
for i := len(domain) - 1; i >= 0; i-- {
idx := char2Index[domain[i]]
if ac.Trie[node][idx].NextNode == 0 {
ac.Count++
if len(ac.Trie) < ac.Count+1 {
ac.Trie = append(ac.Trie, newNode())
ac.Fail = append(ac.Fail, 0)
ac.Exists = append(ac.Exists, MatchType{
Type: Full,
Exist: false,
})
}
ac.Trie[node][idx] = Edge{
Type: TrieEdge,
NextNode: ac.Count,
}
}
node = ac.Trie[node][idx].NextNode
}
ac.Exists[node] = MatchType{
Type: t,
Exist: true,
}
switch t {
case Domain:
ac.Exists[node] = MatchType{
Type: Full,
Exist: true,
}
idx := char2Index['.']
if ac.Trie[node][idx].NextNode == 0 {
ac.Count++
if len(ac.Trie) < ac.Count+1 {
ac.Trie = append(ac.Trie, newNode())
ac.Fail = append(ac.Fail, 0)
ac.Exists = append(ac.Exists, MatchType{
Type: Full,
Exist: false,
})
}
ac.Trie[node][idx] = Edge{
Type: TrieEdge,
NextNode: ac.Count,
}
}
node = ac.Trie[node][idx].NextNode
ac.Exists[node] = MatchType{
Type: t,
Exist: true,
}
default:
break
}
}
func (ac *ACAutomaton) Build() {
queue := list.New()
for i := 0; i < validCharCount; i++ {
if ac.Trie[0][i].NextNode != 0 {
queue.PushBack(ac.Trie[0][i])
}
}
for {
front := queue.Front()
if front == nil {
break
} else {
node := front.Value.(Edge).NextNode
queue.Remove(front)
for i := 0; i < validCharCount; i++ {
if ac.Trie[node][i].NextNode != 0 {
ac.Fail[ac.Trie[node][i].NextNode] = ac.Trie[ac.Fail[node]][i].NextNode
queue.PushBack(ac.Trie[node][i])
} else {
ac.Trie[node][i] = Edge{
Type: FailEdge,
NextNode: ac.Trie[ac.Fail[node]][i].NextNode,
}
}
}
}
}
}
func (ac *ACAutomaton) Match(s string) bool {
node := 0
fullMatch := true
// 1. the match string is all through trie edge. FULL MATCH or DOMAIN
// 2. the match string is through a fail edge. NOT FULL MATCH
// 2.1 Through a fail edge, but there exists a valid node. SUBSTR
for i := len(s) - 1; i >= 0; i-- {
chr := int(s[i])
if chr >= len(char2Index) {
return false
}
idx := char2Index[chr]
fullMatch = fullMatch && ac.Trie[node][idx].Type
node = ac.Trie[node][idx].NextNode
switch ac.Exists[node].Type {
case Substr:
return true
case Domain:
if fullMatch {
return true
}
default:
break
}
}
return fullMatch && ac.Exists[node].Exist
}
-62
View File
@@ -1,62 +0,0 @@
package strmatcher_test
import (
"strconv"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/strmatcher"
)
func BenchmarkACAutomaton(b *testing.B) {
ac := NewACAutomaton()
for i := 1; i <= 1024; i++ {
ac.Add(strconv.Itoa(i)+".xray.com", Domain)
}
ac.Build()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ac.Match("0.xray.com")
}
}
func BenchmarkDomainMatcherGroup(b *testing.B) {
g := new(DomainMatcherGroup)
for i := 1; i <= 1024; i++ {
g.Add(strconv.Itoa(i)+".example.com", uint32(i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.Match("0.example.com")
}
}
func BenchmarkFullMatcherGroup(b *testing.B) {
g := new(FullMatcherGroup)
for i := 1; i <= 1024; i++ {
g.Add(strconv.Itoa(i)+".example.com", uint32(i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.Match("0.example.com")
}
}
func BenchmarkMarchGroup(b *testing.B) {
g := new(MatcherGroup)
for i := 1; i <= 1024; i++ {
m, err := Domain.New(strconv.Itoa(i) + ".example.com")
common.Must(err)
g.Add(m)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.Match("0.example.com")
}
}
-98
View File
@@ -1,98 +0,0 @@
package strmatcher
import "strings"
func breakDomain(domain string) []string {
return strings.Split(domain, ".")
}
type node struct {
values []uint32
sub map[string]*node
}
// DomainMatcherGroup is a IndexMatcher for a large set of Domain matchers.
// Visible for testing only.
type DomainMatcherGroup struct {
root *node
}
func (g *DomainMatcherGroup) Add(domain string, value uint32) {
if g.root == nil {
g.root = new(node)
}
current := g.root
parts := breakDomain(domain)
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
if current.sub == nil {
current.sub = make(map[string]*node)
}
next := current.sub[part]
if next == nil {
next = new(node)
current.sub[part] = next
}
current = next
}
current.values = append(current.values, value)
}
func (g *DomainMatcherGroup) addMatcher(m domainMatcher, value uint32) {
g.Add(string(m), value)
}
func (g *DomainMatcherGroup) Match(domain string) []uint32 {
if domain == "" {
return nil
}
current := g.root
if current == nil {
return nil
}
nextPart := func(idx int) int {
for i := idx - 1; i >= 0; i-- {
if domain[i] == '.' {
return i
}
}
return -1
}
matches := [][]uint32{}
idx := len(domain)
for {
if idx == -1 || current.sub == nil {
break
}
nidx := nextPart(idx)
part := domain[nidx+1 : idx]
next := current.sub[part]
if next == nil {
break
}
current = next
idx = nidx
if len(current.values) > 0 {
matches = append(matches, current.values)
}
}
switch len(matches) {
case 0:
return nil
case 1:
return matches[0]
default:
result := []uint32{}
for idx := range matches {
// Insert reversely, the subdomain that matches further ranks higher
result = append(result, matches[len(matches)-1-idx]...)
}
return result
}
}
-25
View File
@@ -1,25 +0,0 @@
package strmatcher
type FullMatcherGroup struct {
matchers map[string][]uint32
}
func (g *FullMatcherGroup) Add(domain string, value uint32) {
if g.matchers == nil {
g.matchers = make(map[string][]uint32)
}
g.matchers[domain] = append(g.matchers[domain], value)
}
func (g *FullMatcherGroup) addMatcher(m fullMatcher, value uint32) {
g.Add(string(m), value)
}
func (g *FullMatcherGroup) Match(str string) []uint32 {
if g.matchers == nil {
return nil
}
return g.matchers[str]
}
-56
View File
@@ -1,56 +0,0 @@
package strmatcher
import (
"regexp"
"strings"
)
type fullMatcher string
func (m fullMatcher) Match(s string) bool {
return string(m) == s
}
func (m fullMatcher) String() string {
return "full:" + string(m)
}
type substrMatcher string
func (m substrMatcher) Match(s string) bool {
return strings.Contains(s, string(m))
}
func (m substrMatcher) String() string {
return "keyword:" + string(m)
}
type domainMatcher string
func (m domainMatcher) Match(s string) bool {
pattern := string(m)
if !strings.HasSuffix(s, pattern) {
return false
}
return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.'
}
func (m domainMatcher) String() string {
return "domain:" + string(m)
}
type RegexMatcher struct {
Pattern string
reg *regexp.Regexp
}
func (m *RegexMatcher) Match(s string) bool {
if m.reg == nil {
m.reg = regexp.MustCompile(m.Pattern)
}
return m.reg.MatchString(s)
}
func (m *RegexMatcher) String() string {
return "regexp:" + m.Pattern
}
-73
View File
@@ -1,73 +0,0 @@
package strmatcher_test
import (
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/strmatcher"
)
func TestMatcher(t *testing.T) {
cases := []struct {
pattern string
mType Type
input string
output bool
}{
{
pattern: "example.com",
mType: Domain,
input: "www.example.com",
output: true,
},
{
pattern: "example.com",
mType: Domain,
input: "example.com",
output: true,
},
{
pattern: "example.com",
mType: Domain,
input: "www.fxample.com",
output: false,
},
{
pattern: "example.com",
mType: Domain,
input: "xample.com",
output: false,
},
{
pattern: "example.com",
mType: Domain,
input: "xexample.com",
output: false,
},
{
pattern: "example.com",
mType: Full,
input: "example.com",
output: true,
},
{
pattern: "example.com",
mType: Full,
input: "xexample.com",
output: false,
},
{
pattern: "example.com",
mType: Regex,
input: "examplexcom",
output: true,
},
}
for _, test := range cases {
matcher, err := test.mType.New(test.pattern)
common.Must(err)
if m := matcher.Match(test.input); m != test.output {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
-308
View File
@@ -1,308 +0,0 @@
package strmatcher
import (
"math/bits"
"regexp"
"sort"
"strings"
"unsafe"
)
// PrimeRK is the prime base used in Rabin-Karp algorithm.
const PrimeRK = 16777619
// calculate the rolling murmurHash of given string
func RollingHash(s string) uint32 {
h := uint32(0)
for i := len(s) - 1; i >= 0; i-- {
h = h*PrimeRK + uint32(s[i])
}
return h
}
// A MphMatcherGroup is divided into three parts:
// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table;
// 2. `substr` patterns are matched by ac automaton;
// 3. `regex` patterns are matched with the regex library.
type MphMatcherGroup struct {
Ac *ACAutomaton
OtherMatchers []MatcherEntry
Rules []string
Level0 []uint32
Level0Mask int
Level1 []uint32
Level1Mask int
Count uint32
RuleMap *map[string]uint32
}
func (g *MphMatcherGroup) AddFullOrDomainPattern(pattern string, t Type) {
h := RollingHash(pattern)
switch t {
case Domain:
(*g.RuleMap)["."+pattern] = h*PrimeRK + uint32('.')
fallthrough
case Full:
(*g.RuleMap)[pattern] = h
default:
}
}
func NewMphMatcherGroup() *MphMatcherGroup {
return &MphMatcherGroup{
Ac: nil,
OtherMatchers: nil,
Rules: nil,
Level0: nil,
Level0Mask: 0,
Level1: nil,
Level1Mask: 0,
Count: 1,
RuleMap: &map[string]uint32{},
}
}
// AddPattern adds a pattern to MphMatcherGroup
func (g *MphMatcherGroup) AddPattern(pattern string, t Type) (uint32, error) {
switch t {
case Substr:
if g.Ac == nil {
g.Ac = NewACAutomaton()
}
g.Ac.Add(pattern, t)
case Full, Domain:
pattern = strings.ToLower(pattern)
g.AddFullOrDomainPattern(pattern, t)
case Regex:
r, err := regexp.Compile(pattern)
if err != nil {
return 0, err
}
g.OtherMatchers = append(g.OtherMatchers, MatcherEntry{
M: &RegexMatcher{Pattern: pattern, reg: r},
Id: g.Count,
})
default:
panic("Unknown type")
}
return g.Count, nil
}
// Build builds a minimal perfect hash table and ac automaton from insert rules
func (g *MphMatcherGroup) Build() {
if g.Ac != nil {
g.Ac.Build()
}
keyLen := len(*g.RuleMap)
if keyLen == 0 {
keyLen = 1
(*g.RuleMap)["empty___"] = RollingHash("empty___")
}
g.Level0 = make([]uint32, nextPow2(keyLen/4))
g.Level0Mask = len(g.Level0) - 1
g.Level1 = make([]uint32, nextPow2(keyLen))
g.Level1Mask = len(g.Level1) - 1
sparseBuckets := make([][]int, len(g.Level0))
var ruleIdx int
for rule, hash := range *g.RuleMap {
n := int(hash) & g.Level0Mask
g.Rules = append(g.Rules, rule)
sparseBuckets[n] = append(sparseBuckets[n], ruleIdx)
ruleIdx++
}
g.RuleMap = nil
var buckets []indexBucket
for n, vals := range sparseBuckets {
if len(vals) > 0 {
buckets = append(buckets, indexBucket{n, vals})
}
}
sort.Sort(bySize(buckets))
occ := make([]bool, len(g.Level1))
var tmpOcc []int
for _, bucket := range buckets {
seed := uint32(0)
for {
findSeed := true
tmpOcc = tmpOcc[:0]
for _, i := range bucket.vals {
n := int(strhashFallback(unsafe.Pointer(&g.Rules[i]), uintptr(seed))) & g.Level1Mask
if occ[n] {
for _, n := range tmpOcc {
occ[n] = false
}
seed++
findSeed = false
break
}
occ[n] = true
tmpOcc = append(tmpOcc, n)
g.Level1[n] = uint32(i)
}
if findSeed {
g.Level0[bucket.n] = seed
break
}
}
}
}
func nextPow2(v int) int {
if v <= 1 {
return 1
}
const MaxUInt = ^uint(0)
n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1
return int(n)
}
// Lookup searches for s in t and returns its index and whether it was found.
func (g *MphMatcherGroup) Lookup(h uint32, s string) bool {
i0 := int(h) & g.Level0Mask
seed := g.Level0[i0]
i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.Level1Mask
n := g.Level1[i1]
return s == g.Rules[int(n)]
}
// Match implements IndexMatcher.Match.
func (g *MphMatcherGroup) Match(pattern string) []uint32 {
result := []uint32{}
hash := uint32(0)
for i := len(pattern) - 1; i >= 0; i-- {
hash = hash*PrimeRK + uint32(pattern[i])
if pattern[i] == '.' {
if g.Lookup(hash, pattern[i:]) {
result = append(result, 1)
return result
}
}
}
if g.Lookup(hash, pattern) {
result = append(result, 1)
return result
}
if g.Ac != nil && g.Ac.Match(pattern) {
result = append(result, 1)
return result
}
for _, e := range g.OtherMatchers {
if e.M.Match(pattern) {
result = append(result, e.Id)
return result
}
}
return nil
}
type indexBucket struct {
n int
vals []int
}
type bySize []indexBucket
func (s bySize) Len() int { return len(s) }
func (s bySize) Less(i, j int) bool { return len(s[i].vals) > len(s[j].vals) }
func (s bySize) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
type stringStruct struct {
str unsafe.Pointer
len int
}
func strhashFallback(a unsafe.Pointer, h uintptr) uintptr {
x := (*stringStruct)(a)
return memhashFallback(x.str, h, uintptr(x.len))
}
const (
// Constants for multiplication: four random odd 64-bit numbers.
m1 = 16877499708836156737
m2 = 2820277070424839065
m3 = 9497967016996688599
m4 = 15839092249703872147
)
var hashkey = [4]uintptr{1, 1, 1, 1}
func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr {
h := uint64(seed + s*hashkey[0])
tail:
switch {
case s == 0:
case s < 4:
h ^= uint64(*(*byte)(p))
h ^= uint64(*(*byte)(add(p, s>>1))) << 8
h ^= uint64(*(*byte)(add(p, s-1))) << 16
h = rotl31(h*m1) * m2
case s <= 8:
h ^= uint64(readUnaligned32(p))
h ^= uint64(readUnaligned32(add(p, s-4))) << 32
h = rotl31(h*m1) * m2
case s <= 16:
h ^= readUnaligned64(p)
h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, s-8))
h = rotl31(h*m1) * m2
case s <= 32:
h ^= readUnaligned64(p)
h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, 8))
h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, s-16))
h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, s-8))
h = rotl31(h*m1) * m2
default:
v1 := h
v2 := uint64(seed * hashkey[1])
v3 := uint64(seed * hashkey[2])
v4 := uint64(seed * hashkey[3])
for s >= 32 {
v1 ^= readUnaligned64(p)
v1 = rotl31(v1*m1) * m2
p = add(p, 8)
v2 ^= readUnaligned64(p)
v2 = rotl31(v2*m2) * m3
p = add(p, 8)
v3 ^= readUnaligned64(p)
v3 = rotl31(v3*m3) * m4
p = add(p, 8)
v4 ^= readUnaligned64(p)
v4 = rotl31(v4*m4) * m1
p = add(p, 8)
s -= 32
}
h = v1 ^ v2 ^ v3 ^ v4
goto tail
}
h ^= h >> 29
h *= m3
h ^= h >> 32
return uintptr(h)
}
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(p) + x)
}
func readUnaligned32(p unsafe.Pointer) uint32 {
q := (*[4]byte)(p)
return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24
}
func rotl31(x uint64) uint64 {
return (x << 31) | (x >> (64 - 31))
}
func readUnaligned64(p unsafe.Pointer) uint64 {
q := (*[8]byte)(p)
return uint64(q[0]) | uint64(q[1])<<8 | uint64(q[2])<<16 | uint64(q[3])<<24 | uint64(q[4])<<32 | uint64(q[5])<<40 | uint64(q[6])<<48 | uint64(q[7])<<56
}
func (g *MphMatcherGroup) Size() uint32 {
return g.Count
}

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