Compare commits

..
34 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
117 changed files with 7765 additions and 1032 deletions
+2 -2
View File
@@ -45,8 +45,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+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
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+38 -27
View File
@@ -158,9 +158,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, client)
}
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
var domainMatcher geodata.DomainMatcher
if len(effectiveRules) > 0 {
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
}
// If there is no DNS client in config, add a `localhost` DNS client
@@ -271,24 +274,27 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching
hasMatch := false
MatchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(MatchSlice, func(i, j int) bool {
return MatchSlice[i] < MatchSlice[j]
})
for _, match := range MatchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
return clients
if s.domainMatcher != nil {
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
}
@@ -302,17 +308,13 @@ func (s *DNS) sortClients(domain string) []*Client {
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
}
if len(domainRules) > 0 {
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)
}
logDecision(s.ctx, domain, domainRules, clientNames)
if len(clients) == 0 {
if len(s.clients) > 0 {
@@ -327,6 +329,15 @@ func (s *DNS) sortClients(domain string) []*Client {
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 {
if len(errs) == 0 {
return dns.ErrEmptyResponse
+36 -76
View File
@@ -147,7 +147,9 @@ func TestUDPServerSubnet(t *testing.T) {
},
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{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -345,7 +349,9 @@ func TestPrioritizedDomain(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -414,7 +420,9 @@ func TestUDPServerIPv6(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -481,7 +489,9 @@ func TestStaticHostDomain(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -538,15 +548,8 @@ func TestIPMatch(t *testing.T) {
Port: uint32(port),
},
ExpectedIp: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
// inner ip, will not match
Ip: []byte{192, 168, 11, 1},
Prefix: 32,
},
},
},
// inner ip, will not match
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
},
},
// second dns, match ip
@@ -561,22 +564,8 @@ func TestIPMatch(t *testing.T) {
Port: uint32(port),
},
ExpectedIp: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 4},
Prefix: 32,
},
},
},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
},
},
},
@@ -587,7 +576,9 @@ func TestIPMatch(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -664,9 +655,9 @@ func TestLocalDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will match localhost, localhost-a and localhost-b,
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}},
},
},
{
@@ -704,7 +695,9 @@ func TestLocalDomain(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -887,22 +880,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.8.8 and 8.8.4.4
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 4, 4},
Prefix: 32,
},
},
},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{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}}}},
},
},
{
@@ -922,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will match 8.8.8.8 and 8.8.8.7, etc
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 7},
Prefix: 24,
},
},
},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
},
},
{
@@ -949,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.7.7 (api.google.com)
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 7, 7},
Prefix: 32,
},
},
},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
},
},
{
@@ -976,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.7.8 (v2.api.google.com)
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 7, 8},
Prefix: 32,
},
},
},
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
},
},
},
@@ -994,7 +952,9 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+14 -11
View File
@@ -13,8 +13,8 @@ import (
// StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct {
reps [][]net.Address
matcher geodata.DomainMatcher
responses [][]net.Address
matcher geodata.DomainMatcher
}
// NewStaticHosts creates a new StaticHosts instance.
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
rep = append(rep, addr)
}
}
// if len(rep) == 0 {
// errors.LogError(context.Background(), "empty value in static hosts, ignore this rule: ", mapping.Domain)
// continue
// }
reps = append(reps, rep)
rules = append(rules, mapping.Domain)
}
if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
reps: reps,
matcher: matcher,
responses: reps,
matcher: matcher,
}, nil
}
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0)
found := false
for _, ruleIdx := range h.matcher.Match(domain) {
for _, rep := range h.reps[ruleIdx] {
for _, idx := range h.matcher.Match(domain) {
for _, rep := range h.responses[idx] {
if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
return nil, err
}
}
ips = append(ips, h.reps[ruleIdx]...)
ips = append(ips, h.responses[idx]...)
found = true
}
if !found {
@@ -122,5 +122,8 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5)
}
+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 -1
View File
@@ -308,7 +308,7 @@ func TestServiceTestRoute(t *testing.T) {
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
+8 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
)
type Condition interface {
@@ -356,7 +357,13 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
var dstIP string
var dstPort uint16 = 0
if len(ctx.GetTargetIPs()) > 0 {
// do not use resolved IP because Android process lookup needs original dst ip
resolvableContext, ok := ctx.(*dns.ResolvableContext)
if ok && len(resolvableContext.Context.GetTargetIPs()) > 0 {
dstIP = resolvableContext.Context.GetTargetIPs()[0].String()
dstPort = uint16(resolvableContext.Context.GetTargetPort())
} else if len(ctx.GetTargetIPs()) > 0 {
dstIP = ctx.GetTargetIPs()[0].String()
dstPort = uint16(ctx.GetTargetPort())
}
+8 -12
View File
@@ -92,25 +92,22 @@ func TestRoutingRule(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
Prefix: 128,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
},
},
},
@@ -140,9 +137,8 @@ func TestRoutingRule(t *testing.T) {
SourceIp: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{192, 168, 0, 0},
Prefix: 16,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
},
},
},
+6 -9
View File
@@ -159,9 +159,8 @@ func TestIPOnDemand(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{192, 168, 0, 0},
Prefix: 16,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
},
},
},
@@ -204,9 +203,8 @@ func TestIPIfNonMatchDomain(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{192, 168, 0, 0},
Prefix: 16,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
},
},
},
@@ -249,9 +247,8 @@ func TestIPIfNonMatchIP(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{127, 0, 0, 0},
Prefix: 8,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
},
},
},
+84 -30
View File
@@ -11,7 +11,11 @@ import (
)
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
}
@@ -19,10 +23,54 @@ type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}
type MphDomainMatcherFactory struct{}
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) {
@@ -53,25 +101,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
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.MatcherGroup // TODO: cleanup
shared map[string]strmatcher.MatcherSet // TODO: cleanup
}
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, error) {
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
f.Lock()
defer f.Unlock()
if m := f.shared[key]; m != nil {
return m, nil
if s := 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)
g := strmatcher.NewLinearValueMatcher()
s := strmatcher.NewLinearAnyMatcher()
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
if err != nil {
return nil, err
@@ -83,60 +136,58 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
g.Add(m, 0)
s.Add(m)
}
f.shared[key] = g
return g, err
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.MatcherGroup, 0, len(rules)),
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
custom := strmatcher.NewLinearValueMatcher()
var idx uint32
for _, r := range 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
}
custom.Add(m, 0)
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, idx)
idx++
compact.values = append(compact.values, uint32(i))
default:
panic("unknown domain rule type")
}
}
if len(compact.matchers) != len(rules) {
compact.matchers = append(compact.matchers, custom)
compact.values = append(compact.values, idx+1)
}
return compact, nil
}
type CompactDomainMatcher struct {
matchers []strmatcher.MatcherGroup
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherSet
values []uint32
}
func (c *CompactDomainMatcher) Add(matcher strmatcher.MatcherGroup, value uint32) {
c.matchers = append(c.matchers, matcher)
c.values = append(c.values, value)
}
// Match implements DomainMatcher.
func (c *CompactDomainMatcher) Match(input string) []uint32 {
result := make([]uint32, 0)
var result []uint32
if c.custom != nil {
result = append(result, c.custom.Match(input)...)
}
for i, m := range c.matchers {
if m.MatchAny(input) {
result = append(result, c.values[i])
@@ -147,6 +198,9 @@ func (c *CompactDomainMatcher) Match(input string) []uint32 {
// 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
@@ -175,9 +229,9 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
default:
return &MphDomainMatcherFactory{}
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})
}
}
+79 -2
View File
@@ -1,11 +1,59 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
)
type DomainRegistry struct {
factory DomainMatcherFactory
mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
return r.factory.BuildMatcher(rules)
r.mu.Lock()
defer r.mu.Unlock()
m, err := r.factory.BuildMatcher(rules)
if err != nil {
return nil, err
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *DomainRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
factory := newDomainMatcherFactory()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
return nil
}
func newDomainRegistry() *DomainRegistry {
@@ -15,3 +63,32 @@ func newDomainRegistry() *DomainRegistry {
}
var DomainReg = newDomainRegistry()
type domainMatcherState struct {
matcher DomainMatcher
}
type DynamicDomainMatcher struct {
rules []*DomainRule
state atomic.Pointer[domainMatcherState]
}
// Match implements DomainMatcher.
func (d *DynamicDomainMatcher) Match(input string) []uint32 {
return d.state.Load().matcher.Match(input)
}
// MatchAny implements DomainMatcher.
func (d *DynamicDomainMatcher) MatchAny(input string) bool {
return d.state.Load().matcher.MatchAny(input)
}
func (d *DynamicDomainMatcher) Reload(newMatcher DomainMatcher) {
d.state.Store(&domainMatcherState{matcher: newMatcher})
}
func NewDynamicDomainMatcher(rules []*DomainRule, matcher DomainMatcher) *DynamicDomainMatcher {
d := &DynamicDomainMatcher{rules: rules}
d.Reload(matcher)
return d
}
+96 -39
View File
@@ -433,6 +433,58 @@ func (x *CIDR) GetPrefix() uint32 {
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"`
@@ -444,7 +496,7 @@ type GeoIP struct {
func (x *GeoIP) Reset() {
*x = GeoIP{}
mi := &file_common_geodata_geodat_proto_msgTypes[6]
mi := &file_common_geodata_geodat_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -456,7 +508,7 @@ func (x *GeoIP) String() string {
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[6]
mi := &file_common_geodata_geodat_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -469,7 +521,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
}
func (x *GeoIP) GetCode() string {
@@ -502,7 +554,7 @@ type GeoIPList struct {
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
mi := &file_common_geodata_geodat_proto_msgTypes[7]
mi := &file_common_geodata_geodat_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -514,7 +566,7 @@ func (x *GeoIPList) String() string {
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[7]
mi := &file_common_geodata_geodat_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -527,7 +579,7 @@ func (x *GeoIPList) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
@@ -548,7 +600,7 @@ type GeoIPRule struct {
func (x *GeoIPRule) Reset() {
*x = GeoIPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[8]
mi := &file_common_geodata_geodat_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -560,7 +612,7 @@ func (x *GeoIPRule) String() string {
func (*GeoIPRule) ProtoMessage() {}
func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[8]
mi := &file_common_geodata_geodat_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -573,7 +625,7 @@ func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
func (*GeoIPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
}
func (x *GeoIPRule) GetFile() string {
@@ -610,7 +662,7 @@ type IPRule struct {
func (x *IPRule) Reset() {
*x = IPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[9]
mi := &file_common_geodata_geodat_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -622,7 +674,7 @@ func (x *IPRule) String() string {
func (*IPRule) ProtoMessage() {}
func (x *IPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[9]
mi := &file_common_geodata_geodat_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -635,7 +687,7 @@ func (x *IPRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
func (*IPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10}
}
func (x *IPRule) GetValue() isIPRule_Value {
@@ -654,7 +706,7 @@ func (x *IPRule) GetGeoip() *GeoIPRule {
return nil
}
func (x *IPRule) GetCustom() *CIDR {
func (x *IPRule) GetCustom() *CIDRRule {
if x != nil {
if x, ok := x.Value.(*IPRule_Custom); ok {
return x.Custom
@@ -672,7 +724,7 @@ type IPRule_Geoip struct {
}
type IPRule_Custom struct {
Custom *CIDR `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
}
func (*IPRule_Geoip) isIPRule_Value() {}
@@ -693,7 +745,7 @@ type Domain_Attribute struct {
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
mi := &file_common_geodata_geodat_proto_msgTypes[10]
mi := &file_common_geodata_geodat_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -705,7 +757,7 @@ func (x *Domain_Attribute) String() string {
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[10]
mi := &file_common_geodata_geodat_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -807,7 +859,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
"\x05value\".\n" +
"\x04CIDR\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"o\n" +
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" +
"\bCIDRRule\x12-\n" +
"\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" +
"\x05GeoIP\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
@@ -817,10 +872,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
"\tGeoIPRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"~\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" +
"\x06IPRule\x126\n" +
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x123\n" +
"\x06custom\x18\x02 \x01(\v2\x19.xray.common.geodata.CIDRH\x00R\x06customB\a\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"
@@ -837,7 +892,7 @@ func file_common_geodata_geodat_proto_rawDescGZIP() []byte {
}
var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
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
@@ -846,28 +901,30 @@ var file_common_geodata_geodat_proto_goTypes = []any{
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
(*GeoIP)(nil), // 7: xray.common.geodata.GeoIP
(*GeoIPList)(nil), // 8: xray.common.geodata.GeoIPList
(*GeoIPRule)(nil), // 9: xray.common.geodata.GeoIPRule
(*IPRule)(nil), // 10: xray.common.geodata.IPRule
(*Domain_Attribute)(nil), // 11: xray.common.geodata.Domain.Attribute
(*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
11, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain
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.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
7, // 7: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
9, // 8: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
6, // 9: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDR
10, // [10:10] is the sub-list for method output_type
10, // [10:10] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
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() }
@@ -879,11 +936,11 @@ func file_common_geodata_geodat_proto_init() {
(*DomainRule_Geosite)(nil),
(*DomainRule_Custom)(nil),
}
file_common_geodata_geodat_proto_msgTypes[9].OneofWrappers = []any{
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
(*IPRule_Geoip)(nil),
(*IPRule_Custom)(nil),
}
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
@@ -893,7 +950,7 @@ func file_common_geodata_geodat_proto_init() {
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: 11,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
+6 -1
View File
@@ -66,6 +66,11 @@ message CIDR {
uint32 prefix = 2;
}
message CIDRRule {
CIDR cidr = 1;
bool reverse_match = 2;
}
message GeoIP {
string code = 1;
repeated CIDR cidr = 2;
@@ -85,6 +90,6 @@ message GeoIPRule {
message IPRule {
oneof value {
GeoIPRule geoip = 1;
CIDR custom = 2;
CIDRRule custom = 2;
}
}
+41 -15
View File
@@ -816,8 +816,10 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
defer f.Unlock()
if ipset := f.shared[key]; ipset != nil {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil
}
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules {
@@ -915,24 +917,31 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
return nil, errors.New("failed to build IPv6 set").Base(err)
}
var has4, has6 bool
var max4, max6 int
for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 {
max4 = b
}
}
for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 {
max6 = b
}
}
if max4 == 0 {
if !has4 {
max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
}
if max6 == 0 {
if !has6 {
max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
}
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
@@ -940,45 +949,58 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) {
n := len(rules)
custom := make([]*CIDR, 0, n)
pos := make([]*GeoIPRule, 0, n)
neg := make([]*GeoIPRule, 0, n)
posCustom := make([]*CIDR, 0, n)
negCustom := make([]*CIDR, 0, n)
posGeoip := make([]*GeoIPRule, 0, n)
negGeoip := make([]*GeoIPRule, 0, n)
for _, r := range rules {
switch v := r.Value.(type) {
case *IPRule_Custom:
custom = append(custom, v.Custom)
if !v.Custom.ReverseMatch {
posCustom = append(posCustom, v.Custom.Cidr)
} else {
negCustom = append(negCustom, v.Custom.Cidr)
}
case *IPRule_Geoip:
if !v.Geoip.ReverseMatch {
pos = append(pos, v.Geoip)
posGeoip = append(posGeoip, v.Geoip)
} else {
neg = append(neg, v.Geoip)
negGeoip = append(negGeoip, v.Geoip)
}
default:
panic("unknown ip rule type")
}
}
subs := make([]*HeuristicIPMatcher, 0, 3)
subs := make([]*HeuristicIPMatcher, 0, 4)
if len(custom) > 0 {
ipset, err := f.CreateFromCIDRs(custom)
if len(posCustom) > 0 {
ipset, err := f.CreateFromCIDRs(posCustom)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
}
if len(pos) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(pos)
if len(negCustom) > 0 {
ipset, err := f.CreateFromCIDRs(negCustom)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
}
if len(posGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
}
if len(neg) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(neg)
if len(negGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
if err != nil {
return nil, err
}
@@ -994,3 +1016,7 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
return &HeuristicMultiIPMatcher{matchers: subs}, nil
}
}
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: make(map[string]*IPSet)}
}
+112
View File
@@ -97,6 +97,90 @@ func TestIPMatcher(t *testing.T) {
}
}
func TestIPMatcherFullCIDR4(t *testing.T) {
matcher := buildIPMatcher(
"0.0.0.0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "0.0.0.0",
Output: true,
},
{
Input: "255.255.255.255",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "::0",
Output: false,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherFullCIDR6(t *testing.T) {
matcher := buildIPMatcher(
"::0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: false,
},
{
Input: "0.0.0.0",
Output: false,
},
{
Input: "255.255.255.255",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "::0",
Output: true,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: true,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherRegression(t *testing.T) {
matcher := buildIPMatcher(
"98.108.20.0/22",
@@ -189,6 +273,34 @@ func TestIPReverseMatcher2(t *testing.T) {
}
}
func TestIPCustomReverseMatcher(t *testing.T) {
matcher := buildIPMatcher("!8.8.8.8/32")
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "1.1.1.1",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
+120 -2
View File
@@ -1,17 +1,135 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
return buildOptimizedIPMatcher(r.ipsetFactory, rules)
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
return nil
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
ipsetFactory: newIPSetFactory(),
}
}
var IPReg = newIPRegistry()
type ipMatcherState struct {
matcher IPMatcher
}
type DynamicIPMatcher struct {
rules []*IPRule
state atomic.Pointer[ipMatcherState]
mu sync.Mutex
reverse bool
reverseSet bool
}
// Match implements IPMatcher.
func (d *DynamicIPMatcher) Match(ip net.IP) bool {
return d.state.Load().matcher.Match(ip)
}
// AnyMatch implements IPMatcher.
func (d *DynamicIPMatcher) AnyMatch(ips []net.IP) bool {
return d.state.Load().matcher.AnyMatch(ips)
}
// Matches implements IPMatcher.
func (d *DynamicIPMatcher) Matches(ips []net.IP) bool {
return d.state.Load().matcher.Matches(ips)
}
// FilterIPs implements IPMatcher.
func (d *DynamicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
return d.state.Load().matcher.FilterIPs(ips)
}
// ToggleReverse implements IPMatcher.
func (d *DynamicIPMatcher) ToggleReverse() {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = !d.reverse
d.state.Load().matcher.ToggleReverse()
}
// SetReverse implements IPMatcher.
func (d *DynamicIPMatcher) SetReverse(reverse bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = reverse
d.reverseSet = true
d.state.Load().matcher.SetReverse(reverse)
}
func (d *DynamicIPMatcher) Reload(newMatcher IPMatcher) {
d.mu.Lock()
defer d.mu.Unlock()
if d.reverseSet {
newMatcher.SetReverse(d.reverse)
} else if d.reverse {
newMatcher.ToggleReverse()
}
d.state.Store(&ipMatcherState{matcher: newMatcher})
}
func NewDynamicIPMatcher(rules []*IPRule, matcher IPMatcher) *DynamicIPMatcher {
d := &DynamicIPMatcher{rules: rules}
d.Reload(matcher)
return d
}
+21 -10
View File
@@ -17,6 +17,8 @@ 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:"):]
}
@@ -32,9 +34,9 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
var rule isIPRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoIPRule(r[prefix:])
rule, err = parseGeoIPRule(r[prefix:], reverse)
} else {
rule, err = parseCustomIPRule(r)
rule, err = parseCustomIPRule(r, reverse)
}
if err != nil {
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
@@ -45,7 +47,16 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
return ipRules, nil
}
func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
func cutReversePrefix(s string) (string, bool) {
reverse := false
for strings.HasPrefix(s, "!") {
s = s[1:]
reverse = !reverse
}
return s, reverse
}
func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
file, code, ok := strings.Cut(rule, ":")
if !ok {
return nil, errors.New("syntax error")
@@ -55,11 +66,8 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
return nil, errors.New("empty file")
}
reverse := false
if strings.HasPrefix(code, "!") {
code = code[1:]
reverse = true
}
code, codeReverse := cutReversePrefix(code)
reverse = reverse != codeReverse
if code == "" {
return nil, errors.New("empty code")
}
@@ -78,13 +86,16 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
}, nil
}
func parseCustomIPRule(rule string) (*IPRule_Custom, error) {
func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
cidr, err := parseCIDR(rule)
if err != nil {
return nil, err
}
return &IPRule_Custom{
Custom: cidr,
Custom: &CIDRRule{
Cidr: cidr,
ReverseMatch: reverse,
},
}, nil
}
+55
View File
@@ -13,12 +13,20 @@ func TestParseIPRules(t *testing.T) {
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::",
@@ -30,6 +38,53 @@ func TestParseIPRules(t *testing.T) {
}
}
func TestParseIPRuleReverse(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, tt := range []struct {
rule string
reverse bool
}{
{rule: "!192.168.0.0/24", reverse: true},
{rule: "!!192.168.0.0/24", reverse: false},
{rule: "!!!192.168.0.0/24", reverse: true},
{rule: "!!!!192.168.0.0/24", reverse: false},
{rule: "geoip:cn", reverse: false},
{rule: "!geoip:cn", reverse: true},
{rule: "!!geoip:cn", reverse: false},
{rule: "geoip:!cn", reverse: true},
{rule: "geoip:!!cn", reverse: false},
{rule: "!geoip:!cn", reverse: false},
{rule: "!!geoip:!cn", reverse: true},
{rule: "!geoip:!!cn", reverse: true},
{rule: "ext:geoip.dat:!!!cn", reverse: true},
} {
t.Run(tt.rule, func(t *testing.T) {
rules, err := geodata.ParseIPRules([]string{tt.rule})
if err != nil {
t.Fatalf("Failed to parse ip rules, got %s", err)
}
if len(rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(rules))
}
switch rule := rules[0]; {
case rule.GetGeoip() != nil:
if rule.GetGeoip().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected geoip reverse match to be %t", tt.reverse)
}
case rule.GetCustom() != nil:
if rule.GetCustom().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected custom reverse match to be %t", tt.reverse)
}
default:
t.Fatal("Expected ip rule")
}
})
}
}
func TestParseDomainRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
@@ -0,0 +1,53 @@
package strmatcher
// LinearAnyMatcher is an implementation of AnyMatcher.
type LinearAnyMatcher struct {
full *FullMatcherSet
domain *DomainMatcherSet
substr *SubstrMatcherSet
regex *SimpleMatcherSet
}
func NewLinearAnyMatcher() *LinearAnyMatcher {
return new(LinearAnyMatcher)
}
// Add implements AnyMatcher.Add.
func (s *LinearAnyMatcher) Add(matcher Matcher) {
switch matcher := matcher.(type) {
case FullMatcher:
if s.full == nil {
s.full = NewFullMatcherSet()
}
s.full.AddFullMatcher(matcher)
case DomainMatcher:
if s.domain == nil {
s.domain = NewDomainMatcherSet()
}
s.domain.AddDomainMatcher(matcher)
case SubstrMatcher:
if s.substr == nil {
s.substr = new(SubstrMatcherSet)
}
s.substr.AddSubstrMatcher(matcher)
default:
if s.regex == nil {
s.regex = new(SimpleMatcherSet)
}
s.regex.AddMatcher(matcher)
}
}
// MatchAny implements AnyMatcher.MatchAny.
func (s *LinearAnyMatcher) MatchAny(input string) bool {
if s.full != nil && s.full.MatchAny(input) {
return true
}
if s.domain != nil && s.domain.MatchAny(input) {
return true
}
if s.substr != nil && s.substr.MatchAny(input) {
return true
}
return s.regex != nil && s.regex.MatchAny(input)
}
+64 -6
View File
@@ -3,6 +3,7 @@ package strmatcher
import (
"errors"
"regexp"
"slices"
"strings"
"unicode/utf8"
@@ -99,10 +100,6 @@ func (t Type) New(pattern string) (Matcher, error) {
case Substr:
return SubstrMatcher(pattern), nil
case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern)
@@ -253,13 +250,12 @@ func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error {
}
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
// It is designed to avoid new memory allocation as possible.
func CompositeMatches(matches [][]uint32) []uint32 {
switch len(matches) {
case 0:
return nil
case 1:
return matches[0]
return slices.Clone(matches[0])
default:
result := make([]uint32, 0, 5)
for i := 0; i < len(matches); i++ {
@@ -288,3 +284,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
return result
}
}
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
type MatcherSetForAll interface {
AddMatcher(matcher Matcher)
}
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
type MatcherSetForFull interface {
AddFullMatcher(matcher FullMatcher)
}
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
type MatcherSetForDomain interface {
AddDomainMatcher(matcher DomainMatcher)
}
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
type MatcherSetForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher)
}
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
type MatcherSetForRegex interface {
AddRegexMatcher(matcher *RegexMatcher)
}
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
// It returns error if the MatcherSet does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherSet.
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
if s, ok := s.(IndexMatcher); ok {
s.Add(matcher)
return nil
}
if s, ok := s.(MatcherSetForAll); ok {
s.AddMatcher(matcher)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if s, ok := s.(MatcherSetForFull); ok {
s.AddFullMatcher(matcher)
return nil
}
case DomainMatcher:
if s, ok := s.(MatcherSetForDomain); ok {
s.AddDomainMatcher(matcher)
return nil
}
case SubstrMatcher:
if s, ok := s.(MatcherSetForSubstr); ok {
s.AddSubstrMatcher(matcher)
return nil
}
case *RegexMatcher:
if s, ok := s.(MatcherSetForRegex); ok {
s.AddRegexMatcher(matcher)
return nil
}
}
return errors.New("cannot add matcher to matcher set")
}
@@ -0,0 +1,79 @@
package strmatcher
type trieNode2 struct {
matched bool
children map[string]*trieNode2
}
// DomainMatcherSet is an implementation of MatcherSet.
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
type DomainMatcherSet struct {
root *trieNode2
}
func NewDomainMatcherSet() *DomainMatcherSet {
return &DomainMatcherSet{
root: new(trieNode2),
}
}
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
node := s.root
pattern := matcher.Pattern()
for i := len(pattern); i > 0; {
var part string
for j := i - 1; ; j-- {
if pattern[j] == '.' {
part = pattern[j+1 : i]
i = j
break
}
if j == 0 {
part = pattern[j:i]
i = j
break
}
}
if node.children == nil {
node.children = make(map[string]*trieNode2)
}
next := node.children[part]
if next == nil {
next = new(trieNode2)
node.children[part] = next
}
node = next
}
node.matched = true
}
// MatchAny implements MatcherSet.MatchAny.
func (s *DomainMatcherSet) MatchAny(input string) bool {
node := s.root
for i := len(input); i > 0; {
for j := i - 1; ; j-- {
if input[j] == '.' {
node = node.children[input[j+1:i]]
i = j
break
}
if j == 0 {
node = node.children[input[j:i]]
i = j
break
}
}
if node == nil {
return false
}
if node.matched {
return true
}
if node.children == nil {
return false
}
}
return false
}
@@ -0,0 +1,95 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestDomainMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "a.b.com",
},
{
Pattern: "c.a.b.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "x.example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "a.b.com",
Result: true,
},
{
Domain: "c.a.b.com",
Result: true,
},
{
Domain: "c.a..b.com",
Result: false,
},
{
Domain: ".com",
Result: false,
},
{
Domain: "com",
Result: false,
},
{
Domain: "",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewDomainMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyDomainMatcherSet(t *testing.T) {
s := NewDomainMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -0,0 +1,24 @@
package strmatcher
// FullMatcherSet is an implementation of MatcherSet.
// It uses a hash table to facilitate exact match lookup.
type FullMatcherSet struct {
matchers map[string]struct{}
}
func NewFullMatcherSet() *FullMatcherSet {
return &FullMatcherSet{
matchers: make(map[string]struct{}),
}
}
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
s.matchers[matcher.Pattern()] = struct{}{}
}
// MatchAny implements MatcherSet.Any.
func (s *FullMatcherSet) MatchAny(input string) bool {
_, found := s.matchers[input]
return found
}
@@ -0,0 +1,65 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestFullMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewFullMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyFullMatcherSet(t *testing.T) {
s := NewFullMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -0,0 +1,22 @@
package strmatcher
// SimpleMatcherSet is an implementation of MatcherSet.
// It simply stores all matchers in an array and sequentially matches them.
type SimpleMatcherSet struct {
matchers []Matcher
}
// AddMatcher implements MatcherSetForAll.AddMatcher.
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
s.matchers = append(s.matchers, matcher)
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SimpleMatcherSet) MatchAny(input string) bool {
for _, m := range s.matchers {
if m.Match(input) {
return true
}
}
return false
}
@@ -0,0 +1,69 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSimpleMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "example.com",
mType: Domain,
},
{
pattern: "example.com",
mType: Full,
},
{
pattern: "example.com",
mType: Regex,
},
}
cases := []struct {
input string
output bool
}{
{
input: "www.example.com",
output: true,
},
{
input: "example.com",
output: true,
},
{
input: "www.e3ample.com",
output: false,
},
{
input: "xample.com",
output: false,
},
{
input: "xexample.com",
output: true,
},
{
input: "examplexcom",
output: true,
},
}
matcherSet := &SimpleMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
@@ -0,0 +1,24 @@
package strmatcher
import "strings"
// SubstrMatcherSet is implementation of MatcherSet,
// It is simply implmeneted to comply with the priority specification of Substr matchers.
type SubstrMatcherSet struct {
patterns []string
}
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
s.patterns = append(s.patterns, matcher.Pattern())
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SubstrMatcherSet) MatchAny(input string) bool {
for _, pattern := range s.patterns {
if strings.Contains(input, pattern) {
return true
}
}
return false
}
@@ -0,0 +1,77 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSubstrMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "apis",
mType: Substr,
},
{
pattern: "google",
mType: Substr,
},
{
pattern: "apis",
mType: Substr,
},
}
cases := []struct {
input string
output bool
}{
{
input: "google.com",
output: true,
},
{
input: "apis.com",
output: true,
},
{
input: "googleapis.com",
output: true,
},
{
input: "fonts.googleapis.com",
output: true,
},
{
input: "apis.googleapis.com",
output: true,
},
{
input: "baidu.com",
output: false,
},
{
input: "goog",
output: false,
},
{
input: "api",
output: false,
},
}
matcherSet := &SubstrMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
+21 -1
View File
@@ -15,7 +15,7 @@ const (
)
// Matcher is the interface to determine a string matches a pattern.
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
type Matcher interface {
// Type returns the matcher's type.
Type() Type
@@ -62,6 +62,7 @@ type IndexMatcher interface {
// 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.
@@ -89,6 +90,7 @@ type ValueMatcher interface {
// * 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.
@@ -99,3 +101,21 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherSet interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
type AnyMatcher interface {
// Add adds a new Matcher to AnyMatcher.
Add(matcher Matcher)
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
+40 -2
View File
@@ -1,6 +1,7 @@
package filesystem
import (
"errors"
"io"
"os"
"path/filepath"
@@ -26,11 +27,48 @@ func ReadFile(path string) ([]byte, error) {
}
func ReadAsset(file string) ([]byte, error) {
return ReadFile(platform.GetAssetLocation(file))
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
}
func OpenAsset(file string) (io.ReadCloser, error) {
return NewFileReader(platform.GetAssetLocation(file))
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file)
return path, err
}
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
if !filepath.IsLocal(file) || file == "." {
return "", nil, errors.New("asset path must stay in asset directory")
}
local, err := filepath.Localize(file)
if err != nil {
return "", nil, err
}
path := platform.GetAssetLocation(local)
info, err := os.Stat(path)
if err != nil {
return "", nil, err
}
if !info.Mode().IsRegular() {
return "", nil, errors.New("asset is not a regular file")
}
return path, info, nil
}
func ReadCert(file string) ([]byte, error) {
+32
View File
@@ -0,0 +1,32 @@
package filesystem_test
import (
"path/filepath"
"testing"
. "github.com/xtls/xray-core/common/platform/filesystem"
)
func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{
"",
".",
"..",
"../geoip.dat",
"nested/..",
"nested/../geoip.dat",
"nested//geoip.dat",
"/geoip.dat",
"/tmp/geoip.dat",
`C:\geoip.dat`,
`C:geoip.dat`,
`\\server\share\geoip.dat`,
`nested\geoip.dat`,
`nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"),
} {
if _, err := StatAsset(file); err == nil {
t.Fatalf("expected error for %q", file)
}
}
}
+29
View File
@@ -4,6 +4,7 @@ import (
"testing"
. "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
)
func TestGetInstance(t *testing.T) {
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
t.Error("expect nil, but actually not")
}
}
func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) {
msg := &custom.UDPConfig{
Mode: "standalone",
Client: []*custom.UDPItem{
{Rand: 12, Save: "txid"},
},
}
tm := ToTypedMessage(msg)
if tm == nil {
t.Fatal("expected typed message")
}
roundTrip, err := tm.GetInstance()
if err != nil {
t.Fatalf("GetInstance() failed: %v", err)
}
udp, ok := roundTrip.(*custom.UDPConfig)
if !ok {
t.Fatalf("unexpected round-trip type: %T", roundTrip)
}
if udp.GetMode() != "standalone" {
t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode())
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
return
}
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun" || inbound.Name == "wireguard") {
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
h := blake3.New(8, BaseKey)
h.Write([]byte(inbound.Source.String()))
copy(globalID[:], h.Sum(nil))
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 4
Version_z byte = 13
Version_z byte = 25
)
var (
+9 -3
View File
@@ -52,7 +52,9 @@ func TestXrayDial(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -102,7 +104,9 @@ func TestXrayDialUDPConn(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -169,7 +173,9 @@ func TestXrayDialUDP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+7 -3
View File
@@ -80,13 +80,17 @@ func New() *Client {
d := &net.Dialer{
Timeout: time.Second * 16,
Control: func(network, address string, c syscall.RawConn) error {
var errs []error
for _, ctl := range internet.Controllers {
if err := ctl(network, address, c); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
return err
errs = append(errs, err)
}
}
return nil
err := errors.Combine(errs...)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
}
return err
},
}
+4 -3
View File
@@ -12,13 +12,14 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.11.0
github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.0
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
@@ -27,7 +28,7 @@ require (
golang.org/x/sys v0.43.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v0.5.3
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
+8 -4
View File
@@ -45,14 +45,16 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
@@ -67,6 +69,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d h1:ca0n8upCDojatNr25id4npJBwUsMmLgtrvLYDj4J0Hg=
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
@@ -131,8 +135,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
+129 -12
View File
@@ -1,19 +1,70 @@
package conf
import (
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/proxy/dns"
"google.golang.org/protobuf/proto"
)
type DNSOutboundRuleConfig struct {
Action string `json:"action"`
QType *PortList `json:"qtype"`
Domain *StringList `json:"domain"`
}
func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
rule := &dns.DNSRuleConfig{}
switch strings.ToLower(c.Action) {
case "direct":
rule.Action = dns.RuleAction_Direct
case "drop":
rule.Action = dns.RuleAction_Drop
case "reject":
rule.Action = dns.RuleAction_Reject
case "hijack":
rule.Action = dns.RuleAction_Hijack
default:
return nil, errors.New("unknown action: ", c.Action)
}
if c.QType != nil {
for _, r := range c.QType.Range {
if r.From > r.To {
return nil, errors.New("invalid qtype range: ", r.String())
}
if r.To > 65535 {
return nil, errors.New("dns rule qtype out of range: ", r.String())
}
for qtype := r.From; qtype <= r.To; qtype++ {
rule.Qtype = append(rule.Qtype, int32(qtype))
}
}
}
if c.Domain != nil {
rules, err := geodata.ParseDomainRules(*c.Domain, geodata.Domain_Substr)
if err != nil {
return nil, err
}
rule.Domain = rules
}
return rule, nil
}
type DNSOutboundConfig struct {
Network Network `json:"network"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
UserLevel uint32 `json:"userLevel"`
NonIPQuery string `json:"nonIPQuery"`
BlockTypes []int32 `json:"blockTypes"`
Network Network `json:"network"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
UserLevel uint32 `json:"userLevel"`
Rules []*DNSOutboundRuleConfig `json:"rules"`
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
}
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
@@ -27,12 +78,78 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) {
if c.Address != nil {
config.Server.Address = c.Address.Build()
}
switch c.NonIPQuery {
case "", "reject", "drop", "skip":
default:
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
// todo: remove legacy
if c.NonIPQuery != nil || c.BlockTypes != nil {
if c.Rules != nil {
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
}
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes" in DNS outbound`, `"rules"`)
rules, err := c.buildLegacyDNSPolicy()
if err != nil {
return nil, err
}
config.Rule = rules
return config, nil
}
config.Non_IPQuery = c.NonIPQuery
config.BlockTypes = c.BlockTypes
for _, r := range c.Rules {
rule, err := r.Build()
if err != nil {
return nil, err
}
config.Rule = append(config.Rule, rule)
}
return config, nil
}
// todo: remove legacy
func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) {
rules := make([]*dns.DNSRuleConfig, 0, 3)
mode := "reject"
if c.NonIPQuery != nil && *c.NonIPQuery != "" {
mode = *c.NonIPQuery
}
switch mode {
case "", "reject", "drop", "skip":
default:
return nil, errors.New("unknown nonIPQuery: ", mode)
}
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
if mode == "reject" {
rule.Action = dns.RuleAction_Reject
}
for _, qtype := range *c.BlockTypes {
if qtype < 0 || qtype > 65535 {
return nil, errors.New("legacy blockTypes qtype out of range: ", qtype)
}
rule.Qtype = append(rule.Qtype, qtype)
}
rules = append(rules, rule)
}
{
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack}
rule.Qtype = append(rule.Qtype, 1)
rule.Qtype = append(rule.Qtype, 28)
rules = append(rules, rule)
}
{
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject}
if mode == "reject" {
rule.Action = dns.RuleAction_Reject
} else if mode == "drop" {
rule.Action = dns.RuleAction_Drop
} else if mode == "skip" {
rule.Action = dns.RuleAction_Direct
}
rules = append(rules, rule)
}
return rules, nil
}
+205
View File
@@ -1,8 +1,10 @@
package conf_test
import (
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/dns"
@@ -29,5 +31,208 @@ func TestDnsProxyConfig(t *testing.T) {
},
},
},
{
Input: `{
"rules": [{
"action": "direct",
"qtype": "1,3,23-24"
}, {
"action": "drop",
"qtype": 28,
"domain": ["domain:example.com", "full:example.com"]
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Direct,
Qtype: []int32{1, 3, 23, 24},
},
{
Action: dns.RuleAction_Drop,
Qtype: []int32{28},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Domain,
Value: "example.com",
},
},
},
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Full,
Value: "example.com",
},
},
},
},
},
},
},
},
{
Input: `{
"rules": [{
"action": "reject",
"domain": "keyword:example"
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Reject,
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Substr,
Value: "example",
},
},
},
},
},
},
},
},
{
Input: `{
"rules": [{
"action": "drop",
"qtype": 257
}]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Drop,
Qtype: []int32{257},
},
},
},
},
})
}
// todo: remove legacy
func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
creator := func() Buildable {
return new(DNSOutboundConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"blockTypes": []
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Reject,
},
},
},
},
{
Input: `{
"blockTypes": [1, 65]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Reject,
Qtype: []int32{1, 65},
},
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Reject,
},
},
},
},
{
Input: `{
"nonIPQuery": "drop",
"blockTypes": [1]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Drop,
Qtype: []int32{1},
},
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Drop,
},
},
},
},
{
Input: `{
"nonIPQuery": "skip",
"blockTypes": [65, 28]
}`,
Parser: loadJSON(creator),
Output: &dns.Config{
Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{
{
Action: dns.RuleAction_Drop,
Qtype: []int32{65, 28},
},
{
Action: dns.RuleAction_Hijack,
Qtype: []int32{1, 28},
},
{
Action: dns.RuleAction_Direct,
},
},
},
},
})
}
// todo: remove legacy
func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) {
creator := func() Buildable {
return new(DNSOutboundConfig)
}
_, err := loadJSON(creator)(`{
"rules": [{
"action": "direct",
"qtype": 65
}],
"blockTypes": [65]
}`)
if err == nil || !strings.Contains(err.Error(), `legacy nonIPQuery and blockTypes cannot be mixed with rules`) {
t.Fatal("expected mixed legacy/new config error, but got ", err)
}
}
+17 -8
View File
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
v2net "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/proxy/freedom"
@@ -15,14 +16,15 @@ import (
)
type FreedomConfig struct {
TargetStrategy string `json:"targetStrategy"`
DomainStrategy string `json:"domainStrategy"`
Redirect string `json:"redirect"`
UserLevel uint32 `json:"userLevel"`
Fragment *Fragment `json:"fragment"`
Noise *Noise `json:"noise"`
Noises []*Noise `json:"noises"`
ProxyProtocol uint32 `json:"proxyProtocol"`
TargetStrategy string `json:"targetStrategy"`
DomainStrategy string `json:"domainStrategy"`
Redirect string `json:"redirect"`
UserLevel uint32 `json:"userLevel"`
Fragment *Fragment `json:"fragment"`
Noise *Noise `json:"noise"`
Noises []*Noise `json:"noises"`
ProxyProtocol uint32 `json:"proxyProtocol"`
IPsBlocked *StringList `json:"ipsBlocked"`
}
type Fragment struct {
@@ -162,6 +164,13 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 {
config.ProxyProtocol = c.ProxyProtocol
}
if c.IPsBlocked != nil {
rules, err := geodata.ParseIPRules(*c.IPsBlocked)
if err != nil {
return nil, err
}
config.IpsBlocked = &freedom.IPRules{Rules: rules}
}
return config, nil
}
+71
View File
@@ -0,0 +1,71 @@
package conf
import (
"net/url"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
type GeodataAssetConfig struct {
URL string `json:"url"`
File string `json:"file"`
}
func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
if err := validateHTTPS(c.URL); err != nil {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
}
if _, err := filesystem.StatAsset(c.File); err != nil {
return nil, errors.New("invalid geodata asset file: ", c.File).Base(err)
}
return &geodata.Asset{
Url: c.URL,
File: c.File,
}, nil
}
func validateHTTPS(s string) error {
u, err := url.ParseRequestURI(s)
if err != nil {
return err
}
if u.Scheme != "https" || u.Host == "" {
return errors.New("scheme must be https")
}
return nil
}
type GeodataConfig struct {
Cron *string `json:"cron"`
Outbound string `json:"outbound"`
Assets []*GeodataAssetConfig `json:"assets"`
}
func (c *GeodataConfig) Build() (proto.Message, error) {
config := &geodata.Config{}
if c.Cron != nil {
if _, err := cron.ParseStandard(*c.Cron); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
config.Cron = *c.Cron
}
config.Outbound = c.Outbound
assets := make([]*geodata.Asset, 0, len(c.Assets))
for _, asset := range c.Assets {
built, err := asset.Build()
if err != nil {
return nil, err
}
assets = append(assets, built)
}
config.Assets = assets
return config, nil
}
+75
View File
@@ -0,0 +1,75 @@
package conf_test
import (
"path/filepath"
"testing"
"github.com/xtls/xray-core/app/geodata"
. "github.com/xtls/xray-core/infra/conf"
)
func TestGeodataConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
creator := func() Buildable {
return new(GeodataConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"cron": "0 4 * * *",
"outbound": "proxy",
"assets": [
{"url": "https://example.com/geoip.dat", "file": "geoip.dat"},
{"url": "https://example.com/geosite.dat", "file": "geosite.dat"}
]
}`,
Parser: loadJSON(creator),
Output: &geodata.Config{
Cron: "0 4 * * *",
Outbound: "proxy",
Assets: []*geodata.Asset{
{Url: "https://example.com/geoip.dat", File: "geoip.dat"},
{Url: "https://example.com/geosite.dat", File: "geosite.dat"},
},
},
},
})
}
func TestGeodataAssetConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "geoip.dat",
}).Build(); err != nil {
t.Fatal(err)
}
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "missing.dat",
}).Build(); err == nil {
t.Fatal("expected error")
}
}
func TestGeodataAssetConfigInvalidURL(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, rawURL := range []string{
"",
"http://example.com/geoip.dat",
"ftp://example.com/geoip.dat",
"https:///geoip.dat",
} {
if _, err := (&GeodataAssetConfig{
URL: rawURL,
File: "geoip.dat",
}).Build(); err == nil {
t.Fatalf("expected error for %q", rawURL)
}
}
}
+8 -12
View File
@@ -135,17 +135,15 @@ func TestRouterConfig(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{10, 0, 0, 0},
Prefix: 8,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
Prefix: 128,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
},
},
},
@@ -216,17 +214,15 @@ func TestRouterConfig(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{10, 0, 0, 0},
Prefix: 8,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
Prefix: 128,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
},
},
},
+220 -19
View File
@@ -8,6 +8,7 @@ import (
"math"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
"strings"
@@ -1231,6 +1232,8 @@ func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
}
var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) },
@@ -1256,11 +1259,14 @@ var (
)
type TCPItem struct {
Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Type string `json:"type"`
Packet json.RawMessage `json:"packet"`
Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Capture string `json:"capture"`
Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
}
type HeaderCustomTCP struct {
@@ -1272,22 +1278,22 @@ type HeaderCustomTCP struct {
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
for _, value := range c.Clients {
for _, item := range value {
if len(item.Packet) > 0 && item.Rand > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
for _, value := range c.Servers {
for _, item := range value {
if len(item.Packet) > 0 && item.Rand > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
for _, value := range c.Errors {
for _, item := range value {
if len(item.Packet) > 0 && item.Rand > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
}
@@ -1308,6 +1314,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
@@ -1315,6 +1325,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
@@ -1333,6 +1346,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
@@ -1340,6 +1357,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
@@ -1358,6 +1378,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
DelayMin: int64(item.Delay.From),
DelayMax: int64(item.Delay.To),
@@ -1365,6 +1389,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
}
@@ -1471,26 +1498,185 @@ func (c *NoiseMask) Build() (proto.Message, error) {
}
type UDPItem struct {
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Type string `json:"type"`
Packet json.RawMessage `json:"packet"`
Rand int32 `json:"rand"`
RandRange *Int32Range `json:"randRange"`
Capture string `json:"capture"`
Type string `json:"type"`
Reuse string `json:"reuse"`
Transform *CustomTransform `json:"transform"`
Packet json.RawMessage `json:"packet"`
}
type CustomTransform struct {
Op string `json:"op"`
Args []CustomTransformArg `json:"args"`
}
type CustomTransformArg struct {
Type string `json:"type"`
Bytes json.RawMessage `json:"bytes"`
U64 *uint64 `json:"u64"`
Reuse string `json:"reuse"`
Metadata string `json:"metadata"`
Transform *CustomTransform `json:"transform"`
}
func validateCustomVarName(name string) error {
if name == "" {
return nil
}
if !customVarNamePattern.MatchString(name) {
return errors.New("invalid variable name")
}
return nil
}
func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error {
if err := validateCustomVarName(capture); err != nil {
return err
}
if err := validateCustomVarName(reuse); err != nil {
return err
}
kindCount := 0
if len(packet) > 0 {
kindCount++
}
if rand > 0 {
kindCount++
}
if reuse != "" {
kindCount++
}
if transform != nil {
kindCount++
}
if kindCount > 1 {
return errors.New("exactly one item kind must be set")
}
if kindCount == 0 && capture != "" {
return errors.New("exactly one item kind must be set")
}
return nil
}
func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) {
if transform == nil {
return nil, nil
}
if transform.Op == "" {
return nil, errors.New("transform op is required")
}
if len(transform.Args) == 0 {
return nil, errors.New("transform args are required")
}
args := make([]*custom.ExprArg, 0, len(transform.Args))
for _, arg := range transform.Args {
parsedArg, err := buildCustomTransformArg(arg)
if err != nil {
return nil, err
}
args = append(args, parsedArg)
}
return &custom.Expr{
Op: transform.Op,
Args: args,
}, nil
}
func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) {
kindCount := 0
if len(arg.Bytes) > 0 {
kindCount++
}
if arg.U64 != nil {
kindCount++
}
if arg.Reuse != "" {
kindCount++
}
if arg.Metadata != "" {
kindCount++
}
if arg.Transform != nil {
kindCount++
}
if kindCount != 1 {
return nil, errors.New("transform arg must set exactly one value")
}
if len(arg.Bytes) > 0 {
value, err := PraseByteSlice(arg.Bytes, arg.Type)
if err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Bytes{
Bytes: value,
},
}, nil
}
if arg.U64 != nil {
return &custom.ExprArg{
Value: &custom.ExprArg_U64{
U64: *arg.U64,
},
}, nil
}
if arg.Reuse != "" {
if err := validateCustomVarName(arg.Reuse); err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Var{
Var: arg.Reuse,
},
}, nil
}
if arg.Metadata != "" {
return &custom.ExprArg{
Value: &custom.ExprArg_Metadata{
Metadata: arg.Metadata,
},
}, nil
}
parsedExpr, err := buildCustomTransform(arg.Transform)
if err != nil {
return nil, err
}
return &custom.ExprArg{
Value: &custom.ExprArg_Expr{
Expr: parsedExpr,
},
}, nil
}
type HeaderCustomUDP struct {
Mode string `json:"mode"`
Client []UDPItem `json:"client"`
Server []UDPItem `json:"server"`
}
func (c *HeaderCustomUDP) Build() (proto.Message, error) {
switch c.Mode {
case "", "prefix", "standalone":
default:
return nil, errors.New("unknown udp mode")
}
for _, item := range c.Client {
if len(item.Packet) > 0 && item.Rand > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
for _, item := range c.Server {
if len(item.Packet) > 0 && item.Rand > 0 {
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
return nil, err
}
}
@@ -1506,11 +1692,18 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
client = append(client, &custom.UDPItem{
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
@@ -1526,17 +1719,25 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) {
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
return nil, err
}
transform, err := buildCustomTransform(item.Transform)
if err != nil {
return nil, err
}
server = append(server, &custom.UDPItem{
Rand: item.Rand,
RandMin: item.RandRange.From,
RandMax: item.RandRange.To,
Packet: item.Packet,
Save: item.Capture,
Var: item.Reuse,
Expr: transform,
})
}
return &custom.UDPConfig{
Client: client,
Server: server,
Mode: c.Mode,
}, nil
}
+136
View File
@@ -2,10 +2,12 @@ package conf_test
import (
"encoding/json"
"strings"
"testing"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"google.golang.org/protobuf/proto"
)
@@ -156,3 +158,137 @@ func TestSocketConfig(t *testing.T) {
t.Fatalf("unexpected parsed TFO value, which should be -1")
}
}
func TestHeaderCustomUDPBuild(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
runMultiTestCase(t, []TestCase{
{
Input: `{
"client": [
{
"type": "hex",
"packet": "aabb"
},
{
"rand": 2,
"capture": "seed",
"randRange": "16-32"
}
],
"server": [
{
"capture": "txid",
"transform": {
"op": "concat",
"args": [
{"reuse": "seed"},
{"u64": 258},
{"type": "hex", "bytes": "c0de"}
]
}
},
{
"reuse": "txid"
}
],
"mode": "standalone"
}`,
Parser: parser,
Output: &finalmaskcustom.UDPConfig{
Mode: "standalone",
Client: []*finalmaskcustom.UDPItem{
{
RandMax: 255,
Packet: []byte{0xAA, 0xBB},
},
{
Rand: 2,
RandMin: 16,
RandMax: 32,
Save: "seed",
},
},
Server: []*finalmaskcustom.UDPItem{
{
RandMax: 255,
Save: "txid",
Expr: &finalmaskcustom.Expr{
Op: "concat",
Args: []*finalmaskcustom.ExprArg{
{
Value: &finalmaskcustom.ExprArg_Var{
Var: "seed",
},
},
{
Value: &finalmaskcustom.ExprArg_U64{
U64: 258,
},
},
{
Value: &finalmaskcustom.ExprArg_Bytes{
Bytes: []byte{0xC0, 0xDE},
},
},
},
},
},
{
RandMax: 255,
Var: "txid",
},
},
},
},
})
}
func TestHeaderCustomTCPBuildRejectsMixedItemKinds(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomTCP) })
_, err := parser(`{
"clients": [[
{
"packet": [1, 2],
"reuse": "txid"
}
]]
}`)
if err == nil || !strings.Contains(err.Error(), "exactly one item kind") {
t.Fatalf("expected mixed item kind rejection, got %v", err)
}
}
func TestHeaderCustomUDPBuildRejectsInvalidVariableNames(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
_, err := parser(`{
"client": [
{
"capture": "bad-name",
"rand": 4
}
]
}`)
if err == nil || !strings.Contains(err.Error(), "invalid variable name") {
t.Fatalf("expected invalid variable name rejection, got %v", err)
}
}
func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) {
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
_, err := parser(`{
"client": [
{
"transform": {
"op": "concat"
}
}
]
}`)
if err == nil || !strings.Contains(err.Error(), "transform args") {
t.Fatalf("expected transform arg rejection, got %v", err)
}
}
+3 -6
View File
@@ -7,7 +7,7 @@ import (
type TunConfig struct {
Name string `json:"name"`
MTU []uint32 `json:"mtu"`
MTU uint32 `json:"mtu"`
Gateway []string `json:"gateway"`
DNS []string `json:"dns"`
UserLevel uint32 `json:"userLevel"`
@@ -34,11 +34,8 @@ func (v *TunConfig) Build() (proto.Message, error) {
if config.Name == "" {
config.Name = "xray0"
}
if len(config.MTU) == 0 {
config.MTU = []uint32{1500, 1280}
}
if len(config.MTU) == 1 {
config.MTU = append(config.MTU, config.MTU[0])
if config.MTU == 0 {
config.MTU = 1500
}
return config, nil
}
+14
View File
@@ -361,6 +361,7 @@ type Config struct {
Observatory *ObservatoryConfig `json:"observatory"`
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
Version *VersionConfig `json:"version"`
Geodata *GeodataConfig `json:"geodata"`
}
func (c *Config) findInboundTag(tag string) int {
@@ -433,6 +434,10 @@ func (c *Config) Override(o *Config, fn string) {
c.Version = o.Version
}
if o.Geodata != nil {
c.Geodata = o.Geodata
}
// update the Inbound in slice if the only one in override config has same tag
if len(o.InboundConfigs) > 0 {
for i := range o.InboundConfigs {
@@ -542,6 +547,7 @@ func (c *Config) Build() (*core.Config, error) {
}
if c.Reverse != nil {
return nil, errors.PrintRemovedFeatureError(`"legacy reverse"`, `"VLESS Reverse Proxy"`)
r, err := c.Reverse.Build()
if err != nil {
return nil, errors.New("failed to build reverse configuration").Base(err)
@@ -581,6 +587,14 @@ func (c *Config) Build() (*core.Config, error) {
config.App = append(config.App, serial.ToTypedMessage(r))
}
if c.Geodata != nil {
r, err := c.Geodata.Build()
if err != nil {
return nil, errors.New("failed to build geodata configuration").Base(err)
}
config.App = append(config.App, serial.ToTypedMessage(r))
}
var inbounds []InboundDetourConfig
if len(c.InboundConfigs) > 0 {
+8 -5
View File
@@ -99,9 +99,8 @@ func TestXrayConfig(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{10, 0, 0, 0},
Prefix: 8,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
},
},
},
@@ -216,8 +215,12 @@ func TestSniffingConfig_Build(t *testing.T) {
if rule == nil {
t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i)
}
if !reflect.DeepEqual(rule.Ip, tc.ip) || rule.Prefix != tc.prefix {
t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, rule.Ip, rule.Prefix, tc.ip, tc.prefix)
cidr := rule.GetCidr()
if cidr == nil {
t.Fatalf("SniffingConfig.Build() produced a custom ip rule without cidr at index %d", i)
}
if !reflect.DeepEqual(cidr.Ip, tc.ip) || cidr.Prefix != tc.prefix {
t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, cidr.Ip, cidr.Prefix, tc.ip, tc.prefix)
}
}
}
+1
View File
@@ -20,6 +20,7 @@ import (
// Other optional features.
_ "github.com/xtls/xray-core/app/dns"
_ "github.com/xtls/xray-core/app/dns/fakedns"
_ "github.com/xtls/xray-core/app/geodata"
_ "github.com/xtls/xray-core/app/log"
_ "github.com/xtls/xray-core/app/metrics"
_ "github.com/xtls/xray-core/app/policy"
+162 -41
View File
@@ -7,6 +7,7 @@
package dns
import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -22,21 +23,130 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type RuleAction int32
const (
RuleAction_Direct RuleAction = 0
RuleAction_Drop RuleAction = 1
RuleAction_Reject RuleAction = 2
RuleAction_Hijack RuleAction = 3
)
// Enum value maps for RuleAction.
var (
RuleAction_name = map[int32]string{
0: "Direct",
1: "Drop",
2: "Reject",
3: "Hijack",
}
RuleAction_value = map[string]int32{
"Direct": 0,
"Drop": 1,
"Reject": 2,
"Hijack": 3,
}
)
func (x RuleAction) Enum() *RuleAction {
p := new(RuleAction)
*p = x
return p
}
func (x RuleAction) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (RuleAction) Descriptor() protoreflect.EnumDescriptor {
return file_proxy_dns_config_proto_enumTypes[0].Descriptor()
}
func (RuleAction) Type() protoreflect.EnumType {
return &file_proxy_dns_config_proto_enumTypes[0]
}
func (x RuleAction) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use RuleAction.Descriptor instead.
func (RuleAction) EnumDescriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
type DNSRuleConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.dns.RuleAction" json:"action,omitempty"`
Qtype []int32 `protobuf:"varint,2,rep,packed,name=qtype,proto3" json:"qtype,omitempty"`
Domain []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DNSRuleConfig) Reset() {
*x = DNSRuleConfig{}
mi := &file_proxy_dns_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DNSRuleConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSRuleConfig) ProtoMessage() {}
func (x *DNSRuleConfig) ProtoReflect() protoreflect.Message {
mi := &file_proxy_dns_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSRuleConfig.ProtoReflect.Descriptor instead.
func (*DNSRuleConfig) Descriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
func (x *DNSRuleConfig) GetAction() RuleAction {
if x != nil {
return x.Action
}
return RuleAction_Direct
}
func (x *DNSRuleConfig) GetQtype() []int32 {
if x != nil {
return x.Qtype
}
return nil
}
func (x *DNSRuleConfig) GetDomain() []*geodata.DomainRule {
if x != nil {
return x.Domain
}
return nil
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Server is the DNS server address. If specified, this address overrides the
// original one.
Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
Non_IPQuery string `protobuf:"bytes,3,opt,name=non_IP_query,json=nonIPQuery,proto3" json:"non_IP_query,omitempty"`
BlockTypes []int32 `protobuf:"varint,4,rep,packed,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
Rule []*DNSRuleConfig `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
Server *net.Endpoint `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_proxy_dns_config_proto_msgTypes[0]
mi := &file_proxy_dns_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -48,7 +158,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_dns_config_proto_msgTypes[0]
mi := &file_proxy_dns_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -61,14 +171,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetServer() *net.Endpoint {
if x != nil {
return x.Server
}
return nil
return file_proxy_dns_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetUserLevel() uint32 {
@@ -78,16 +181,16 @@ func (x *Config) GetUserLevel() uint32 {
return 0
}
func (x *Config) GetNon_IPQuery() string {
func (x *Config) GetRule() []*DNSRuleConfig {
if x != nil {
return x.Non_IPQuery
return x.Rule
}
return ""
return nil
}
func (x *Config) GetBlockTypes() []int32 {
func (x *Config) GetServer() *net.Endpoint {
if x != nil {
return x.BlockTypes
return x.Server
}
return nil
}
@@ -96,15 +199,25 @@ var File_proxy_dns_config_proto protoreflect.FileDescriptor
const file_proxy_dns_config_proto_rawDesc = "" +
"\n" +
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" +
"\x06Config\x121\n" +
"\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\n" +
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" +
"\rDNSRuleConfig\x122\n" +
"\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" +
"\x05qtype\x18\x02 \x03(\x05R\x05qtype\x127\n" +
"\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\"\x8d\x01\n" +
"\x06Config\x12\x1d\n" +
"\n" +
"user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" +
"\fnon_IP_query\x18\x03 \x01(\tR\n" +
"nonIPQuery\x12\x1f\n" +
"\vblock_types\x18\x04 \x03(\x05R\n" +
"blockTypesBL\n" +
"user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" +
"\x04rule\x18\x02 \x03(\v2\x1d.xray.proxy.dns.DNSRuleConfigR\x04rule\x121\n" +
"\x06server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\x06server*:\n" +
"\n" +
"RuleAction\x12\n" +
"\n" +
"\x06Direct\x10\x00\x12\b\n" +
"\x04Drop\x10\x01\x12\n" +
"\n" +
"\x06Reject\x10\x02\x12\n" +
"\n" +
"\x06Hijack\x10\x03BL\n" +
"\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3"
var (
@@ -119,18 +232,25 @@ func file_proxy_dns_config_proto_rawDescGZIP() []byte {
return file_proxy_dns_config_proto_rawDescData
}
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proxy_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_dns_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.dns.Config
(*net.Endpoint)(nil), // 1: xray.common.net.Endpoint
(RuleAction)(0), // 0: xray.proxy.dns.RuleAction
(*DNSRuleConfig)(nil), // 1: xray.proxy.dns.DNSRuleConfig
(*Config)(nil), // 2: xray.proxy.dns.Config
(*geodata.DomainRule)(nil), // 3: xray.common.geodata.DomainRule
(*net.Endpoint)(nil), // 4: xray.common.net.Endpoint
}
var file_proxy_dns_config_proto_depIdxs = []int32{
1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
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
0, // 0: xray.proxy.dns.DNSRuleConfig.action:type_name -> xray.proxy.dns.RuleAction
3, // 1: xray.proxy.dns.DNSRuleConfig.domain:type_name -> xray.common.geodata.DomainRule
1, // 2: xray.proxy.dns.Config.rule:type_name -> xray.proxy.dns.DNSRuleConfig
4, // 3: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_proxy_dns_config_proto_init() }
@@ -143,13 +263,14 @@ func file_proxy_dns_config_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumEnums: 1,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proxy_dns_config_proto_goTypes,
DependencyIndexes: file_proxy_dns_config_proto_depIdxs,
EnumInfos: file_proxy_dns_config_proto_enumTypes,
MessageInfos: file_proxy_dns_config_proto_msgTypes,
}.Build()
File_proxy_dns_config_proto = out.File
+17 -6
View File
@@ -7,12 +7,23 @@ option java_package = "com.xray.proxy.dns";
option java_multiple_files = true;
import "common/net/destination.proto";
import "common/geodata/geodat.proto";
enum RuleAction {
Direct = 0;
Drop = 1;
Reject = 2;
Hijack = 3;
}
message DNSRuleConfig {
RuleAction action = 1;
repeated int32 qtype = 2;
repeated xray.common.geodata.DomainRule domain = 3;
}
message Config {
// Server is the DNS server address. If specified, this address overrides the
// original one.
xray.common.net.Endpoint server = 1;
uint32 user_level = 2;
string non_IP_query = 3;
repeated int32 block_types = 4;
uint32 user_level = 1;
repeated DNSRuleConfig rule = 2;
xray.common.net.Endpoint server = 3;
}
+98 -50
View File
@@ -11,6 +11,7 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
dns_proto "github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session"
@@ -40,6 +41,31 @@ func init() {
}))
}
type DNSRule struct {
action RuleAction
qTypes []uint16
domains geodata.DomainMatcher
}
func (r *DNSRule) matchQType(qType uint16) bool {
if len(r.qTypes) == 0 {
return true
}
for _, t := range r.qTypes {
if t == qType {
return true
}
}
return false
}
func (r *DNSRule) Apply(qType uint16, domain string) bool {
if !r.matchQType(qType) {
return false
}
return r.domains == nil || r.domains.MatchAny(strings.TrimSuffix(strings.ToLower(domain), "."))
}
type ownLinkVerifier interface {
IsOwnLink(ctx context.Context) bool
}
@@ -50,8 +76,7 @@ type Handler struct {
ownLinkVerifier ownLinkVerifier
server net.Destination
timeout time.Duration
nonIPQuery string
blockTypes []int32
rules []*DNSRule
}
func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error {
@@ -65,11 +90,26 @@ func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager polic
if config.Server != nil {
h.server = config.Server.AsDestination()
}
h.nonIPQuery = config.Non_IPQuery
if h.nonIPQuery == "" {
h.nonIPQuery = "reject"
h.rules = make([]*DNSRule, 0, len(config.Rule))
for _, r := range config.Rule {
rule := &DNSRule{
action: r.Action,
qTypes: make([]uint16, 0, len(r.Qtype)),
}
for _, t := range r.Qtype {
rule.qTypes = append(rule.qTypes, uint16(t))
}
if len(r.Domain) > 0 {
m, err := geodata.DomainReg.BuildDomainMatcher(r.Domain)
if err != nil {
return err
}
rule.domains = m
}
h.rules = append(h.rules, rule)
}
h.blockTypes = config.BlockTypes
return nil
}
@@ -77,30 +117,38 @@ func (h *Handler) isOwnLink(ctx context.Context) bool {
return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx)
}
func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) {
func parseQuery(b []byte) (id uint16, qType dnsmessage.Type, domain string, ok bool) {
var parser dnsmessage.Parser
header, err := parser.Start(b)
if err != nil {
errors.LogInfoInner(context.Background(), err, "parser start")
return
}
id = header.ID
q, err := parser.Question()
if err != nil {
errors.LogInfoInner(context.Background(), err, "question")
return
}
domain = q.Name.String()
qType = q.Type
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
return
}
r = true
domain = q.Name.String()
ok = true
return
}
func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction {
qCode := uint16(qType)
for _, r := range h.rules {
if r.Apply(qCode, domain) {
return r.action
}
}
if qType == dnsmessage.TypeA || qType == dnsmessage.TypeAAAA {
return RuleAction_Hijack
}
return RuleAction_Reject
}
// Process implements proxy.Outbound.
func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error {
outbounds := session.OutboundsFromContext(ctx)
@@ -183,51 +231,51 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
if err == io.EOF {
return nil
}
if err != nil {
return err
}
timer.Update()
if !h.isOwnLink(ctx) {
isIPQuery, domain, id, qType := parseIPQuery(b.Bytes())
if len(h.blockTypes) > 0 {
for _, blocktype := range h.blockTypes {
if blocktype == int32(qType) {
b.Release()
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
if h.nonIPQuery == "reject" {
err := h.rejectNonIPQuery(id, qType, domain, writer)
if err != nil {
return err
}
}
return nil
}
}
}
if isIPQuery {
b.Release()
go h.handleIPQuery(id, qType, domain, writer, timer)
continue
}
if h.nonIPQuery == "drop" {
b.Release()
continue
}
if h.nonIPQuery == "reject" {
b.Release()
err := h.rejectNonIPQuery(id, qType, domain, writer)
if err != nil {
return err
}
continue
if h.isOwnLink(ctx) {
if err := connWriter.WriteMessage(b); err != nil {
return err
}
continue
}
if err := connWriter.WriteMessage(b); err != nil {
return err
id, qType, domain, ok := parseQuery(b.Bytes())
if !ok {
b.Release()
continue
}
switch h.applyRules(qType, domain) {
case RuleAction_Drop:
b.Release()
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
case RuleAction_Reject:
b.Release()
errors.LogInfo(ctx, "rejected type ", qType, " query for domain ", domain)
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
return err
}
case RuleAction_Hijack:
b.Release()
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
errors.LogError(ctx, "can only hijack A/AAAA records")
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
return err
}
} else {
go h.handleIPQuery(id, qType, domain, writer, timer)
}
case RuleAction_Direct:
if err := connWriter.WriteMessage(b); err != nil {
return err
}
default:
panic("unknown rule action")
}
}
}
+124
View File
@@ -14,6 +14,7 @@ import (
_ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound"
"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/serial"
"github.com/xtls/xray-core/core"
@@ -368,3 +369,126 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
t.Error(r)
}
}
func TestDNSRules(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
}
defer dnsServer.Shutdown()
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
serverPort := udp.PickPort()
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dnsapp.Config{
NameServer: []*dnsapp.NameServer{
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Inbound: []*core.InboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(port),
Networks: []net.Network{net.Network_UDP},
}),
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
Rule: []*dns_proxy.DNSRuleConfig{
{
Qtype: []int32{int32(dns.TypeA)},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Domain,
Value: "facebook.com",
},
},
},
},
Action: dns_proxy.RuleAction_Direct,
},
{
Qtype: []int32{int32(dns.TypeA)},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Full,
Value: "google.com",
},
},
},
},
Action: dns_proxy.RuleAction_Reject,
},
},
}),
},
},
}
v, err := core.New(config)
common.Must(err)
common.Must(v.Start())
defer v.Close()
{
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = []dns.Question{{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
c := new(dns.Client)
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
common.Must(err)
if in.Rcode != dns.RcodeRefused {
t.Fatal("expected Refused, but got ", in.Rcode)
}
}
{
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = []dns.Question{{Name: "facebook.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
c := new(dns.Client)
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
common.Must(err)
if in.Rcode != dns.RcodeSuccess {
t.Fatal("expected Success, but got ", in.Rcode)
}
}
}
+84 -21
View File
@@ -7,6 +7,7 @@
package freedom
import (
geodata "github.com/xtls/xray-core/common/geodata"
protocol "github.com/xtls/xray-core/common/protocol"
internet "github.com/xtls/xray-core/transport/internet"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
@@ -251,6 +252,50 @@ func (x *Noise) GetApplyTo() string {
return ""
}
type IPRules struct {
state protoimpl.MessageState `protogen:"open.v1"`
Rules []*geodata.IPRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IPRules) Reset() {
*x = IPRules{}
mi := &file_proxy_freedom_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IPRules) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IPRules) ProtoMessage() {}
func (x *IPRules) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IPRules.ProtoReflect.Descriptor instead.
func (*IPRules) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3}
}
func (x *IPRules) GetRules() []*geodata.IPRule {
if x != nil {
return x.Rules
}
return nil
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"`
@@ -259,13 +304,14 @@ type Config struct {
Fragment *Fragment `protobuf:"bytes,5,opt,name=fragment,proto3" json:"fragment,omitempty"`
ProxyProtocol uint32 `protobuf:"varint,6,opt,name=proxy_protocol,json=proxyProtocol,proto3" json:"proxy_protocol,omitempty"`
Noises []*Noise `protobuf:"bytes,7,rep,name=noises,proto3" json:"noises,omitempty"`
IpsBlocked *IPRules `protobuf:"bytes,8,opt,name=ips_blocked,json=ipsBlocked,proto3,oneof" json:"ips_blocked,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_proxy_freedom_config_proto_msgTypes[3]
mi := &file_proxy_freedom_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -277,7 +323,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[3]
mi := &file_proxy_freedom_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -290,7 +336,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3}
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{4}
}
func (x *Config) GetDomainStrategy() internet.DomainStrategy {
@@ -335,11 +381,18 @@ func (x *Config) GetNoises() []*Noise {
return nil
}
func (x *Config) GetIpsBlocked() *IPRules {
if x != nil {
return x.IpsBlocked
}
return nil
}
var File_proxy_freedom_config_proto protoreflect.FileDescriptor
const file_proxy_freedom_config_proto_rawDesc = "" +
"\n" +
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\"S\n" +
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\x1a\x1bcommon/geodata/geodat.proto\"S\n" +
"\x13DestinationOverride\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
"\bFragment\x12!\n" +
@@ -362,7 +415,9 @@ const file_proxy_freedom_config_proto_rawDesc = "" +
"\tdelay_min\x18\x03 \x01(\x04R\bdelayMin\x12\x1b\n" +
"\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" +
"\x06packet\x18\x05 \x01(\fR\x06packet\x12\x19\n" +
"\bapply_to\x18\x06 \x01(\tR\aapplyTo\"\xe9\x02\n" +
"\bapply_to\x18\x06 \x01(\tR\aapplyTo\"<\n" +
"\aIPRules\x121\n" +
"\x05rules\x18\x01 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x05rules\"\xbc\x03\n" +
"\x06Config\x12P\n" +
"\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" +
"\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" +
@@ -370,7 +425,10 @@ const file_proxy_freedom_config_proto_rawDesc = "" +
"user_level\x18\x04 \x01(\rR\tuserLevel\x128\n" +
"\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" +
"\x0eproxy_protocol\x18\x06 \x01(\rR\rproxyProtocol\x121\n" +
"\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noisesBX\n" +
"\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noises\x12A\n" +
"\vips_blocked\x18\b \x01(\v2\x1b.xray.proxy.freedom.IPRulesH\x00R\n" +
"ipsBlocked\x88\x01\x01B\x0e\n" +
"\f_ips_blockedBX\n" +
"\x16com.xray.proxy.freedomP\x01Z'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3"
var (
@@ -385,26 +443,30 @@ func file_proxy_freedom_config_proto_rawDescGZIP() []byte {
return file_proxy_freedom_config_proto_rawDescData
}
var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_proxy_freedom_config_proto_goTypes = []any{
(*DestinationOverride)(nil), // 0: xray.proxy.freedom.DestinationOverride
(*Fragment)(nil), // 1: xray.proxy.freedom.Fragment
(*Noise)(nil), // 2: xray.proxy.freedom.Noise
(*Config)(nil), // 3: xray.proxy.freedom.Config
(*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint
(internet.DomainStrategy)(0), // 5: xray.transport.internet.DomainStrategy
(*IPRules)(nil), // 3: xray.proxy.freedom.IPRules
(*Config)(nil), // 4: xray.proxy.freedom.Config
(*protocol.ServerEndpoint)(nil), // 5: xray.common.protocol.ServerEndpoint
(*geodata.IPRule)(nil), // 6: xray.common.geodata.IPRule
(internet.DomainStrategy)(0), // 7: xray.transport.internet.DomainStrategy
}
var file_proxy_freedom_config_proto_depIdxs = []int32{
4, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
5, // 1: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
0, // 2: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
1, // 3: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
2, // 4: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
5, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
6, // 1: xray.proxy.freedom.IPRules.rules:type_name -> xray.common.geodata.IPRule
7, // 2: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
0, // 3: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
1, // 4: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
2, // 5: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
3, // 6: xray.proxy.freedom.Config.ips_blocked:type_name -> xray.proxy.freedom.IPRules
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
}
func init() { file_proxy_freedom_config_proto_init() }
@@ -412,13 +474,14 @@ func file_proxy_freedom_config_proto_init() {
if File_proxy_freedom_config_proto != nil {
return
}
file_proxy_freedom_config_proto_msgTypes[4].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
+6
View File
@@ -8,6 +8,7 @@ option java_multiple_files = true;
import "common/protocol/server_spec.proto";
import "transport/internet/config.proto";
import "common/geodata/geodat.proto";
message DestinationOverride {
xray.common.protocol.ServerEndpoint server = 1;
@@ -32,6 +33,10 @@ message Noise {
string apply_to = 6;
}
message IPRules {
repeated xray.common.geodata.IPRule rules = 1;
}
message Config {
xray.transport.internet.DomainStrategy domain_strategy = 1;
DestinationOverride destination_override = 3;
@@ -39,4 +44,5 @@ message Config {
Fragment fragment = 5;
uint32 proxy_protocol = 6;
repeated Noise noises = 7;
optional IPRules ips_blocked = 8;
}
+119 -38
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"io"
"strings"
"time"
"github.com/pires/go-proxyproto"
@@ -12,6 +13,7 @@ import (
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/dice"
"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/platform"
"github.com/xtls/xray-core/common/retry"
@@ -30,6 +32,32 @@ import (
var useSplice bool
var defaultPrivateBlockIP = []string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
"ff00::/8",
}
var defaultPrivateBlockIPMatcher = func() geodata.IPMatcher {
rules := common.Must2(geodata.ParseIPRules(defaultPrivateBlockIP))
return common.Must2(geodata.IPReg.BuildIPMatcher(rules))
}()
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
h := new(Handler)
@@ -50,14 +78,22 @@ func init() {
// Handler handles Freedom connections.
type Handler struct {
policyManager policy.Manager
config *Config
policyManager policy.Manager
config *Config
blockedIPMatcher geodata.IPMatcher
}
// Init initializes the Handler with necessary parameters.
func (h *Handler) Init(config *Config, pm policy.Manager) error {
h.config = config
h.policyManager = pm
if config.IpsBlocked != nil && len(config.IpsBlocked.Rules) > 0 {
m, err := geodata.IPReg.BuildIPMatcher(config.IpsBlocked.Rules)
if err != nil {
return errors.New("failed to build blocked ip matcher").Base(err)
}
h.blockedIPMatcher = m
}
return nil
}
@@ -75,6 +111,32 @@ func isValidAddress(addr *net.IPOrDomain) bool {
return a != net.AnyIP && a != net.AnyIPv6
}
func (h *Handler) getBlockedIPMatcher(ctx context.Context, inbound *session.Inbound) geodata.IPMatcher {
if h.blockedIPMatcher != nil {
return h.blockedIPMatcher
}
if h.config.IpsBlocked != nil && len(h.config.IpsBlocked.Rules) == 0 { // "ipsBlocked": []
return nil
}
if inbound == nil {
return nil
}
switch inbound.Name {
case "vmess", "trojan", "hysteria", "wireguard":
errors.LogInfo(ctx, "applying default private IP blocking policy for inbound ", inbound.Name)
return defaultPrivateBlockIPMatcher
}
if strings.HasPrefix(inbound.Name, "vless") || strings.HasPrefix(inbound.Name, "shadowsocks") {
errors.LogInfo(ctx, "applying default private IP blocking policy for inbound ", inbound.Name)
return defaultPrivateBlockIPMatcher
}
return nil
}
func isBlockedAddress(matcher geodata.IPMatcher, addr net.Address) bool {
return matcher != nil && addr != nil && addr.Family().IsIP() && matcher.Match(addr.IP())
}
// Process implements proxy.Outbound.
func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
outbounds := session.OutboundsFromContext(ctx)
@@ -85,6 +147,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.Name = "freedom"
ob.CanSpliceCopy = 1
inbound := session.InboundFromContext(ctx)
blockedIPMatcher := h.getBlockedIPMatcher(ctx, inbound)
destination := ob.Target
origTargetAddr := ob.OriginalTarget.Address
@@ -138,23 +201,26 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return err
}
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr()
dstAddr := rawConn.RemoteAddr()
header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
if _, err = header.WriteTo(rawConn); err != nil {
rawConn.Close()
return err
}
}
conn = rawConn
return nil
})
if err != nil {
return errors.New("failed to open connection to ", destination).Base(err)
}
if remoteAddr := net.DestinationFromAddr(conn.RemoteAddr()).Address; isBlockedAddress(blockedIPMatcher, remoteAddr) {
conn.Close()
return errors.New("blocked target IP: ", remoteAddr).AtInfo()
}
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr()
dstAddr := conn.RemoteAddr()
header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
if _, err = header.WriteTo(conn); err != nil {
conn.Close()
return errors.New("failed to set PROXY protocol v", version).Base(err)
}
}
defer conn.Close()
errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr())
@@ -189,7 +255,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
writer = buf.NewWriter(conn)
}
} else {
writer = NewPacketWriter(conn, h, UDPOverride, destination)
writer = NewPacketWriter(conn, h, UDPOverride, destination, blockedIPMatcher)
if h.config.Noises != nil {
errors.LogDebug(ctx, "NOISE", h.config.Noises)
writer = &NoisePacketWriter{
@@ -224,7 +290,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
if destination.Network == net.Network_TCP {
reader = buf.NewReader(conn)
} else {
reader = NewPacketReader(conn, UDPOverride, destination)
reader = NewPacketReader(conn, UDPOverride, destination, blockedIPMatcher)
}
if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to process response").Base(err)
@@ -243,7 +309,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return nil
}
func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {
func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination, blockedIPMatcher geodata.IPMatcher) buf.Reader {
iConn := conn
statConn, ok := iConn.(*stat.CounterConnection)
if ok {
@@ -262,6 +328,7 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
return &PacketReader{
PacketConnWrapper: c,
Counter: counter,
BlockedIPMatcher: blockedIPMatcher,
IsOverridden: isOverridden,
InitUnchangedAddr: DialDest.Address,
InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address,
@@ -273,6 +340,7 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
type PacketReader struct {
*internet.PacketConnWrapper
stats.Counter
BlockedIPMatcher geodata.IPMatcher
IsOverridden bool
InitUnchangedAddr net.Address
InitChangedAddr net.Address
@@ -281,33 +349,40 @@ type PacketReader struct {
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
b := buf.New()
b.Resize(0, buf.Size)
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return nil, err
}
b.Resize(0, int32(n))
// if udp dest addr is changed, we are unable to get the correct src addr
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
if !r.IsOverridden {
address := net.IPAddress(d.(*net.UDPAddr).IP)
if r.InitChangedAddr == address {
address = r.InitUnchangedAddr
for {
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return nil, err
}
b.UDP = &net.Destination{
Address: address,
Port: net.Port(d.(*net.UDPAddr).Port),
Network: net.Network_UDP,
udpAddr := d.(*net.UDPAddr)
sourceAddr := net.IPAddress(udpAddr.IP)
if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) {
continue
}
b.Resize(0, int32(n))
// if udp dest addr is changed, we are unable to get the correct src addr
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
if !r.IsOverridden {
if r.InitChangedAddr == sourceAddr {
sourceAddr = r.InitUnchangedAddr
}
b.UDP = &net.Destination{
Address: sourceAddr,
Port: net.Port(udpAddr.Port),
Network: net.Network_UDP,
}
}
if r.Counter != nil {
r.Counter.Add(int64(n))
}
return buf.MultiBuffer{b}, nil
}
if r.Counter != nil {
r.Counter.Add(int64(n))
}
return buf.MultiBuffer{b}, nil
}
// DialDest means the dial target used in the dialer when creating conn
func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, DialDest net.Destination) buf.Writer {
func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, DialDest net.Destination, blockedIPMatcher geodata.IPMatcher) buf.Writer {
iConn := conn
statConn, ok := iConn.(*stat.CounterConnection)
if ok {
@@ -328,6 +403,7 @@ func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, Dia
PacketConnWrapper: c,
Counter: counter,
Handler: h,
BlockedIPMatcher: blockedIPMatcher,
UDPOverride: UDPOverride,
ResolvedUDPAddr: resolvedUDPAddr,
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
@@ -341,7 +417,8 @@ type PacketWriter struct {
*internet.PacketConnWrapper
stats.Counter
*Handler
UDPOverride net.Destination
BlockedIPMatcher geodata.IPMatcher
UDPOverride net.Destination
// Dest of udp packets might be a domain, we will resolve them to IP
// But resolver will return a random one if the domain has many IPs
@@ -399,6 +476,10 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
}
}
}
if isBlockedAddress(w.BlockedIPMatcher, b.UDP.Address) {
b.Release()
continue
}
destAddr := b.UDP.RawNetAddr()
if destAddr == nil {
b.Release()
+9 -2
View File
@@ -245,10 +245,14 @@ type UDPReader struct {
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if r.firstMsg != nil {
buffer := buf.New()
buffer.Write(r.firstMsg.Data)
_, err := buffer.Write(r.firstMsg.Data)
if err != nil {
return nil, err
}
buffer.UDP = r.firstDest
r.firstMsg = nil
r.firstDest = nil
return buf.MultiBuffer{buffer}, nil
}
@@ -275,7 +279,10 @@ func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
}
buffer := buf.New()
buffer.Write(dfMsg.Data)
if _, err := buffer.Write(dfMsg.Data); err != nil {
return nil, err
}
buffer.UDP = &dest
return buf.MultiBuffer{buffer}, nil
+4 -15
View File
@@ -82,24 +82,13 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
inbound := session.InboundFromContext(ctx)
inbound.Name = "hysteria"
inbound.CanSpliceCopy = 3
inbound.User = &protocol.MemoryUser{}
iConn := stat.TryUnwrapStatsConn(conn)
var useremail string
var userlevel uint32
type User interface{ User() *protocol.MemoryUser }
if v, ok := iConn.(User); ok {
if v, ok := iConn.(User); ok && v.User() != nil {
inbound.User = v.User()
if inbound.User != nil {
useremail = inbound.User.Email
userlevel = inbound.User.Level
}
} else {
// get a dummy user
inbound.User = &protocol.MemoryUser{
Email: "",
Level: 0,
}
}
if _, ok := iConn.(*hysteria.InterUdpConn); ok {
@@ -154,7 +143,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
Writer: writer,
})
} else {
sessionPolicy := s.policyManager.ForLevel(userlevel)
sessionPolicy := s.policyManager.ForLevel(inbound.User.Level)
common.Must(conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)))
addr, err := ReadTCPRequest(conn)
@@ -178,7 +167,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
To: dest,
Status: log.AccessAccepted,
Reason: "",
Email: useremail,
Email: inbound.User.Email,
})
errors.LogInfo(ctx, "tunnelling request to ", dest)
+14 -76
View File
@@ -4,13 +4,8 @@ import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/retry"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
@@ -32,81 +27,24 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
destination := ob.Target
errors.LogInfo(ctx, "opening connection to ", destination)
content := new(session.Content)
content.SkipDNSResolve = true
input := link.Reader
output := link.Writer
ctx = session.ContextWithContent(ctx, content)
inbound := &session.Inbound{}
originInbound := session.InboundFromContext(ctx)
if originInbound != nil {
// get a shallow copy to avoid modifying the inbound tag in upstream context
*inbound = *originInbound
}
inbound.Tag = l.config.InboundTag
ctx = session.ContextWithInbound(ctx, inbound)
var conn net.Conn
err := retry.ExponentialBackoff(2, 100).On(func() error {
dialDest := destination
content := new(session.Content)
content.SkipDNSResolve = true
ctx = session.ContextWithContent(ctx, content)
inbound := session.InboundFromContext(ctx)
if inbound == nil {
inbound = &session.Inbound{}
}
inbound.Tag = l.config.InboundTag
ctx = session.ContextWithInbound(ctx, inbound)
rawConn, err := l.dispatcherInstance.Dispatch(ctx, dialDest)
if err != nil {
return err
}
var readerOpt cnc.ConnectionOption
if dialDest.Network == net.Network_TCP {
readerOpt = cnc.ConnectionOutputMulti(rawConn.Reader)
} else {
readerOpt = cnc.ConnectionOutputMultiUDP(rawConn.Reader)
}
conn = cnc.NewConnection(cnc.ConnectionInputMulti(rawConn.Writer), readerOpt)
return nil
})
err := l.dispatcherInstance.DispatchLink(ctx, destination, link)
if err != nil {
return errors.New("failed to open connection to ", destination).Base(err)
errors.New(ctx, "failed to process loopback connection").Base(err)
return err
}
defer conn.Close()
requestDone := func() error {
var writer buf.Writer
if destination.Network == net.Network_TCP {
writer = buf.NewWriter(conn)
} else {
writer = &buf.SequentialWriter{Writer: conn}
}
if err := buf.Copy(input, writer); err != nil {
return errors.New("failed to process request").Base(err)
}
return nil
}
responseDone := func() error {
var reader buf.Reader
if destination.Network == net.Network_TCP {
reader = buf.NewReader(conn)
} else {
reader = buf.NewPacketReader(conn)
}
if err := buf.Copy(reader, output); err != nil {
return errors.New("failed to process response").Base(err)
}
return nil
}
if err := task.Run(ctx, requestDone, task.OnSuccess(responseDone, task.Close(output))); err != nil {
return errors.New("connection ends").Base(err)
}
return nil
}
+19
View File
@@ -173,6 +173,25 @@ Note on ipv6 support. \
Despite Windows also giving the adapter autoconfigured ipv6 address, the ipv6 is not possible until the interface has any _routable_ ipv6 address (given link-local address will not accept traffic from external addresses). \
So everything applicable for ipv4 above also works for ipv6, you only need to give the interface some address manually, e.g. anything private like fc00::a:b:c:d/64 will do just fine
## FreeBSD SUPPORT
FreeBSD support of the same functionality is implemented through tun(4).
Interface name in the configuration must comply to the scheme "tunN", where N is some number. \
It's necessary to set an IP address to the interface, ex.:
```
ifconfig tun0 inet 169.254.10.1/30
```
To attach routing to the interface, route command like following can be executed:
```
route add -net 1.1.1.0/24 -iface tun10
```
```
route add -inet6 -host 2606:4700:4700::1111 -iface tun10
route add -inet6 -host 2606:4700:4700::1001 -iface tun10
```
Important to remember that everything written above about Linux routing concept, also apply to FreeBSD. If you simply route default route through tun interface, that will result network loop and immediate network failure.
## MAC OS X SUPPORT
Darwin (Mac OS X) support of the same functionality is implemented through utun (userspace tunnel).
+4 -4
View File
@@ -24,7 +24,7 @@ const (
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
MTU []uint32 `protobuf:"varint,2,rep,packed,name=MTU,proto3" json:"MTU,omitempty"`
MTU uint32 `protobuf:"varint,2,opt,name=MTU,proto3" json:"MTU,omitempty"`
Gateway []string `protobuf:"bytes,3,rep,name=gateway,proto3" json:"gateway,omitempty"`
DNS []string `protobuf:"bytes,4,rep,name=DNS,proto3" json:"DNS,omitempty"`
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
@@ -71,11 +71,11 @@ func (x *Config) GetName() string {
return ""
}
func (x *Config) GetMTU() []uint32 {
func (x *Config) GetMTU() uint32 {
if x != nil {
return x.MTU
}
return nil
return 0
}
func (x *Config) GetGateway() []string {
@@ -120,7 +120,7 @@ const file_proxy_tun_config_proto_rawDesc = "" +
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
"\x06Config\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03MTU\x18\x02 \x03(\rR\x03MTU\x12\x18\n" +
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
"\agateway\x18\x03 \x03(\tR\agateway\x12\x10\n" +
"\x03DNS\x18\x04 \x03(\tR\x03DNS\x12\x1d\n" +
"\n" +
+1 -1
View File
@@ -8,7 +8,7 @@ option java_multiple_files = true;
message Config {
string name = 1;
repeated uint32 MTU = 2;
uint32 MTU = 2;
repeated string gateway = 3;
repeated string DNS = 4;
uint32 user_level = 5;
+1 -2
View File
@@ -110,8 +110,7 @@ func (t *stackGVisor) Start() error {
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
if srcIP == nil || dstIP == nil {
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice())
return true
panic(id)
}
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
+1 -1
View File
@@ -73,7 +73,7 @@ func (t *AndroidTun) Index() (int, error) {
func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
return fdbased.New(&fdbased.Options{
FDs: []int{t.tunFd},
MTU: t.options.MTU[0],
MTU: t.options.MTU,
RXChecksumOffload: true,
})
}
+16 -12
View File
@@ -3,7 +3,7 @@
package tun
import (
go_errors "errors"
"errors"
"fmt"
"net"
"net/netip"
@@ -12,7 +12,6 @@ import (
"unsafe"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/buffer"
@@ -76,7 +75,7 @@ func NewTun(options *Config) (Tun, error) {
return nil, err
}
err = setup(options.Name, options.MTU[0])
err = setup(options.Name, options.MTU)
if err != nil {
_ = tunFile.Close()
return nil, err
@@ -121,7 +120,7 @@ func (t *DarwinTun) Index() (int, error) {
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize)
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
defer b.Release()
// prepare Darwin specific packet header
@@ -143,7 +142,7 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
b.SetByte(3, family)
if _, err := t.tunFile.Write(b.Bytes()); err != nil {
if go_errors.Is(err, unix.EAGAIN) {
if errors.Is(err, unix.EAGAIN) {
return &tcpip.ErrWouldBlock{}
}
return &tcpip.ErrAborted{}
@@ -156,11 +155,11 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// which will make the stack call Wait which should implement desired push-back
func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize)
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
// read the bytes to the interface file
n, err := b.ReadFrom(t.tunFile)
if go_errors.Is(err, unix.EAGAIN) || go_errors.Is(err, unix.EINTR) {
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
b.Release()
return 0, nil, ErrQueueEmpty
}
@@ -193,7 +192,7 @@ func (t *DarwinTun) Wait() {
}
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
}
// open the interface, by creating new utunN if in the system and returning its file descriptor
@@ -373,12 +372,17 @@ func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
}
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
var err1, err2 error
switch network {
case "tcp4", "udp4", "ip4":
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
case "tcp6", "udp6", "ip6":
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
fallthrough
case "tcp4", "udp4", "ip4":
err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
default:
return errors.New("unknown network ", network)
panic(network + " " + address)
}
return errors.Join(err1, err2)
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !linux && !windows && !android && !darwin
//go:build !linux && !windows && !android && !darwin && !freebsd
package tun
+147
View File
@@ -0,0 +1,147 @@
//go:build freebsd
package tun
import (
"errors"
"net"
_ "unsafe"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/buf"
)
const tunHeaderSize = 4
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
type FreeBSDTun struct {
device tun.Device
mtu uint32
}
var _ Tun = (*FreeBSDTun)(nil)
var _ GVisorDevice = (*FreeBSDTun)(nil)
// NewTun builds new tun interface handler
func NewTun(options *Config) (Tun, error) {
tunDev, err := tun.CreateTUN(options.Name, int(options.MTU))
if err != nil {
return nil, err
}
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil
}
func (t *FreeBSDTun) Start() error {
return nil
}
func (t *FreeBSDTun) Close() error {
return t.device.Close()
}
func (t *FreeBSDTun) Name() (string, error) {
return t.device.Name()
}
func (t *FreeBSDTun) Index() (int, error) {
name, err := t.Name()
if err != nil {
return 0, err
}
iface, err := net.InterfaceByName(name)
if err != nil {
return 0, err
}
return iface.Index, nil
}
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
defer b.Release()
// prepare Unix specific packet header
_, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0})
// copy the bytes of slices that compose the packet into the allocated buffer
for _, packetElement := range packet.AsSlices() {
_, _ = b.Write(packetElement)
}
// fill Unix specific header from the first raw packet byte, that we can access now
var family byte
switch b.Byte(4) >> 4 {
case 4:
family = unix.AF_INET
case 6:
family = unix.AF_INET6
default:
return &tcpip.ErrAborted{}
}
b.SetByte(3, family)
if _, err := t.device.File().Write(b.Bytes()); err != nil {
if errors.Is(err, unix.EAGAIN) {
return &tcpip.ErrWouldBlock{}
}
return &tcpip.ErrAborted{}
}
return nil
}
// ReadPacket implements GVisorDevice method to read one packet from the tun device
// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line,
// which will make the stack call Wait which should implement desired push-back
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize)
// read the bytes to the interface file
n, err := b.ReadFrom(t.device.File())
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
b.Release()
return 0, nil, ErrQueueEmpty
}
if err != nil {
b.Release()
return 0, nil, err
}
// discard empty or sub-empty packets
if n <= tunHeaderSize {
b.Release()
return 0, nil, ErrQueueEmpty
}
// network protocol version from first byte of the raw packet, the one that follows Unix specific header
version := b.Byte(tunHeaderSize) >> 4
packetBuffer := buffer.MakeWithData(b.BytesFrom(tunHeaderSize))
return version, stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: packetBuffer,
IsForwardedPacket: true,
OnRelease: func() {
b.Release()
},
}), nil
}
// Wait some cpu cycles
func (t *FreeBSDTun) Wait() {
procyield(1)
}
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil
}
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return nil
}
+2 -2
View File
@@ -30,7 +30,7 @@ func NewTun(options *Config) (Tun, error) {
return nil, err
}
tunLink, err := setup(options.Name, int(options.MTU[0]))
tunLink, err := setup(options.Name, int(options.MTU))
if err != nil {
_ = unix.Close(tunFd)
return nil, err
@@ -121,7 +121,7 @@ func (t *LinuxTun) Index() (int, error) {
func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) {
return fdbased.New(&fdbased.Options{
FDs: []int{t.tunFd},
MTU: t.options.MTU[0],
MTU: t.options.MTU,
RXChecksumOffload: true,
})
}
+15 -18
View File
@@ -134,7 +134,7 @@ func (t *WindowsTun) Start() error {
ipif.DadTransmits = 0
ipif.ManagedAddressConfigurationSupported = false
ipif.OtherStatefulConfigurationSupported = false
ipif.NLMTU = t.options.MTU[0]
ipif.NLMTU = t.options.MTU
ipif.UseAutomaticMetric = false
ipif.Metric = 0
err = ipif.Set()
@@ -151,7 +151,7 @@ func (t *WindowsTun) Start() error {
ipif.DadTransmits = 0
ipif.ManagedAddressConfigurationSupported = false
ipif.OtherStatefulConfigurationSupported = false
ipif.NLMTU = t.options.MTU[1]
ipif.NLMTU = t.options.MTU
ipif.UseAutomaticMetric = false
ipif.Metric = 0
err = ipif.Set()
@@ -278,7 +278,7 @@ func (t *WindowsTun) Wait() {
}
func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
}
const (
@@ -290,26 +290,23 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
var index [4]byte
binary.BigEndian.PutUint32(index[:], uint32(iface.Index))
var err1, err2, err3, err4 error
switch network {
case "tcp4", "udp4", "ip4":
err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
if err != nil {
return err
}
if network == "udp4" {
return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_MULTICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
}
case "tcp6", "udp6", "ip6":
err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
if err != nil {
return err
}
err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
if network == "udp6" {
return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
}
fallthrough
case "tcp4", "udp4", "ip4":
err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
if network == "udp4" || network == "udp6" {
err4 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_MULTICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
}
default:
return errors.New("unknown network ", network)
panic(network + " " + address)
}
return nil
return errors.Combine(err1, err2, err3, err4)
}
+12 -19
View File
@@ -74,7 +74,7 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
dest: &dst,
}:
default:
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full")
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full 2")
}
}
@@ -98,25 +98,18 @@ type udpConn struct {
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
for {
e, ok := <-c.egress
if !ok {
return nil, io.EOF
}
b := buf.New()
_, err := b.Write(e.data)
if err != nil {
errors.LogDebugInner(context.Background(), err, "drop udp with size ", len(e.data), " to ", e.dest.NetAddr(), " original ", c.dst.NetAddr())
b.Release()
continue
}
b.UDP = e.dest
return buf.MultiBuffer{b}, nil
e, ok := <-c.egress
if !ok {
return nil, io.EOF
}
b := buf.New()
if _, err := b.Write(e.data); err != nil {
return nil, err
}
b.UDP = e.dest
return buf.MultiBuffer{b}, nil
}
// Read packets from the connection
+1
View File
@@ -100,6 +100,7 @@ func New(ctx context.Context, config *Config) (*Handler, error) {
if a.Reverse != nil {
rvsCtx := session.ContextWithInbound(ctx, &session.Inbound{
Tag: a.Reverse.Tag,
Name: "vless-reverse",
User: handler.server.User, // TODO: email
})
if sc := a.Reverse.Sniffing; sc != nil && sc.Enabled {
+14 -21
View File
@@ -192,8 +192,7 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
if srcIP == nil || dstIP == nil {
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice())
return true
panic(id)
}
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
@@ -256,7 +255,7 @@ func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte)
dest: &dst,
}:
default:
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full")
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full 2")
}
}
@@ -346,25 +345,19 @@ type udpConn struct {
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
for {
q, ok := <-c.queue
if !ok {
return nil, io.EOF
}
b := buf.New()
_, err := b.Write(q.p)
if err != nil {
errors.LogDebugInner(context.Background(), err, "drop udp with size ", len(q.p), " to ", q.dest.NetAddr(), " original ", c.dst.NetAddr())
b.Release()
continue
}
b.UDP = q.dest
return buf.MultiBuffer{b}, nil
q, ok := <-c.queue
if !ok {
return nil, io.EOF
}
b := buf.New()
if _, err := b.Write(q.p); err != nil {
return nil, err
}
b.UDP = q.dest
return buf.MultiBuffer{b}, nil
}
func (c *udpConn) Read(p []byte) (int, error) {
+2 -2
View File
@@ -78,9 +78,9 @@ func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, erro
var conn net.PacketConn
var err error
if raddr.Addr().Is4() {
conn, err = d.lc.ListenPacket(context.Background(), "udp4", ":0")
conn, err = d.lc.ListenPacket(context.Background(), "udp", "0.0.0.0:0")
} else {
conn, err = d.lc.ListenPacket(context.Background(), "udp6", ":0")
conn, err = d.lc.ListenPacket(context.Background(), "udp", "[::]:0")
}
if err != nil {
return nil, err
+6 -2
View File
@@ -387,7 +387,9 @@ func TestCommanderAddRemoveUser(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -573,7 +575,9 @@ func TestCommanderStats(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+2 -3
View File
@@ -47,9 +47,8 @@ func TestResolveIP(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDR{
Ip: []byte{127, 0, 0, 0},
Prefix: 8,
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
},
},
},
+8 -4
View File
@@ -59,7 +59,9 @@ func TestDokodemoTCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -97,7 +99,7 @@ func TestDokodemoTCP(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
}),
@@ -157,7 +159,9 @@ func TestDokodemoUDP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -189,7 +193,7 @@ func TestDokodemoUDP(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
}),
+25 -11
View File
@@ -61,7 +61,9 @@ func TestPassiveConnection(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -120,7 +122,9 @@ func TestProxy(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -147,7 +151,9 @@ func TestProxy(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -173,7 +179,7 @@ func TestProxy(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: serverUserID.String(),
}),
@@ -192,7 +198,7 @@ func TestProxy(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(proxyPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: proxyUserID.String(),
}),
@@ -245,7 +251,9 @@ func TestProxyOverKCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -272,7 +280,9 @@ func TestProxyOverKCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
StreamSettings: &internet.StreamConfig{
ProtocolName: "mkcp",
@@ -303,7 +313,7 @@ func TestProxyOverKCP(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: serverUserID.String(),
}),
@@ -325,7 +335,7 @@ func TestProxyOverKCP(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(proxyPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: proxyUserID.String(),
}),
@@ -504,7 +514,9 @@ func TestUDPConnection(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -654,7 +666,9 @@ func TestDialXray(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+15 -5
View File
@@ -47,7 +47,9 @@ func TestHttpConformance(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -108,7 +110,9 @@ func TestHttpError(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -156,7 +160,9 @@ func TestHTTPConnectMethod(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -236,7 +242,9 @@ func TestHttpPost(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -306,7 +314,9 @@ func TestHttpBasicAuth(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+8 -4
View File
@@ -84,7 +84,9 @@ func TestVMessClosing(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -122,7 +124,7 @@ func TestVMessClosing(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
SecuritySettings: &protocol.SecurityConfig{
@@ -190,7 +192,9 @@ func TestZeroBuffer(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -222,7 +226,7 @@ func TestZeroBuffer(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
SecuritySettings: &protocol.SecurityConfig{
+6 -2
View File
@@ -81,7 +81,9 @@ func testShadowsocks2022Tcp(t *testing.T, method string, password string) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -164,7 +166,9 @@ func testShadowsocks2022Udp(t *testing.T, method string, password string) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+21 -11
View File
@@ -52,7 +52,9 @@ func TestShadowsocksChaCha20Poly1305TCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -75,10 +77,10 @@ func TestShadowsocksChaCha20Poly1305TCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{
Server: &protocol.ServerEndpoint{
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: account,
},
},
@@ -138,7 +140,9 @@ func TestShadowsocksAES256GCMTCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -170,7 +174,7 @@ func TestShadowsocksAES256GCMTCP(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: account,
},
},
@@ -231,7 +235,9 @@ func TestShadowsocksAES128GCMUDP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -263,7 +269,7 @@ func TestShadowsocksAES128GCMUDP(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: account,
},
},
@@ -323,7 +329,9 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -361,7 +369,7 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: account,
},
},
@@ -416,7 +424,9 @@ func TestShadowsocksNone(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -442,7 +452,7 @@ func TestShadowsocksNone(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: account,
},
},
+12 -6
View File
@@ -50,7 +50,9 @@ func TestSocksBridgeTCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -76,7 +78,7 @@ func TestSocksBridgeTCP(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&socks.Account{
Username: "Test Account",
Password: "Test Password",
@@ -125,7 +127,9 @@ func TestSocksWithHttpRequest(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -151,7 +155,7 @@ func TestSocksWithHttpRequest(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&http.Account{
Username: "Test Account",
Password: "Test Password",
@@ -251,7 +255,7 @@ func TestSocksBridageUDP(t *testing.T) {
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&socks.Account{
Username: "Test Account",
Password: "Test Password",
@@ -424,7 +428,9 @@ func TestSocksConformanceMod(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+30 -10
View File
@@ -68,7 +68,9 @@ func TestSimpleTLSConnection(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -179,7 +181,9 @@ func TestAutoIssuingCertificate(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -282,7 +286,9 @@ func TestTLSOverKCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -380,7 +386,9 @@ func TestTLSOverWebSocket(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -494,7 +502,9 @@ func TestGRPC(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -608,7 +618,9 @@ func TestGRPCMultiMode(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -715,7 +727,9 @@ func TestSimpleTLSConnectionPinned(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -812,7 +826,9 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -908,7 +924,9 @@ func TestUTLSConnectionPinned(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -1006,7 +1024,9 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+4 -2
View File
@@ -62,7 +62,9 @@ func TestHTTPConnectionHeader(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -88,7 +90,7 @@ func TestHTTPConnectionHeader(t *testing.T) {
Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
User: &protocol.User{
User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(),
}),
+15 -5
View File
@@ -66,7 +66,9 @@ func TestVless(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -169,7 +171,9 @@ func TestVlessTls(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -290,7 +294,9 @@ func TestVlessXtlsVision(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -420,7 +426,9 @@ func TestVlessXtlsVisionReality(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -557,7 +565,9 @@ func TestVlessRealityFingerprints(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+36 -12
View File
@@ -61,7 +61,9 @@ func TestVMessGCM(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -159,7 +161,9 @@ func TestVMessGCMReadv(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -260,7 +264,9 @@ func TestVMessGCMUDP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -355,7 +361,9 @@ func TestVMessChacha20(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -451,7 +459,9 @@ func TestVMessNone(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -549,7 +559,9 @@ func TestVMessKCP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -652,7 +664,9 @@ func TestVMessKCPLarge(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -756,7 +770,9 @@ func TestVMessGCMMux(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -867,7 +883,9 @@ func TestVMessGCMMuxUDP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -988,7 +1006,9 @@ func TestVMessZero(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -1083,7 +1103,9 @@ func TestVMessGCMLengthAuth(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
@@ -1183,7 +1205,9 @@ func TestVMessGCMLengthAuthPlusNoTerminationSignal(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+3 -1
View File
@@ -62,7 +62,9 @@ func TestWireguard(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
}),
},
},
}
+43 -14
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
@@ -26,6 +27,8 @@ type task struct {
}
var conns chan *websocket.Conn
var server *http.Server
var mu sync.Mutex
var upgrader = &websocket.Upgrader{
ReadBufferSize: 0,
@@ -36,27 +39,48 @@ var upgrader = &websocket.Upgrader{
},
}
func init() {
// Used by external projects when using xray as a go module
func Reload() {
addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
mu.Lock()
defer mu.Unlock()
if server != nil {
server.Close()
}
if HasBrowserDialer() {
for len(conns) > 0 {
select {
case c := <-conns:
c.Close()
default:
}
}
conns = nil
}
if addr != "" {
token := uuid.New()
csrfToken := token.String()
webpage = bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken))
webpage := bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken))
conns = make(chan *websocket.Conn, 256)
go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/websocket" {
if r.URL.Query().Get("token") == csrfToken {
if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
conns <- conn
} else {
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error")
server = &http.Server{
Addr: addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/websocket" {
if r.URL.Query().Get("token") == csrfToken {
if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
conns <- conn
} else {
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error")
}
}
} else {
w.Header().Set("Access-Control-Allow-Origin", "*");
w.Write(webpage)
}
} else {
w.Header().Set("Access-Control-Allow-Origin", "*");
w.Write(webpage)
}
}))
}),
}
go server.ListenAndServe()
}
}
@@ -194,3 +218,8 @@ func CheckOK(conn *websocket.Conn) error {
return nil
}
func init() {
Reload()
}
+51 -9
View File
@@ -5,6 +5,7 @@ import (
"net"
"sync"
"github.com/xtls/xray-core/common/bytespool"
"github.com/xtls/xray-core/common/errors"
)
@@ -30,6 +31,19 @@ func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketCon
var conns []net.PacketConn
for i, mask := range m.udpmasks {
if _, ok := mask.(headerConn); ok {
if mode, ok := mask.(headerConnMode); ok && !mode.UseHeaderConn() {
if len(conns) > 0 {
raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw}
sizes = nil
conns = nil
}
var err error
raw, err = mask.WrapPacketConnClient(raw, i, len(m.udpmasks)-1)
if err != nil {
return nil, err
}
continue
}
conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1)
if err != nil {
return nil, err
@@ -63,6 +77,19 @@ func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketCon
var conns []net.PacketConn
for i, mask := range m.udpmasks {
if _, ok := mask.(headerConn); ok {
if mode, ok := mask.(headerConnMode); ok && !mode.UseHeaderConn() {
if len(conns) > 0 {
raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw}
sizes = nil
conns = nil
}
var err error
raw, err = mask.WrapPacketConnServer(raw, i, len(m.udpmasks)-1)
if err != nil {
return nil, err
}
continue
}
conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1)
if err != nil {
return nil, err
@@ -99,22 +126,34 @@ type headerConn interface {
HeaderConn()
}
type headerConnMode interface {
UseHeaderConn() bool
}
type headerSize interface {
Size() int
}
type headerManagerConn struct {
sizes []int
conns []net.PacketConn
sync.Mutex
net.PacketConn
m sync.Mutex
sizes []int
conns []net.PacketConn
writeBuf [UDPSize]byte
}
type headerReadAddrAware interface {
SetReadAddr(net.Addr)
}
func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
buf := p
if len(buf) < UDPSize {
buf = make([]byte, UDPSize)
b := bytespool.Alloc(UDPSize)
b = b[:UDPSize]
defer bytespool.Free(b)
buf = b
}
n, addr, err = c.PacketConn.ReadFrom(buf)
@@ -134,6 +173,9 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
}
for i := range c.conns {
if aware, ok := c.conns[i].(headerReadAddrAware); ok {
aware.SetReadAddr(addr)
}
n, _, err = c.conns[i].ReadFrom(newBuf)
if n == 0 || err != nil {
errors.LogDebug(context.Background(), addr, " mask read err ", err)
@@ -153,8 +195,8 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
}
func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.m.Lock()
defer c.m.Unlock()
c.Lock()
defer c.Unlock()
sum := 0
for _, size := range c.sizes {
@@ -169,7 +211,7 @@ func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error)
n = copy(c.writeBuf[sum:], p)
for i := len(c.conns) - 1; i >= 0; i-- {
n, err = c.conns[i].WriteTo(c.writeBuf[sum-c.sizes[i]:n+sum], nil)
n, err = c.conns[i].WriteTo(c.writeBuf[sum-c.sizes[i]:n+sum], addr)
if n == 0 || err != nil {
errors.LogDebug(context.Background(), addr, " mask write err ", err)
return 0, nil
@@ -249,8 +291,8 @@ func (l *tcpListener) Accept() (net.Conn, error) {
newConn, err := l.m.WrapConnServer(conn)
if err != nil {
errors.LogDebugInner(context.Background(), err, "mask err")
// conn.Close()
return conn, nil
_ = conn.Close()
return nil, err
}
return newConn, nil
@@ -19,12 +19,22 @@ func (c *UDPConfig) UDP() {
}
func (c *UDPConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
if c.Mode == "standalone" {
return NewConnClientUDPStandalone(c, raw)
}
return NewConnClientUDP(c, raw)
}
func (c *UDPConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
if c.Mode == "standalone" {
return NewConnServerUDPStandalone(c, raw)
}
return NewConnServerUDP(c, raw)
}
func (c *UDPConfig) HeaderConn() {
}
func (c *UDPConfig) UseHeaderConn() bool {
return c.Mode != "standalone"
}
@@ -21,6 +21,188 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Expr struct {
state protoimpl.MessageState `protogen:"open.v1"`
Op string `protobuf:"bytes,1,opt,name=op,proto3" json:"op,omitempty"`
Args []*ExprArg `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Expr) Reset() {
*x = Expr{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Expr) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Expr) ProtoMessage() {}
func (x *Expr) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_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 Expr.ProtoReflect.Descriptor instead.
func (*Expr) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{0}
}
func (x *Expr) GetOp() string {
if x != nil {
return x.Op
}
return ""
}
func (x *Expr) GetArgs() []*ExprArg {
if x != nil {
return x.Args
}
return nil
}
type ExprArg struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Value:
//
// *ExprArg_Bytes
// *ExprArg_U64
// *ExprArg_Var
// *ExprArg_Metadata
// *ExprArg_Expr
Value isExprArg_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ExprArg) Reset() {
*x = ExprArg{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ExprArg) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExprArg) ProtoMessage() {}
func (x *ExprArg) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_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 ExprArg.ProtoReflect.Descriptor instead.
func (*ExprArg) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{1}
}
func (x *ExprArg) GetValue() isExprArg_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *ExprArg) GetBytes() []byte {
if x != nil {
if x, ok := x.Value.(*ExprArg_Bytes); ok {
return x.Bytes
}
}
return nil
}
func (x *ExprArg) GetU64() uint64 {
if x != nil {
if x, ok := x.Value.(*ExprArg_U64); ok {
return x.U64
}
}
return 0
}
func (x *ExprArg) GetVar() string {
if x != nil {
if x, ok := x.Value.(*ExprArg_Var); ok {
return x.Var
}
}
return ""
}
func (x *ExprArg) GetMetadata() string {
if x != nil {
if x, ok := x.Value.(*ExprArg_Metadata); ok {
return x.Metadata
}
}
return ""
}
func (x *ExprArg) GetExpr() *Expr {
if x != nil {
if x, ok := x.Value.(*ExprArg_Expr); ok {
return x.Expr
}
}
return nil
}
type isExprArg_Value interface {
isExprArg_Value()
}
type ExprArg_Bytes struct {
Bytes []byte `protobuf:"bytes,1,opt,name=bytes,proto3,oneof"`
}
type ExprArg_U64 struct {
U64 uint64 `protobuf:"varint,2,opt,name=u64,proto3,oneof"`
}
type ExprArg_Var struct {
Var string `protobuf:"bytes,3,opt,name=var,proto3,oneof"`
}
type ExprArg_Metadata struct {
Metadata string `protobuf:"bytes,4,opt,name=metadata,proto3,oneof"`
}
type ExprArg_Expr struct {
Expr *Expr `protobuf:"bytes,5,opt,name=expr,proto3,oneof"`
}
func (*ExprArg_Bytes) isExprArg_Value() {}
func (*ExprArg_U64) isExprArg_Value() {}
func (*ExprArg_Var) isExprArg_Value() {}
func (*ExprArg_Metadata) isExprArg_Value() {}
func (*ExprArg_Expr) isExprArg_Value() {}
type TCPItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
DelayMin int64 `protobuf:"varint,1,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"`
@@ -29,13 +211,16 @@ type TCPItem struct {
RandMin int32 `protobuf:"varint,4,opt,name=rand_min,json=randMin,proto3" json:"rand_min,omitempty"`
RandMax int32 `protobuf:"varint,5,opt,name=rand_max,json=randMax,proto3" json:"rand_max,omitempty"`
Packet []byte `protobuf:"bytes,6,opt,name=packet,proto3" json:"packet,omitempty"`
Save string `protobuf:"bytes,7,opt,name=save,proto3" json:"save,omitempty"`
Var string `protobuf:"bytes,8,opt,name=var,proto3" json:"var,omitempty"`
Expr *Expr `protobuf:"bytes,9,opt,name=expr,proto3" json:"expr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TCPItem) Reset() {
*x = TCPItem{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[0]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -47,7 +232,7 @@ func (x *TCPItem) String() string {
func (*TCPItem) ProtoMessage() {}
func (x *TCPItem) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[0]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -60,7 +245,7 @@ func (x *TCPItem) ProtoReflect() protoreflect.Message {
// Deprecated: Use TCPItem.ProtoReflect.Descriptor instead.
func (*TCPItem) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{0}
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{2}
}
func (x *TCPItem) GetDelayMin() int64 {
@@ -105,6 +290,27 @@ func (x *TCPItem) GetPacket() []byte {
return nil
}
func (x *TCPItem) GetSave() string {
if x != nil {
return x.Save
}
return ""
}
func (x *TCPItem) GetVar() string {
if x != nil {
return x.Var
}
return ""
}
func (x *TCPItem) GetExpr() *Expr {
if x != nil {
return x.Expr
}
return nil
}
type TCPSequence struct {
state protoimpl.MessageState `protogen:"open.v1"`
Sequence []*TCPItem `protobuf:"bytes,1,rep,name=sequence,proto3" json:"sequence,omitempty"`
@@ -114,7 +320,7 @@ type TCPSequence struct {
func (x *TCPSequence) Reset() {
*x = TCPSequence{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[1]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -126,7 +332,7 @@ func (x *TCPSequence) String() string {
func (*TCPSequence) ProtoMessage() {}
func (x *TCPSequence) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[1]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -139,7 +345,7 @@ func (x *TCPSequence) ProtoReflect() protoreflect.Message {
// Deprecated: Use TCPSequence.ProtoReflect.Descriptor instead.
func (*TCPSequence) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{1}
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{3}
}
func (x *TCPSequence) GetSequence() []*TCPItem {
@@ -160,7 +366,7 @@ type TCPConfig struct {
func (x *TCPConfig) Reset() {
*x = TCPConfig{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[2]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -172,7 +378,7 @@ func (x *TCPConfig) String() string {
func (*TCPConfig) ProtoMessage() {}
func (x *TCPConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[2]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -185,7 +391,7 @@ func (x *TCPConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use TCPConfig.ProtoReflect.Descriptor instead.
func (*TCPConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{2}
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{4}
}
func (x *TCPConfig) GetClients() []*TCPSequence {
@@ -215,13 +421,16 @@ type UDPItem struct {
RandMin int32 `protobuf:"varint,2,opt,name=rand_min,json=randMin,proto3" json:"rand_min,omitempty"`
RandMax int32 `protobuf:"varint,3,opt,name=rand_max,json=randMax,proto3" json:"rand_max,omitempty"`
Packet []byte `protobuf:"bytes,4,opt,name=packet,proto3" json:"packet,omitempty"`
Save string `protobuf:"bytes,5,opt,name=save,proto3" json:"save,omitempty"`
Var string `protobuf:"bytes,6,opt,name=var,proto3" json:"var,omitempty"`
Expr *Expr `protobuf:"bytes,7,opt,name=expr,proto3" json:"expr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UDPItem) Reset() {
*x = UDPItem{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[3]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -233,7 +442,7 @@ func (x *UDPItem) String() string {
func (*UDPItem) ProtoMessage() {}
func (x *UDPItem) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[3]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -246,7 +455,7 @@ func (x *UDPItem) ProtoReflect() protoreflect.Message {
// Deprecated: Use UDPItem.ProtoReflect.Descriptor instead.
func (*UDPItem) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{3}
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{5}
}
func (x *UDPItem) GetRand() int32 {
@@ -277,17 +486,39 @@ func (x *UDPItem) GetPacket() []byte {
return nil
}
func (x *UDPItem) GetSave() string {
if x != nil {
return x.Save
}
return ""
}
func (x *UDPItem) GetVar() string {
if x != nil {
return x.Var
}
return ""
}
func (x *UDPItem) GetExpr() *Expr {
if x != nil {
return x.Expr
}
return nil
}
type UDPConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Client []*UDPItem `protobuf:"bytes,1,rep,name=client,proto3" json:"client,omitempty"`
Server []*UDPItem `protobuf:"bytes,2,rep,name=server,proto3" json:"server,omitempty"`
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UDPConfig) Reset() {
*x = UDPConfig{}
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[4]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -299,7 +530,7 @@ func (x *UDPConfig) String() string {
func (*UDPConfig) ProtoMessage() {}
func (x *UDPConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[4]
mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -312,7 +543,7 @@ func (x *UDPConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use UDPConfig.ProtoReflect.Descriptor instead.
func (*UDPConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{4}
return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{6}
}
func (x *UDPConfig) GetClient() []*UDPItem {
@@ -329,32 +560,56 @@ func (x *UDPConfig) GetServer() []*UDPItem {
return nil
}
func (x *UDPConfig) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
var File_transport_internet_finalmask_header_custom_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_header_custom_config_proto_rawDesc = "" +
"\n" +
"7transport/internet/finalmask/header/custom/config.proto\x12/xray.transport.internet.finalmask.header.custom\"\xa5\x01\n" +
"7transport/internet/finalmask/header/custom/config.proto\x12/xray.transport.internet.finalmask.header.custom\"d\n" +
"\x04Expr\x12\x0e\n" +
"\x02op\x18\x01 \x01(\tR\x02op\x12L\n" +
"\x04args\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.ExprArgR\x04args\"\xbd\x01\n" +
"\aExprArg\x12\x16\n" +
"\x05bytes\x18\x01 \x01(\fH\x00R\x05bytes\x12\x12\n" +
"\x03u64\x18\x02 \x01(\x04H\x00R\x03u64\x12\x12\n" +
"\x03var\x18\x03 \x01(\tH\x00R\x03var\x12\x1c\n" +
"\bmetadata\x18\x04 \x01(\tH\x00R\bmetadata\x12K\n" +
"\x04expr\x18\x05 \x01(\v25.xray.transport.internet.finalmask.header.custom.ExprH\x00R\x04exprB\a\n" +
"\x05value\"\x96\x02\n" +
"\aTCPItem\x12\x1b\n" +
"\tdelay_min\x18\x01 \x01(\x03R\bdelayMin\x12\x1b\n" +
"\tdelay_max\x18\x02 \x01(\x03R\bdelayMax\x12\x12\n" +
"\x04rand\x18\x03 \x01(\x05R\x04rand\x12\x19\n" +
"\brand_min\x18\x04 \x01(\x05R\arandMin\x12\x19\n" +
"\brand_max\x18\x05 \x01(\x05R\arandMax\x12\x16\n" +
"\x06packet\x18\x06 \x01(\fR\x06packet\"c\n" +
"\x06packet\x18\x06 \x01(\fR\x06packet\x12\x12\n" +
"\x04save\x18\a \x01(\tR\x04save\x12\x10\n" +
"\x03var\x18\b \x01(\tR\x03var\x12I\n" +
"\x04expr\x18\t \x01(\v25.xray.transport.internet.finalmask.header.custom.ExprR\x04expr\"c\n" +
"\vTCPSequence\x12T\n" +
"\bsequence\x18\x01 \x03(\v28.xray.transport.internet.finalmask.header.custom.TCPItemR\bsequence\"\x91\x02\n" +
"\tTCPConfig\x12V\n" +
"\aclients\x18\x01 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\aclients\x12V\n" +
"\aservers\x18\x02 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\aservers\x12T\n" +
"\x06errors\x18\x03 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\x06errors\"k\n" +
"\x06errors\x18\x03 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\x06errors\"\xdc\x01\n" +
"\aUDPItem\x12\x12\n" +
"\x04rand\x18\x01 \x01(\x05R\x04rand\x12\x19\n" +
"\brand_min\x18\x02 \x01(\x05R\arandMin\x12\x19\n" +
"\brand_max\x18\x03 \x01(\x05R\arandMax\x12\x16\n" +
"\x06packet\x18\x04 \x01(\fR\x06packet\"\xaf\x01\n" +
"\x06packet\x18\x04 \x01(\fR\x06packet\x12\x12\n" +
"\x04save\x18\x05 \x01(\tR\x04save\x12\x10\n" +
"\x03var\x18\x06 \x01(\tR\x03var\x12I\n" +
"\x04expr\x18\a \x01(\v25.xray.transport.internet.finalmask.header.custom.ExprR\x04expr\"\xc3\x01\n" +
"\tUDPConfig\x12P\n" +
"\x06client\x18\x01 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06client\x12P\n" +
"\x06server\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06serverB\xaf\x01\n" +
"\x06server\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06server\x12\x12\n" +
"\x04mode\x18\x03 \x01(\tR\x04modeB\xaf\x01\n" +
"3com.xray.transport.internet.finalmask.header.customP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/header/custom\xaa\x02/Xray.Transport.Internet.Finalmask.Header.Customb\x06proto3"
var (
@@ -369,26 +624,32 @@ func file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP()
return file_transport_internet_finalmask_header_custom_config_proto_rawDescData
}
var file_transport_internet_finalmask_header_custom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_transport_internet_finalmask_header_custom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_transport_internet_finalmask_header_custom_config_proto_goTypes = []any{
(*TCPItem)(nil), // 0: xray.transport.internet.finalmask.header.custom.TCPItem
(*TCPSequence)(nil), // 1: xray.transport.internet.finalmask.header.custom.TCPSequence
(*TCPConfig)(nil), // 2: xray.transport.internet.finalmask.header.custom.TCPConfig
(*UDPItem)(nil), // 3: xray.transport.internet.finalmask.header.custom.UDPItem
(*UDPConfig)(nil), // 4: xray.transport.internet.finalmask.header.custom.UDPConfig
(*Expr)(nil), // 0: xray.transport.internet.finalmask.header.custom.Expr
(*ExprArg)(nil), // 1: xray.transport.internet.finalmask.header.custom.ExprArg
(*TCPItem)(nil), // 2: xray.transport.internet.finalmask.header.custom.TCPItem
(*TCPSequence)(nil), // 3: xray.transport.internet.finalmask.header.custom.TCPSequence
(*TCPConfig)(nil), // 4: xray.transport.internet.finalmask.header.custom.TCPConfig
(*UDPItem)(nil), // 5: xray.transport.internet.finalmask.header.custom.UDPItem
(*UDPConfig)(nil), // 6: xray.transport.internet.finalmask.header.custom.UDPConfig
}
var file_transport_internet_finalmask_header_custom_config_proto_depIdxs = []int32{
0, // 0: xray.transport.internet.finalmask.header.custom.TCPSequence.sequence:type_name -> xray.transport.internet.finalmask.header.custom.TCPItem
1, // 1: xray.transport.internet.finalmask.header.custom.TCPConfig.clients:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence
1, // 2: xray.transport.internet.finalmask.header.custom.TCPConfig.servers:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence
1, // 3: xray.transport.internet.finalmask.header.custom.TCPConfig.errors:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence
3, // 4: xray.transport.internet.finalmask.header.custom.UDPConfig.client:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem
3, // 5: xray.transport.internet.finalmask.header.custom.UDPConfig.server:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem
6, // [6:6] is the sub-list for method output_type
6, // [6:6] 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
1, // 0: xray.transport.internet.finalmask.header.custom.Expr.args:type_name -> xray.transport.internet.finalmask.header.custom.ExprArg
0, // 1: xray.transport.internet.finalmask.header.custom.ExprArg.expr:type_name -> xray.transport.internet.finalmask.header.custom.Expr
0, // 2: xray.transport.internet.finalmask.header.custom.TCPItem.expr:type_name -> xray.transport.internet.finalmask.header.custom.Expr
2, // 3: xray.transport.internet.finalmask.header.custom.TCPSequence.sequence:type_name -> xray.transport.internet.finalmask.header.custom.TCPItem
3, // 4: xray.transport.internet.finalmask.header.custom.TCPConfig.clients:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence
3, // 5: xray.transport.internet.finalmask.header.custom.TCPConfig.servers:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence
3, // 6: xray.transport.internet.finalmask.header.custom.TCPConfig.errors:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence
0, // 7: xray.transport.internet.finalmask.header.custom.UDPItem.expr:type_name -> xray.transport.internet.finalmask.header.custom.Expr
5, // 8: xray.transport.internet.finalmask.header.custom.UDPConfig.client:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem
5, // 9: xray.transport.internet.finalmask.header.custom.UDPConfig.server:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem
10, // [10:10] is the sub-list for method output_type
10, // [10:10] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
}
func init() { file_transport_internet_finalmask_header_custom_config_proto_init() }
@@ -396,13 +657,20 @@ func file_transport_internet_finalmask_header_custom_config_proto_init() {
if File_transport_internet_finalmask_header_custom_config_proto != nil {
return
}
file_transport_internet_finalmask_header_custom_config_proto_msgTypes[1].OneofWrappers = []any{
(*ExprArg_Bytes)(nil),
(*ExprArg_U64)(nil),
(*ExprArg_Var)(nil),
(*ExprArg_Metadata)(nil),
(*ExprArg_Expr)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_custom_config_proto_rawDesc), len(file_transport_internet_finalmask_header_custom_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 5,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
@@ -6,6 +6,21 @@ option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/head
option java_package = "com.xray.transport.internet.finalmask.header.custom";
option java_multiple_files = true;
message Expr {
string op = 1;
repeated ExprArg args = 2;
}
message ExprArg {
oneof value {
bytes bytes = 1;
uint64 u64 = 2;
string var = 3;
string metadata = 4;
Expr expr = 5;
}
}
message TCPItem {
int64 delay_min = 1;
int64 delay_max = 2;
@@ -13,6 +28,9 @@ message TCPItem {
int32 rand_min = 4;
int32 rand_max = 5;
bytes packet = 6;
string save = 7;
string var = 8;
Expr expr = 9;
}
message TCPSequence {
@@ -30,9 +48,13 @@ message UDPItem {
int32 rand_min = 2;
int32 rand_max = 3;
bytes packet = 4;
string save = 5;
string var = 6;
Expr expr = 7;
}
message UDPConfig {
repeated UDPItem client = 1;
repeated UDPItem server = 2;
}
string mode = 3;
}
@@ -0,0 +1,625 @@
package custom
import (
"encoding/binary"
"net"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
)
type evalValue struct {
bytes []byte
u64 *uint64
isBytes bool
}
type evalContext struct {
vars map[string][]byte
metadata map[string]evalValue
}
func newEvalContext() *evalContext {
return &evalContext{
vars: make(map[string][]byte),
metadata: make(map[string]evalValue),
}
}
func newEvalContextWithAddrs(local, remote net.Addr) *evalContext {
ctx := newEvalContext()
loadMetadata(ctx.metadata, "local", local)
loadMetadata(ctx.metadata, "remote", remote)
return ctx
}
func evaluateUDPItems(items []*UDPItem) ([]byte, error) {
return evaluateUDPItemsWithContext(items, newEvalContext())
}
func evaluateUDPItemsWithContext(items []*UDPItem, ctx *evalContext) ([]byte, error) {
var out []byte
for _, item := range items {
value, err := evaluateItem(item.Rand, item.RandMin, item.RandMax, item.Packet, item.Save, item.Var, item.Expr, ctx)
if err != nil {
return nil, err
}
out = append(out, value...)
}
return out, nil
}
func measureUDPItems(items []*UDPItem) (int, error) {
return measureUDPItemsWithFallback(items, nil)
}
func measureUDPItemsWithFallback(items []*UDPItem, fallback map[string]int) (int, error) {
sizeCtx := make(map[string]int)
for key, value := range fallback {
sizeCtx[key] = value
}
total := 0
for _, item := range items {
itemSize, err := measureItem(item.Rand, item.Packet, item.Save, item.Var, item.Expr, sizeCtx)
if err != nil {
return 0, err
}
total += itemSize
}
return total, nil
}
func collectSavedUDPSizes(items []*UDPItem) map[string]int {
sizeCtx := make(map[string]int)
for _, item := range items {
itemSize, err := measureItem(item.Rand, item.Packet, item.Save, item.Var, item.Expr, sizeCtx)
if err != nil {
continue
}
if item.Save != "" {
sizeCtx[item.Save] = itemSize
}
}
return sizeCtx
}
func measureItem(randLen int32, packet []byte, save, varName string, expr *Expr, sizeCtx map[string]int) (int, error) {
var size int
switch {
case randLen > 0:
size = int(randLen)
case len(packet) > 0:
size = len(packet)
case varName != "":
length, ok := sizeCtx[varName]
if !ok {
return 0, errors.New("unknown variable: ", varName)
}
size = length
case expr != nil:
exprSize, err := measureExpr(expr, sizeCtx)
if err != nil {
return 0, err
}
size = exprSize
default:
size = 0
}
if save != "" {
sizeCtx[save] = size
}
return size, nil
}
func evaluateTCPSequence(sequence *TCPSequence) ([]byte, error) {
ctx := newEvalContext()
var out []byte
for _, item := range sequence.Sequence {
value, err := evaluateItem(item.Rand, item.RandMin, item.RandMax, item.Packet, item.Save, item.Var, item.Expr, ctx)
if err != nil {
return nil, err
}
out = append(out, value...)
}
return out, nil
}
func evaluateItem(randLen, randMin, randMax int32, packet []byte, save, varName string, expr *Expr, ctx *evalContext) ([]byte, error) {
var value []byte
switch {
case randLen > 0:
value = make([]byte, randLen)
crypto.RandBytesBetween(value, byte(randMin), byte(randMax))
case len(packet) > 0:
value = append([]byte(nil), packet...)
case varName != "":
saved, ok := ctx.vars[varName]
if !ok {
return nil, errors.New("unknown variable: ", varName)
}
value = append([]byte(nil), saved...)
case expr != nil:
evaluated, err := evaluateExpr(expr, ctx)
if err != nil {
return nil, err
}
bytesValue, err := evaluated.asBytes()
if err != nil {
return nil, err
}
value = bytesValue
default:
value = nil
}
if save != "" {
ctx.vars[save] = append([]byte(nil), value...)
}
return value, nil
}
func evaluateExpr(expr *Expr, ctx *evalContext) (evalValue, error) {
switch expr.GetOp() {
case "concat":
var out []byte
for _, arg := range expr.GetArgs() {
value, err := evaluateExprArg(arg, ctx)
if err != nil {
return evalValue{}, err
}
bytesValue, err := value.asBytes()
if err != nil {
return evalValue{}, err
}
out = append(out, bytesValue...)
}
return evalValue{bytes: out, isBytes: true}, nil
case "slice":
if len(expr.GetArgs()) != 3 {
return evalValue{}, errors.New("slice expects 3 args")
}
source, err := evaluateExprArg(expr.GetArgs()[0], ctx)
if err != nil {
return evalValue{}, err
}
offset, err := evaluateExprArg(expr.GetArgs()[1], ctx)
if err != nil {
return evalValue{}, err
}
length, err := evaluateExprArg(expr.GetArgs()[2], ctx)
if err != nil {
return evalValue{}, err
}
sourceBytes, err := source.asBytes()
if err != nil {
return evalValue{}, err
}
offsetU64, err := offset.asU64()
if err != nil {
return evalValue{}, err
}
lengthU64, err := length.asU64()
if err != nil {
return evalValue{}, err
}
end := offsetU64 + lengthU64
if end > uint64(len(sourceBytes)) {
return evalValue{}, errors.New("slice out of bounds")
}
return evalValue{bytes: append([]byte(nil), sourceBytes[offsetU64:end]...), isBytes: true}, nil
case "xor16":
return evaluateXor(expr.GetArgs(), 0xFFFF, 2, ctx)
case "xor32":
return evaluateXor(expr.GetArgs(), 0xFFFFFFFF, 4, ctx)
case "be16":
return evaluatePack(expr.GetArgs(), "be16", 2, binary.BigEndian, ctx)
case "be32":
return evaluatePack(expr.GetArgs(), "be32", 4, binary.BigEndian, ctx)
case "le16":
return evaluatePack(expr.GetArgs(), "le16", 2, binary.LittleEndian, ctx)
case "le32":
return evaluatePack(expr.GetArgs(), "le32", 4, binary.LittleEndian, ctx)
case "le64":
return evaluatePack(expr.GetArgs(), "le64", 8, binary.LittleEndian, ctx)
case "pad":
return evaluatePad(expr.GetArgs(), ctx)
case "truncate":
return evaluateTruncate(expr.GetArgs(), ctx)
case "add":
return evaluateBinaryU64Op(expr.GetArgs(), "add", ctx, func(left, right uint64) (uint64, error) {
if left > ^uint64(0)-right {
return 0, errors.New("add overflow")
}
return left + right, nil
})
case "sub":
return evaluateBinaryU64Op(expr.GetArgs(), "sub", ctx, func(left, right uint64) (uint64, error) {
if left < right {
return 0, errors.New("sub underflow")
}
return left - right, nil
})
case "and":
return evaluateBinaryU64Op(expr.GetArgs(), "and", ctx, func(left, right uint64) (uint64, error) {
return left & right, nil
})
case "or":
return evaluateBinaryU64Op(expr.GetArgs(), "or", ctx, func(left, right uint64) (uint64, error) {
return left | right, nil
})
case "shl":
return evaluateShift(expr.GetArgs(), "shl", ctx, func(value uint64, shift uint) (uint64, error) {
if shift >= 64 {
return 0, errors.New("shift out of range")
}
if value > (^uint64(0) >> shift) {
return 0, errors.New("shl overflow")
}
return value << shift, nil
})
case "shr":
return evaluateShift(expr.GetArgs(), "shr", ctx, func(value uint64, shift uint) (uint64, error) {
if shift >= 64 {
return 0, errors.New("shift out of range")
}
return value >> shift, nil
})
default:
return evalValue{}, errors.New("unsupported expr op: ", expr.GetOp())
}
}
func evaluatePack(args []*ExprArg, name string, width int, order binary.ByteOrder, ctx *evalContext) (evalValue, error) {
if len(args) != 1 {
return evalValue{}, errors.New(name, " expects 1 arg")
}
value, err := evaluateExprArg(args[0], ctx)
if err != nil {
return evalValue{}, err
}
u64Value, err := value.asU64()
if err != nil {
return evalValue{}, err
}
switch width {
case 2:
if u64Value > 0xFFFF {
return evalValue{}, errors.New(name, " overflow")
}
out := make([]byte, 2)
order.PutUint16(out, uint16(u64Value))
return evalValue{bytes: out, isBytes: true}, nil
case 4:
if u64Value > 0xFFFFFFFF {
return evalValue{}, errors.New(name, " overflow")
}
out := make([]byte, 4)
order.PutUint32(out, uint32(u64Value))
return evalValue{bytes: out, isBytes: true}, nil
case 8:
out := make([]byte, 8)
order.PutUint64(out, u64Value)
return evalValue{bytes: out, isBytes: true}, nil
default:
return evalValue{}, errors.New("unsupported pack width")
}
}
func evaluatePad(args []*ExprArg, ctx *evalContext) (evalValue, error) {
if len(args) != 3 {
return evalValue{}, errors.New("pad expects 3 args")
}
source, err := evaluateExprArg(args[0], ctx)
if err != nil {
return evalValue{}, err
}
target, err := evaluateExprArg(args[1], ctx)
if err != nil {
return evalValue{}, err
}
fill, err := evaluateExprArg(args[2], ctx)
if err != nil {
return evalValue{}, err
}
sourceBytes, err := source.asBytes()
if err != nil {
return evalValue{}, err
}
targetU64, err := target.asU64()
if err != nil {
return evalValue{}, err
}
fillBytes, err := fill.asBytes()
if err != nil {
return evalValue{}, err
}
if len(fillBytes) == 0 {
return evalValue{}, errors.New("pad fill must not be empty")
}
if targetU64 < uint64(len(sourceBytes)) {
return evalValue{}, errors.New("pad target shorter than source")
}
out := append([]byte(nil), sourceBytes...)
for uint64(len(out)) < targetU64 {
remaining := int(targetU64) - len(out)
if remaining >= len(fillBytes) {
out = append(out, fillBytes...)
continue
}
out = append(out, fillBytes[:remaining]...)
}
return evalValue{bytes: out, isBytes: true}, nil
}
func evaluateTruncate(args []*ExprArg, ctx *evalContext) (evalValue, error) {
if len(args) != 2 {
return evalValue{}, errors.New("truncate expects 2 args")
}
source, err := evaluateExprArg(args[0], ctx)
if err != nil {
return evalValue{}, err
}
length, err := evaluateExprArg(args[1], ctx)
if err != nil {
return evalValue{}, err
}
sourceBytes, err := source.asBytes()
if err != nil {
return evalValue{}, err
}
lengthU64, err := length.asU64()
if err != nil {
return evalValue{}, err
}
if lengthU64 > uint64(len(sourceBytes)) {
return evalValue{}, errors.New("truncate out of bounds")
}
return evalValue{bytes: append([]byte(nil), sourceBytes[:lengthU64]...), isBytes: true}, nil
}
func evaluateBinaryU64Op(args []*ExprArg, name string, ctx *evalContext, op func(left, right uint64) (uint64, error)) (evalValue, error) {
if len(args) != 2 {
return evalValue{}, errors.New(name, " expects 2 args")
}
left, err := evaluateExprArg(args[0], ctx)
if err != nil {
return evalValue{}, err
}
right, err := evaluateExprArg(args[1], ctx)
if err != nil {
return evalValue{}, err
}
leftU64, err := left.asU64()
if err != nil {
return evalValue{}, err
}
rightU64, err := right.asU64()
if err != nil {
return evalValue{}, err
}
result, err := op(leftU64, rightU64)
if err != nil {
return evalValue{}, err
}
return evalValue{u64: &result}, nil
}
func evaluateShift(args []*ExprArg, name string, ctx *evalContext, op func(value uint64, shift uint) (uint64, error)) (evalValue, error) {
if len(args) != 2 {
return evalValue{}, errors.New(name, " expects 2 args")
}
value, err := evaluateExprArg(args[0], ctx)
if err != nil {
return evalValue{}, err
}
shift, err := evaluateExprArg(args[1], ctx)
if err != nil {
return evalValue{}, err
}
valueU64, err := value.asU64()
if err != nil {
return evalValue{}, err
}
shiftU64, err := shift.asU64()
if err != nil {
return evalValue{}, err
}
if shiftU64 >= 64 {
return evalValue{}, errors.New("shift out of range")
}
result, err := op(valueU64, uint(shiftU64))
if err != nil {
return evalValue{}, err
}
return evalValue{u64: &result}, nil
}
func evaluateXor(args []*ExprArg, mask uint64, width int, ctx *evalContext) (evalValue, error) {
if len(args) != 2 {
return evalValue{}, errors.New("xor expects 2 args")
}
left, err := evaluateExprArg(args[0], ctx)
if err != nil {
return evalValue{}, err
}
right, err := evaluateExprArg(args[1], ctx)
if err != nil {
return evalValue{}, err
}
leftU64, err := left.asU64()
if err != nil {
return evalValue{}, err
}
rightU64, err := right.asU64()
if err != nil {
return evalValue{}, err
}
if width == 2 && (leftU64 > 0xFFFF || rightU64 > 0xFFFF) {
return evalValue{}, errors.New("xor16 overflow")
}
if width == 4 && (leftU64 > 0xFFFFFFFF || rightU64 > 0xFFFFFFFF) {
return evalValue{}, errors.New("xor32 overflow")
}
result := (leftU64 ^ rightU64) & mask
return evalValue{u64: &result}, nil
}
func measureExpr(expr *Expr, sizeCtx map[string]int) (int, error) {
switch expr.GetOp() {
case "concat":
total := 0
for _, arg := range expr.GetArgs() {
size, err := measureExprArg(arg, sizeCtx)
if err != nil {
return 0, err
}
total += size
}
return total, nil
case "slice":
if len(expr.GetArgs()) != 3 {
return 0, errors.New("slice expects 3 args")
}
lengthArg := expr.GetArgs()[2]
if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok {
return int(value.U64), nil
}
return 0, errors.New("slice length must be u64")
case "be16":
return 2, nil
case "be32":
return 4, nil
case "le16":
return 2, nil
case "le32":
return 4, nil
case "le64":
return 8, nil
case "pad":
if len(expr.GetArgs()) != 3 {
return 0, errors.New("pad expects 3 args")
}
lengthArg := expr.GetArgs()[1]
if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok {
return int(value.U64), nil
}
return 0, errors.New("pad length must be u64")
case "truncate":
if len(expr.GetArgs()) != 2 {
return 0, errors.New("truncate expects 2 args")
}
lengthArg := expr.GetArgs()[1]
if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok {
return int(value.U64), nil
}
return 0, errors.New("truncate length must be u64")
default:
return 0, errors.New("expr size is not bytes for op: ", expr.GetOp())
}
}
func evaluateExprArg(arg *ExprArg, ctx *evalContext) (evalValue, error) {
switch value := arg.GetValue().(type) {
case *ExprArg_Bytes:
return evalValue{bytes: append([]byte(nil), value.Bytes...), isBytes: true}, nil
case *ExprArg_U64:
return evalValue{u64: &value.U64}, nil
case *ExprArg_Var:
saved, ok := ctx.vars[value.Var]
if !ok {
return evalValue{}, errors.New("unknown variable: ", value.Var)
}
return evalValue{bytes: append([]byte(nil), saved...), isBytes: true}, nil
case *ExprArg_Metadata:
metadata, ok := ctx.metadata[value.Metadata]
if !ok {
return evalValue{}, errors.New("unknown metadata: ", value.Metadata)
}
return metadata, nil
case *ExprArg_Expr:
return evaluateExpr(value.Expr, ctx)
default:
return evalValue{}, errors.New("empty expr arg")
}
}
func measureExprArg(arg *ExprArg, sizeCtx map[string]int) (int, error) {
switch value := arg.GetValue().(type) {
case *ExprArg_Bytes:
return len(value.Bytes), nil
case *ExprArg_U64:
return 0, errors.New("u64 arg has no byte width")
case *ExprArg_Var:
length, ok := sizeCtx[value.Var]
if !ok {
return 0, errors.New("unknown variable: ", value.Var)
}
return length, nil
case *ExprArg_Metadata:
return 0, errors.New("metadata not implemented: ", value.Metadata)
case *ExprArg_Expr:
return measureExpr(value.Expr, sizeCtx)
default:
return 0, errors.New("empty expr arg")
}
}
func (v evalValue) asBytes() ([]byte, error) {
if v.isBytes {
return append([]byte(nil), v.bytes...), nil
}
return nil, errors.New("expr value is not bytes")
}
func (v evalValue) asU64() (uint64, error) {
if v.u64 != nil {
return *v.u64, nil
}
return 0, errors.New("expr value is not u64")
}
func sizeMapFromEvalContext(ctx *evalContext) map[string]int {
sizes := make(map[string]int, len(ctx.vars))
for key, value := range ctx.vars {
sizes[key] = len(value)
}
return sizes
}
func loadMetadata(dst map[string]evalValue, prefix string, addr net.Addr) {
if addr == nil {
return
}
switch value := addr.(type) {
case *net.UDPAddr:
loadIPPortMetadata(dst, prefix, value.IP, value.Port)
case *net.TCPAddr:
loadIPPortMetadata(dst, prefix, value.IP, value.Port)
}
}
func loadIPPortMetadata(dst map[string]evalValue, prefix string, ip net.IP, port int) {
portValue := uint64(port)
dst[prefix+"_port"] = evalValue{u64: &portValue}
if prefix == "remote" {
dst["src_port_u16"] = evalValue{u64: &portValue}
} else if prefix == "local" {
dst["dst_port_u16"] = evalValue{u64: &portValue}
}
if ip4 := ip.To4(); ip4 != nil {
ipValue := uint64(binary.BigEndian.Uint32(ip4))
dst[prefix+"_ip4_u32"] = evalValue{u64: &ipValue}
if prefix == "remote" {
dst["src_ip4_u32"] = evalValue{u64: &ipValue}
} else if prefix == "local" {
dst["dst_ip4_u32"] = evalValue{u64: &ipValue}
}
}
}
@@ -0,0 +1,491 @@
package custom
import (
"bytes"
"testing"
)
func TestEvaluatorSaveAndReuseWithinPacket(t *testing.T) {
items := []*UDPItem{
{
Rand: 4,
RandMin: 0x2A,
RandMax: 0x2A,
Save: "txid",
},
{
Var: "txid",
},
}
got, err := evaluateUDPItems(items)
if err != nil {
t.Fatal(err)
}
want := bytes.Repeat([]byte{0x2A}, 8)
if !bytes.Equal(got, want) {
t.Fatalf("unexpected output: %x", got)
}
}
func TestEvaluatorSliceReturnsWindow(t *testing.T) {
sequence := &TCPSequence{
Sequence: []*TCPItem{
{
Expr: &Expr{
Op: "slice",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}},
{Value: &ExprArg_U64{U64: 1}},
{Value: &ExprArg_U64{U64: 2}},
},
},
},
},
}
got, err := evaluateTCPSequence(sequence)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, []byte{2, 3}) {
t.Fatalf("unexpected output: %x", got)
}
}
func TestEvaluatorConcatPreservesOrder(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "concat",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte("ab")}},
{Value: &ExprArg_Bytes{Bytes: []byte("cd")}},
{Value: &ExprArg_Bytes{Bytes: []byte("ef")}},
},
},
},
}
got, err := evaluateUDPItems(items)
if err != nil {
t.Fatal(err)
}
if string(got) != "abcdef" {
t.Fatalf("unexpected output: %q", got)
}
}
func TestEvaluatorBeXorProducesExpectedBytes(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "xor16",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 0x1234}},
{Value: &ExprArg_U64{U64: 0xFFFF}},
},
},
},
},
},
},
},
}
got, err := evaluateUDPItems(items)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, []byte{0xED, 0xCB}) {
t.Fatalf("unexpected output: %x", got)
}
}
func TestEvaluatorRejectsInvalidArgType(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{0x01}}},
},
},
},
}
_, err := evaluateUDPItems(items)
if err == nil {
t.Fatal("expected evaluator error")
}
}
func TestEvaluatorLittleEndianProducesExpectedBytes(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "concat",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "le16",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 0x1234}},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "le32",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 0xA1B2C3D4}},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "le64",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 0x0102030405060708}},
},
},
},
},
},
},
},
}
got, err := evaluateUDPItems(items)
if err != nil {
t.Fatal(err)
}
want := []byte{
0x34, 0x12,
0xD4, 0xC3, 0xB2, 0xA1,
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected output: %x", got)
}
}
func TestEvaluatorPadAndTruncateShapeBytes(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "concat",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "pad",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{0xAA, 0xBB}}},
{Value: &ExprArg_U64{U64: 5}},
{Value: &ExprArg_Bytes{Bytes: []byte{0xCC, 0xDD}}},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "truncate",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}},
{Value: &ExprArg_U64{U64: 2}},
},
},
},
},
},
},
},
}
got, err := evaluateUDPItems(items)
if err != nil {
t.Fatal(err)
}
want := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xCC, 0x01, 0x02}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected output: %x", got)
}
}
func TestMeasureUDPItemsSupportsPadAndTruncate(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "pad",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}},
{Value: &ExprArg_U64{U64: 4}},
{Value: &ExprArg_Bytes{Bytes: []byte{0x00}}},
},
},
},
{
Expr: &Expr{
Op: "truncate",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}},
{Value: &ExprArg_U64{U64: 3}},
},
},
},
}
got, err := measureUDPItems(items)
if err != nil {
t.Fatal(err)
}
if got != 7 {
t.Fatalf("unexpected size: %d", got)
}
}
func TestEvaluatorArithmeticAndBitwiseProduceExpectedBytes(t *testing.T) {
items := []*UDPItem{
{
Expr: &Expr{
Op: "concat",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "add",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 1}},
{Value: &ExprArg_U64{U64: 2}},
},
},
},
},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "sub",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 10}},
{Value: &ExprArg_U64{U64: 3}},
},
},
},
},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "and",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 0xF0F0}},
{Value: &ExprArg_U64{U64: 0x0FF0}},
},
},
},
},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "or",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "shl",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 1}},
{Value: &ExprArg_U64{U64: 8}},
},
},
},
},
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "shr",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 0x80}},
{Value: &ExprArg_U64{U64: 7}},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}
got, err := evaluateUDPItems(items)
if err != nil {
t.Fatal(err)
}
want := []byte{
0x00, 0x03,
0x00, 0x07,
0x00, 0xF0,
0x01, 0x01,
}
if !bytes.Equal(got, want) {
t.Fatalf("unexpected output: %x", got)
}
}
func TestEvaluatorRejectsInvalidShapingAndArithmetic(t *testing.T) {
tests := []struct {
name string
items []*UDPItem
match string
}{
{
name: "pad with empty fill",
items: []*UDPItem{
{
Expr: &Expr{
Op: "pad",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}},
{Value: &ExprArg_U64{U64: 4}},
{Value: &ExprArg_Bytes{Bytes: []byte{}}},
},
},
},
},
match: "pad fill",
},
{
name: "truncate beyond source",
items: []*UDPItem{
{
Expr: &Expr{
Op: "truncate",
Args: []*ExprArg{
{Value: &ExprArg_Bytes{Bytes: []byte{1, 2}}},
{Value: &ExprArg_U64{U64: 3}},
},
},
},
},
match: "truncate",
},
{
name: "sub underflow",
items: []*UDPItem{
{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "sub",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 1}},
{Value: &ExprArg_U64{U64: 2}},
},
},
},
},
},
},
},
},
match: "underflow",
},
{
name: "shift too large",
items: []*UDPItem{
{
Expr: &Expr{
Op: "be16",
Args: []*ExprArg{
{
Value: &ExprArg_Expr{
Expr: &Expr{
Op: "shl",
Args: []*ExprArg{
{Value: &ExprArg_U64{U64: 1}},
{Value: &ExprArg_U64{U64: 64}},
},
},
},
},
},
},
},
},
match: "shift",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := evaluateUDPItems(tt.items)
if err == nil {
t.Fatal("expected evaluator error")
}
if !bytes.Contains([]byte(err.Error()), []byte(tt.match)) {
t.Fatalf("unexpected error: %v", err)
}
})
}
}

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