Compare commits

...

36 Commits

Author SHA1 Message Date
Fangliding ba8157f2f1 Handle usage on packet-up switch 2026-05-11 15:07:56 +08:00
Fangliding 1c4f3ec598 Use hKeepAlivePeriod for xhttp3 if not specified in quicParams 2026-05-11 14:53:00 +08:00
RPRX 228f1e13aa Xray-core v26.5.3
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

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

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-05-03 11:53:13 +00:00
LjhAUMEM 15968585f3 quicParams config: Better unmarshaling udpHop (#6068)
Fixes https://github.com/XTLS/Xray-core/issues/6067#issuecomment-4365869178
2026-05-03 11:04:54 +00:00
Relsa 4951994ebe README.md: Remove NetProxy-Magisk from Magisk & Android Clients (#6066)
https://github.com/XTLS/Xray-core/pull/6066#issuecomment-4365784320

---------

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

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

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

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

---------

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

---------

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

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

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

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

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

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 22:31:59 +00:00
RPRX 1836b1c6e4 Browser Dialer: Potential optimized IP and non-standard port
Fixes https://github.com/2dust/v2rayNG/issues/5519#issuecomment-4335112057
2026-04-28 20:57:20 +00:00
RPRX b4f08981be v26.4.25
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-04-25 23:16:35 +00:00
RPRX cd4d0baacd Xray-core: Mark "legacy reverse" as removed to avoid confusions
https://github.com/XTLS/Xray-core/issues/5973#issuecomment-4273582111

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

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

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

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

---------

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

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

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

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

---------

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 15:09:05 +00:00
128 changed files with 5124 additions and 2146 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
+5 -2
View File
@@ -73,7 +73,6 @@
- [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless)
- [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- Magisk
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk)
- [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
- Homebrew
@@ -111,6 +110,8 @@
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- [XrayUI-dev](https://github.com/PhoenixNil/XrayUI-dev)
- Android
- [v2rayNG](https://github.com/2dust/v2rayNG)
- [X-flutter](https://github.com/XTLS/X-flutter)
@@ -118,7 +119,7 @@
- [SimpleXray](https://github.com/lhear/SimpleXray)
- [XrayFA](https://github.com/Q7DF1/XrayFA)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- [OneXray](https://github.com/OneXray/OneXray)
- iOS & macOS arm64 & tvOS
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064)
@@ -143,10 +144,12 @@
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
- iOS & macOS arm64 & tvOS
- [Anywhere](https://github.com/NodePassProject/Anywhere)
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
- [Loon](https://apps.apple.com/us/app/loon/id1373567447)
- [Egern](https://apps.apple.com/us/app/egern/id1616105820)
+19 -4
View File
@@ -4,12 +4,14 @@ import (
"context"
"net"
"sync"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/signal/done"
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/transport/internet"
"google.golang.org/grpc"
)
@@ -73,14 +75,27 @@ func (c *Commander) Start() error {
}
}
if len(c.listen) > 0 {
if l, err := net.Listen("tcp", c.listen); err != nil {
var addr net.Addr
if strings.HasPrefix(c.listen, "/") || strings.HasPrefix(c.listen, "@") {
addr = &net.UnixAddr{Name: c.listen, Net: "unix"}
} else {
tcpAddr, err := net.ResolveTCPAddr("tcp", c.listen)
if err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to parse listen address ", c.listen)
return err
}
addr = tcpAddr
}
l, err := internet.ListenSystem(context.Background(), addr, nil)
if err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen)
return err
} else {
errors.LogInfo(context.Background(), "API server listening on ", l.Addr())
go listen(l)
}
errors.LogInfo(context.Background(), "API server listening on ", l.Addr())
go listen(l)
return nil
}
+27 -22
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,25 +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 {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
if s.domainMatcher != nil {
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
}
+8 -8
View File
@@ -148,7 +148,7 @@ func TestUDPServerSubnet(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -210,7 +210,7 @@ func TestUDPServer(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -350,7 +350,7 @@ func TestPrioritizedDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -421,7 +421,7 @@ func TestUDPServerIPv6(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -490,7 +490,7 @@ func TestStaticHostDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -577,7 +577,7 @@ func TestIPMatch(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -696,7 +696,7 @@ func TestLocalDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -953,7 +953,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+8 -12
View File
@@ -17,7 +17,7 @@ import (
type Holder struct {
domainToIP cache.Lru
ipRange *net.IPNet
mu *sync.Mutex
mu sync.Mutex
config *FakeDnsPool
}
@@ -49,9 +49,7 @@ func (fkdns *Holder) Start() error {
}
func (fkdns *Holder) Close() error {
fkdns.domainToIP = nil
fkdns.ipRange = nil
fkdns.mu = nil
// nothing to do for now, just wait GC
return nil
}
@@ -70,7 +68,7 @@ func NewFakeDNSHolder() (*Holder, error) {
}
func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) {
return &Holder{nil, nil, nil, conf}, nil
return &Holder{config: conf}, nil
}
func (fkdns *Holder) initializeFromConfig() error {
@@ -92,7 +90,6 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
}
fkdns.domainToIP = cache.NewLru(lruSize)
fkdns.ipRange = ipRange
fkdns.mu = new(sync.Mutex)
return nil
}
@@ -103,7 +100,7 @@ func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
if v, ok := fkdns.domainToIP.Get(domain); ok {
return []net.Address{v.(net.Address)}
}
currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
currentTimeMillis := uint64(time.Now().UnixMilli())
ones, bits := fkdns.ipRange.Mask.Size()
rooms := bits - ones
if rooms < 64 {
@@ -202,12 +199,11 @@ func (h *HolderMulti) Start() error {
}
func (h *HolderMulti) Close() error {
var errs []error
for _, v := range h.holders {
if err := v.Close(); err != nil {
return errors.New("Cannot close all fake dns pools").Base(err)
}
errs = append(errs, v.Close())
}
return nil
return errors.Combine(errs...)
}
func (h *HolderMulti) createHolderGroups() error {
@@ -222,7 +218,7 @@ func (h *HolderMulti) createHolderGroups() error {
}
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
holderMulti := &HolderMulti{nil, conf}
holderMulti := &HolderMulti{config: conf}
if err := holderMulti.createHolderGroups(); err != nil {
return nil, err
}
+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))
}))
}
+4 -6
View File
@@ -18,9 +18,9 @@ import (
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/proxy/hysteria/account"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
hysteria_proxy "github.com/xtls/xray-core/proxy/hysteria"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/udp"
@@ -134,10 +134,8 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
func (w *tcpWorker) Start() error {
ctx := context.Background()
type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator }
if v, ok := w.proxy.(HysteriaInboundValidator); ok {
ctx = hyCtx.ContextWithRequireDatagram(ctx, true)
ctx = hyCtx.ContextWithValidator(ctx, v.HysteriaInboundValidator())
if v, ok := w.proxy.(*hysteria_proxy.Server); ok {
ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator())
}
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
+3 -3
View File
@@ -48,7 +48,7 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
})
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*stat.CounterConnection)
@@ -78,7 +78,7 @@ func TestOutboundWithStatCounter(t *testing.T) {
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
})
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*stat.CounterConnection)
@@ -118,7 +118,7 @@ func TestTagsCache(t *testing.T) {
tag := fmt.Sprintf("%s%d", tags_prefix, idx)
cfg := &core.OutboundHandlerConfig{
Tag: tag,
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
}
if h, err := NewHandler(ctx, cfg); err == nil {
if err := ohm.AddHandler(ctx, h); err == nil {
+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())
}
+65 -13
View File
@@ -23,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) {
@@ -57,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
@@ -87,16 +136,19 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
g.Add(m, 0)
s.Add(m)
}
f.shared[key] = g
return g, err
f.shared[key] = s
return s, err
}
// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
@@ -126,7 +178,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherGroup
matchers []strmatcher.MatcherSet
values []uint32
}
@@ -178,8 +230,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
default:
return &MphDomainMatcherFactory{}
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
}
}
+3 -3
View File
@@ -10,7 +10,7 @@ import (
)
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
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"}}},
@@ -31,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
factory := &CompactDomainMatcherFactory{shared: 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"}}},
@@ -50,7 +50,7 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
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"}}},
})
+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
}
+15 -2
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
@@ -1007,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)}
}
+84
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",
+120 -2
View File
@@ -1,17 +1,135 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
return buildOptimizedIPMatcher(r.ipsetFactory, rules)
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
return nil
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
ipsetFactory: newIPSetFactory(),
}
}
var IPReg = newIPRegistry()
type ipMatcherState struct {
matcher IPMatcher
}
type DynamicIPMatcher struct {
rules []*IPRule
state atomic.Pointer[ipMatcherState]
mu sync.Mutex
reverse bool
reverseSet bool
}
// Match implements IPMatcher.
func (d *DynamicIPMatcher) Match(ip net.IP) bool {
return d.state.Load().matcher.Match(ip)
}
// AnyMatch implements IPMatcher.
func (d *DynamicIPMatcher) AnyMatch(ips []net.IP) bool {
return d.state.Load().matcher.AnyMatch(ips)
}
// Matches implements IPMatcher.
func (d *DynamicIPMatcher) Matches(ips []net.IP) bool {
return d.state.Load().matcher.Matches(ips)
}
// FilterIPs implements IPMatcher.
func (d *DynamicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
return d.state.Load().matcher.FilterIPs(ips)
}
// ToggleReverse implements IPMatcher.
func (d *DynamicIPMatcher) ToggleReverse() {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = !d.reverse
d.state.Load().matcher.ToggleReverse()
}
// SetReverse implements IPMatcher.
func (d *DynamicIPMatcher) SetReverse(reverse bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = reverse
d.reverseSet = true
d.state.Load().matcher.SetReverse(reverse)
}
func (d *DynamicIPMatcher) Reload(newMatcher IPMatcher) {
d.mu.Lock()
defer d.mu.Unlock()
if d.reverseSet {
newMatcher.SetReverse(d.reverse)
} else if d.reverse {
newMatcher.ToggleReverse()
}
d.state.Store(&ipMatcherState{matcher: newMatcher})
}
func NewDynamicIPMatcher(rules []*IPRule, matcher IPMatcher) *DynamicIPMatcher {
d := &DynamicIPMatcher{rules: rules}
d.Reload(matcher)
return d
}
@@ -0,0 +1,53 @@
package strmatcher
// LinearAnyMatcher is an implementation of AnyMatcher.
type LinearAnyMatcher struct {
full *FullMatcherSet
domain *DomainMatcherSet
substr *SubstrMatcherSet
regex *SimpleMatcherSet
}
func NewLinearAnyMatcher() *LinearAnyMatcher {
return new(LinearAnyMatcher)
}
// Add implements AnyMatcher.Add.
func (s *LinearAnyMatcher) Add(matcher Matcher) {
switch matcher := matcher.(type) {
case FullMatcher:
if s.full == nil {
s.full = NewFullMatcherSet()
}
s.full.AddFullMatcher(matcher)
case DomainMatcher:
if s.domain == nil {
s.domain = NewDomainMatcherSet()
}
s.domain.AddDomainMatcher(matcher)
case SubstrMatcher:
if s.substr == nil {
s.substr = new(SubstrMatcherSet)
}
s.substr.AddSubstrMatcher(matcher)
default:
if s.regex == nil {
s.regex = new(SimpleMatcherSet)
}
s.regex.AddMatcher(matcher)
}
}
// MatchAny implements AnyMatcher.MatchAny.
func (s *LinearAnyMatcher) MatchAny(input string) bool {
if s.full != nil && s.full.MatchAny(input) {
return true
}
if s.domain != nil && s.domain.MatchAny(input) {
return true
}
if s.substr != nil && s.substr.MatchAny(input) {
return true
}
return s.regex != nil && s.regex.MatchAny(input)
}
+62 -4
View File
@@ -100,10 +100,6 @@ func (t Type) New(pattern string) (Matcher, error) {
case Substr:
return SubstrMatcher(pattern), nil
case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern)
@@ -288,3 +284,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
return result
}
}
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
type MatcherSetForAll interface {
AddMatcher(matcher Matcher)
}
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
type MatcherSetForFull interface {
AddFullMatcher(matcher FullMatcher)
}
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
type MatcherSetForDomain interface {
AddDomainMatcher(matcher DomainMatcher)
}
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
type MatcherSetForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher)
}
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
type MatcherSetForRegex interface {
AddRegexMatcher(matcher *RegexMatcher)
}
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
// It returns error if the MatcherSet does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherSet.
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
if s, ok := s.(IndexMatcher); ok {
s.Add(matcher)
return nil
}
if s, ok := s.(MatcherSetForAll); ok {
s.AddMatcher(matcher)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if s, ok := s.(MatcherSetForFull); ok {
s.AddFullMatcher(matcher)
return nil
}
case DomainMatcher:
if s, ok := s.(MatcherSetForDomain); ok {
s.AddDomainMatcher(matcher)
return nil
}
case SubstrMatcher:
if s, ok := s.(MatcherSetForSubstr); ok {
s.AddSubstrMatcher(matcher)
return nil
}
case *RegexMatcher:
if s, ok := s.(MatcherSetForRegex); ok {
s.AddRegexMatcher(matcher)
return nil
}
}
return errors.New("cannot add matcher to matcher set")
}
@@ -0,0 +1,79 @@
package strmatcher
type trieNode2 struct {
matched bool
children map[string]*trieNode2
}
// DomainMatcherSet is an implementation of MatcherSet.
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
type DomainMatcherSet struct {
root *trieNode2
}
func NewDomainMatcherSet() *DomainMatcherSet {
return &DomainMatcherSet{
root: new(trieNode2),
}
}
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
node := s.root
pattern := matcher.Pattern()
for i := len(pattern); i > 0; {
var part string
for j := i - 1; ; j-- {
if pattern[j] == '.' {
part = pattern[j+1 : i]
i = j
break
}
if j == 0 {
part = pattern[j:i]
i = j
break
}
}
if node.children == nil {
node.children = make(map[string]*trieNode2)
}
next := node.children[part]
if next == nil {
next = new(trieNode2)
node.children[part] = next
}
node = next
}
node.matched = true
}
// MatchAny implements MatcherSet.MatchAny.
func (s *DomainMatcherSet) MatchAny(input string) bool {
node := s.root
for i := len(input); i > 0; {
for j := i - 1; ; j-- {
if input[j] == '.' {
node = node.children[input[j+1:i]]
i = j
break
}
if j == 0 {
node = node.children[input[j:i]]
i = j
break
}
}
if node == nil {
return false
}
if node.matched {
return true
}
if node.children == nil {
return false
}
}
return false
}
@@ -0,0 +1,95 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestDomainMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "a.b.com",
},
{
Pattern: "c.a.b.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "x.example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "a.b.com",
Result: true,
},
{
Domain: "c.a.b.com",
Result: true,
},
{
Domain: "c.a..b.com",
Result: false,
},
{
Domain: ".com",
Result: false,
},
{
Domain: "com",
Result: false,
},
{
Domain: "",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewDomainMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyDomainMatcherSet(t *testing.T) {
s := NewDomainMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -0,0 +1,24 @@
package strmatcher
// FullMatcherSet is an implementation of MatcherSet.
// It uses a hash table to facilitate exact match lookup.
type FullMatcherSet struct {
matchers map[string]struct{}
}
func NewFullMatcherSet() *FullMatcherSet {
return &FullMatcherSet{
matchers: make(map[string]struct{}),
}
}
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
s.matchers[matcher.Pattern()] = struct{}{}
}
// MatchAny implements MatcherSet.Any.
func (s *FullMatcherSet) MatchAny(input string) bool {
_, found := s.matchers[input]
return found
}
@@ -0,0 +1,65 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestFullMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewFullMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyFullMatcherSet(t *testing.T) {
s := NewFullMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -0,0 +1,22 @@
package strmatcher
// SimpleMatcherSet is an implementation of MatcherSet.
// It simply stores all matchers in an array and sequentially matches them.
type SimpleMatcherSet struct {
matchers []Matcher
}
// AddMatcher implements MatcherSetForAll.AddMatcher.
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
s.matchers = append(s.matchers, matcher)
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SimpleMatcherSet) MatchAny(input string) bool {
for _, m := range s.matchers {
if m.Match(input) {
return true
}
}
return false
}
@@ -0,0 +1,69 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSimpleMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "example.com",
mType: Domain,
},
{
pattern: "example.com",
mType: Full,
},
{
pattern: "example.com",
mType: Regex,
},
}
cases := []struct {
input string
output bool
}{
{
input: "www.example.com",
output: true,
},
{
input: "example.com",
output: true,
},
{
input: "www.e3ample.com",
output: false,
},
{
input: "xample.com",
output: false,
},
{
input: "xexample.com",
output: true,
},
{
input: "examplexcom",
output: true,
},
}
matcherSet := &SimpleMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
@@ -0,0 +1,24 @@
package strmatcher
import "strings"
// SubstrMatcherSet is implementation of MatcherSet,
// It is simply implmeneted to comply with the priority specification of Substr matchers.
type SubstrMatcherSet struct {
patterns []string
}
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
s.patterns = append(s.patterns, matcher.Pattern())
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SubstrMatcherSet) MatchAny(input string) bool {
for _, pattern := range s.patterns {
if strings.Contains(input, pattern) {
return true
}
}
return false
}
@@ -0,0 +1,77 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSubstrMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "apis",
mType: Substr,
},
{
pattern: "google",
mType: Substr,
},
{
pattern: "apis",
mType: Substr,
},
}
cases := []struct {
input string
output bool
}{
{
input: "google.com",
output: true,
},
{
input: "apis.com",
output: true,
},
{
input: "googleapis.com",
output: true,
},
{
input: "fonts.googleapis.com",
output: true,
},
{
input: "apis.googleapis.com",
output: true,
},
{
input: "baidu.com",
output: false,
},
{
input: "goog",
output: false,
},
{
input: "api",
output: false,
},
}
matcherSet := &SubstrMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
+19 -1
View File
@@ -15,7 +15,7 @@ const (
)
// Matcher is the interface to determine a string matches a pattern.
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
type Matcher interface {
// Type returns the matcher's type.
Type() Type
@@ -101,3 +101,21 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherSet interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
type AnyMatcher interface {
// Add adds a new Matcher to AnyMatcher.
Add(matcher Matcher)
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
+40 -2
View File
@@ -1,6 +1,7 @@
package filesystem
import (
"errors"
"io"
"os"
"path/filepath"
@@ -26,11 +27,48 @@ func ReadFile(path string) ([]byte, error) {
}
func ReadAsset(file string) ([]byte, error) {
return ReadFile(platform.GetAssetLocation(file))
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
}
func OpenAsset(file string) (io.ReadCloser, error) {
return NewFileReader(platform.GetAssetLocation(file))
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file)
return path, err
}
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
if !filepath.IsLocal(file) || file == "." {
return "", nil, errors.New("asset path must stay in asset directory")
}
local, err := filepath.Localize(file)
if err != nil {
return "", nil, err
}
path := platform.GetAssetLocation(local)
info, err := os.Stat(path)
if err != nil {
return "", nil, err
}
if !info.Mode().IsRegular() {
return "", nil, errors.New("asset is not a regular file")
}
return path, info, nil
}
func ReadCert(file string) ([]byte, error) {
+32
View File
@@ -0,0 +1,32 @@
package filesystem_test
import (
"path/filepath"
"testing"
. "github.com/xtls/xray-core/common/platform/filesystem"
)
func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{
"",
".",
"..",
"../geoip.dat",
"nested/..",
"nested/../geoip.dat",
"nested//geoip.dat",
"/geoip.dat",
"/tmp/geoip.dat",
`C:\geoip.dat`,
`C:geoip.dat`,
`\\server\share\geoip.dat`,
`nested\geoip.dat`,
`nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"),
} {
if _, err := StatAsset(file); err == nil {
t.Fatalf("expected error for %q", file)
}
}
}
+1
View File
@@ -17,6 +17,7 @@ const (
UseFreedomSplice = "xray.buf.splice"
UseVmessPadding = "xray.vmess.padding"
UseCone = "xray.cone.disabled"
UseStrictJSON = "xray.json.strict"
BufferSize = "xray.ray.buffer.size"
BrowserDialerAddress = "xray.browser.dialer"
+43
View File
@@ -0,0 +1,43 @@
package task
import (
"runtime"
"golang.org/x/sync/errgroup"
)
// ParallelForN runs fn(0..n-1) in parallel across runtime.GOMAXPROCS(0) worker
// goroutines. Indices are partitioned into contiguous chunks so the number of
// spawned goroutines stays bounded regardless of n.
//
// fn must be safe to call concurrently from different goroutines (each call
// receives its own unique index). Output collected by writing to indexed slots
// in a pre-allocated slice is a common safe pattern.
//
// Returns the first non-nil error reported by fn; other workers may still be
// finishing briefly afterwards.
func ParallelForN(n int, fn func(i int) error) error {
if n <= 0 {
return nil
}
workers := max(runtime.GOMAXPROCS(0), 1)
workers = min(workers, n)
chunk := (n + workers - 1) / workers
var eg errgroup.Group
for w := range workers {
start := w * chunk
end := min(start+chunk, n)
if start >= end {
break
}
eg.Go(func() error {
for i := start; i < end; i++ {
if err := fn(i); err != nil {
return err
}
}
return nil
})
}
return eg.Wait()
}
+50
View File
@@ -0,0 +1,50 @@
package task_test
import (
"errors"
"sync/atomic"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/task"
)
func TestParallelForN_Empty(t *testing.T) {
called := false
err := ParallelForN(0, func(i int) error {
called = true
return nil
})
common.Must(err)
if called {
t.Fatal("fn should not be called when n=0")
}
}
func TestParallelForN_AllIndicesCovered(t *testing.T) {
const N = 10000
var seen [N]int32
err := ParallelForN(N, func(i int) error {
atomic.AddInt32(&seen[i], 1)
return nil
})
common.Must(err)
for i := 0; i < N; i++ {
if seen[i] != 1 {
t.Fatalf("index %d called %d times, expected 1", i, seen[i])
}
}
}
func TestParallelForN_Error(t *testing.T) {
boom := errors.New("boom")
err := ParallelForN(1000, func(i int) error {
if i == 42 {
return boom
}
return nil
})
if err != boom {
t.Fatalf("expected %v, got %v", boom, err)
}
}
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 4
Version_z byte = 17
Version_y byte = 5
Version_z byte = 3
)
var (
+3 -3
View File
@@ -53,7 +53,7 @@ func TestXrayDial(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -105,7 +105,7 @@ func TestXrayDialUDPConn(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -174,7 +174,7 @@ func TestXrayDialUDP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+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
},
}
+3 -2
View File
@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
go 1.26
require (
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.3
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
@@ -14,6 +14,7 @@ require (
github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.1
@@ -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.6.1
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
+6 -4
View File
@@ -1,7 +1,7 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
@@ -53,6 +53,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
@@ -131,8 +133,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.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
+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"`, `"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)
}
}
+72 -15
View File
@@ -1,6 +1,7 @@
package conf
import (
"context"
"encoding/base64"
"encoding/hex"
"net"
@@ -8,7 +9,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
v2net "github.com/xtls/xray-core/common/net"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/transport/internet"
@@ -16,15 +17,16 @@ 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"`
IPsBlocked *StringList `json:"ipsBlocked"`
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"`
FinalRules []*FreedomFinalRuleConfig `json:"finalRules"`
}
type Fragment struct {
@@ -41,8 +43,21 @@ type Noise struct {
ApplyTo string `json:"applyTo"`
}
type FreedomFinalRuleConfig struct {
Action string `json:"action"`
Network *NetworkList `json:"network"`
Port *PortList `json:"port"`
IP *StringList `json:"ip"`
BlockDelay *Int32Range `json:"blockDelay"`
}
// Build implements Buildable
func (c *FreedomConfig) Build() (proto.Message, error) {
if c.IPsBlocked != nil {
// todo: remove legacy
errors.LogWarning(context.Background(), `The feature "ipsBlocked" has been removed and migrated to "finalRules". Please update your config(s) according to release note and documentation.`)
}
config := new(freedom.Config)
targetStrategy := c.TargetStrategy
if targetStrategy == "" {
@@ -142,12 +157,13 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
}
config.UserLevel = c.UserLevel
if len(c.Redirect) > 0 {
host, portStr, err := net.SplitHostPort(c.Redirect)
if err != nil {
return nil, errors.New("invalid redirect address: ", c.Redirect, ": ", err).Base(err)
}
port, err := v2net.PortFromString(portStr)
port, err := xnet.PortFromString(portStr)
if err != nil {
return nil, errors.New("invalid redirect port: ", c.Redirect, ": ", err).Base(err)
}
@@ -158,19 +174,22 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
}
if len(host) > 0 {
config.DestinationOverride.Server.Address = v2net.NewIPOrDomain(v2net.ParseAddress(host))
config.DestinationOverride.Server.Address = xnet.NewIPOrDomain(xnet.ParseAddress(host))
}
}
if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 {
config.ProxyProtocol = c.ProxyProtocol
}
if c.IPsBlocked != nil {
rules, err := geodata.ParseIPRules(*c.IPsBlocked)
for _, r := range c.FinalRules {
rule, err := r.Build()
if err != nil {
return nil, err
}
config.IpsBlocked = &freedom.IPRules{Rules: rules}
config.FinalRules = append(config.FinalRules, rule)
}
return config, nil
}
@@ -229,3 +248,41 @@ func ParseNoise(noise *Noise) (*freedom.Noise, error) {
}
return NConfig, nil
}
func (c *FreedomFinalRuleConfig) Build() (*freedom.FinalRuleConfig, error) {
rule := &freedom.FinalRuleConfig{}
switch strings.ToLower(c.Action) {
case "allow":
rule.Action = freedom.RuleAction_Allow
case "block":
rule.Action = freedom.RuleAction_Block
default:
return nil, errors.New("unknown action: ", c.Action)
}
if c.Network != nil {
rule.Networks = c.Network.Build()
}
if c.Port != nil {
rule.PortList = c.Port.Build()
}
if c.IP != nil {
rules, err := geodata.ParseIPRules(*c.IP)
if err != nil {
return nil, err
}
rule.Ip = rules
}
if c.BlockDelay != nil {
rule.BlockDelay = &freedom.Range{
Min: uint64(c.BlockDelay.From),
Max: uint64(c.BlockDelay.To),
}
}
return rule, nil
}
+60
View File
@@ -3,6 +3,7 @@ package conf_test
import (
"testing"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
. "github.com/xtls/xray-core/infra/conf"
@@ -38,5 +39,64 @@ func TestFreedomConfig(t *testing.T) {
UserLevel: 1,
},
},
{
Input: `{
"finalRules": [{
"action": "block",
"network": "tcp,udp",
"port": "53,443",
"ip": ["10.0.0.0/8", "2001:db8::/32"],
"blockDelay": "30-60"
}, {
"action": "allow",
"network": ["udp"]
}]
}`,
Parser: loadJSON(creator),
Output: &freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{
{
Action: freedom.RuleAction_Block,
Networks: []net.Network{net.Network_TCP, net.Network_UDP},
PortList: &net.PortList{
Range: []*net.PortRange{
{From: 53, To: 53},
{From: 443, To: 443},
},
},
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{
Ip: []byte{10, 0, 0, 0},
Prefix: 8,
},
},
},
},
{
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{
Ip: net.ParseAddress("2001:db8::").IP(),
Prefix: 32,
},
},
},
},
},
BlockDelay: &freedom.Range{
Min: 30,
Max: 60,
},
},
{
Action: freedom.RuleAction_Allow,
Networks: []net.Network{net.Network_UDP},
},
},
},
},
})
}
+71
View File
@@ -0,0 +1,71 @@
package conf
import (
"net/url"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
type GeodataAssetConfig struct {
URL string `json:"url"`
File string `json:"file"`
}
func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
if err := validateHTTPS(c.URL); err != nil {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
}
if _, err := filesystem.StatAsset(c.File); err != nil {
return nil, errors.New("invalid geodata asset file: ", c.File).Base(err)
}
return &geodata.Asset{
Url: c.URL,
File: c.File,
}, nil
}
func validateHTTPS(s string) error {
u, err := url.ParseRequestURI(s)
if err != nil {
return err
}
if u.Scheme != "https" || u.Host == "" {
return errors.New("scheme must be https")
}
return nil
}
type GeodataConfig struct {
Cron *string `json:"cron"`
Outbound string `json:"outbound"`
Assets []*GeodataAssetConfig `json:"assets"`
}
func (c *GeodataConfig) Build() (proto.Message, error) {
config := &geodata.Config{}
if c.Cron != nil {
if _, err := cron.ParseStandard(*c.Cron); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
config.Cron = *c.Cron
}
config.Outbound = c.Outbound
assets := make([]*geodata.Asset, 0, len(c.Assets))
for _, asset := range c.Assets {
built, err := asset.Build()
if err != nil {
return nil, err
}
assets = append(assets, built)
}
config.Assets = assets
return config, nil
}
+75
View File
@@ -0,0 +1,75 @@
package conf_test
import (
"path/filepath"
"testing"
"github.com/xtls/xray-core/app/geodata"
. "github.com/xtls/xray-core/infra/conf"
)
func TestGeodataConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
creator := func() Buildable {
return new(GeodataConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"cron": "0 4 * * *",
"outbound": "proxy",
"assets": [
{"url": "https://example.com/geoip.dat", "file": "geoip.dat"},
{"url": "https://example.com/geosite.dat", "file": "geosite.dat"}
]
}`,
Parser: loadJSON(creator),
Output: &geodata.Config{
Cron: "0 4 * * *",
Outbound: "proxy",
Assets: []*geodata.Asset{
{Url: "https://example.com/geoip.dat", File: "geoip.dat"},
{Url: "https://example.com/geosite.dat", File: "geosite.dat"},
},
},
},
})
}
func TestGeodataAssetConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "geoip.dat",
}).Build(); err != nil {
t.Fatal(err)
}
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "missing.dat",
}).Build(); err == nil {
t.Fatal("expected error")
}
}
func TestGeodataAssetConfigInvalidURL(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, rawURL := range []string{
"",
"http://example.com/geoip.dat",
"ftp://example.com/geoip.dat",
"https:///geoip.dat",
} {
if _, err := (&GeodataAssetConfig{
URL: rawURL,
File: "geoip.dat",
}).Build(); err == nil {
t.Fatalf("expected error for %q", rawURL)
}
}
}
+13 -6
View File
@@ -4,6 +4,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/hysteria"
"github.com/xtls/xray-core/proxy/hysteria/account"
"google.golang.org/protobuf/proto"
@@ -44,16 +45,22 @@ type HysteriaServerConfig struct {
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
config := new(hysteria.ServerConfig)
if c.Users != nil {
for _, user := range c.Users {
account := &account.Account{
if len(c.Users) > 0 {
config.Users = make([]*protocol.User, len(c.Users))
processUser := func(idx int) error {
user := c.Users[idx]
acc := &account.Account{
Auth: user.Auth,
}
config.Users = append(config.Users, &protocol.User{
config.Users[idx] = &protocol.User{
Email: user.Email,
Level: user.Level,
Account: serial.ToTypedMessage(account),
})
Account: serial.ToTypedMessage(acc),
}
return nil
}
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
return nil, err
}
}
+15 -1
View File
@@ -5,12 +5,22 @@ import (
"io"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
creflect "github.com/xtls/xray-core/common/reflect"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/main/confloader"
)
// UseStrictJSON, when true, makes JSON config decoders skip the custom
// comment-stripping reader and parse input as strict RFC 8259 JSON.
//
// Enabled by setting the env variable xray.json.strict=true (or its normalized
// form XRAY_JSON_STRICT=true). Default false preserves backward-compatible
// behavior for human-edited configs that may contain comments or other
// JSON5/JSONC syntax.
var UseStrictJSON = platform.NewEnvFlag(platform.UseStrictJSON).GetValue(func() string { return "" }) == "true"
func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) {
c, err := mergeConfigs(files)
if err != nil {
@@ -31,7 +41,11 @@ func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
if err != nil {
return nil, errors.New("failed to read config: ", file).Base(err)
}
c, err := ReaderDecoderByFormat[file.Format](r)
decoder := ReaderDecoderByFormat[file.Format]
if file.Format == "json" && UseStrictJSON {
decoder = DecodeJSONConfigStrict
}
c, err := decoder(r)
if err != nil {
return nil, errors.New("failed to decode config: ", file).Base(err)
}
+21
View File
@@ -42,6 +42,9 @@ func findOffset(b []byte, o int) *offset {
// DecodeJSONConfig reads from reader and decode the config into *conf.Config
// syntax error could be detected.
//
// Permissive: accepts JSON with Java/Python-style comments via json_reader.Reader.
// Used for local files and stdin where the config is human-edited.
func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
jsonConfig := &conf.Config{}
@@ -69,6 +72,24 @@ func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
return jsonConfig, nil
}
// DecodeJSONConfigStrict reads standard RFC 8259 JSON without comment-stripping.
// Used for remote sources (http/https/http+unix) where the payload is produced by
// automated systems and cannot contain JSON5/JSONC extensions. Avoids the
// byte-by-byte comment stripper and TeeReader, which are significant overhead on
// large configs.
func DecodeJSONConfigStrict(reader io.Reader) (*conf.Config, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, errors.New("failed to read config file").Base(err)
}
jsonConfig := &conf.Config{}
if err := json.Unmarshal(data, jsonConfig); err != nil {
return nil, errors.New("failed to parse remote JSON config").Base(err)
}
return jsonConfig, nil
}
func LoadJSONConfig(reader io.Reader) (*core.Config, error) {
jsonConfig, err := DecodeJSONConfig(reader)
if err != nil {
+34 -19
View File
@@ -8,6 +8,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
"google.golang.org/protobuf/proto"
@@ -59,23 +60,31 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
config.Network = v.NetworkList.Build()
if v.Users != nil {
for _, user := range v.Users {
account := &shadowsocks.Account{
Password: user.Password,
CipherType: cipherFromString(user.Cipher),
if len(v.Users) > 0 {
config.Users = make([]*protocol.User, len(v.Users))
processUser := func(idx int) error {
user := v.Users[idx]
account := &shadowsocks.Account{
Password: user.Password,
CipherType: cipherFromString(user.Cipher),
}
if account.Password == "" {
return errors.New("Shadowsocks password is not specified.")
}
if account.CipherType < shadowsocks.CipherType_AES_128_GCM ||
account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 {
return errors.New("unsupported cipher method: ", user.Cipher)
}
config.Users[idx] = &protocol.User{
Email: user.Email,
Level: uint32(user.Level),
Account: serial.ToTypedMessage(account),
}
return nil
}
if account.Password == "" {
return nil, errors.New("Shadowsocks password is not specified.")
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
return nil, err
}
if account.CipherType < shadowsocks.CipherType_AES_128_GCM ||
account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 {
return nil, errors.New("unsupported cipher method: ", user.Cipher)
}
config.Users = append(config.Users, &protocol.User{
Email: user.Email,
Level: uint32(user.Level),
Account: serial.ToTypedMessage(account),
})
}
} else {
account := &shadowsocks.Account{
@@ -121,18 +130,24 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
config.Key = v.Password
config.Network = v.NetworkList.Build()
for _, user := range v.Users {
config.Users = make([]*protocol.User, len(v.Users))
processUser := func(idx int) error {
user := v.Users[idx]
if user.Cipher != "" {
return nil, errors.New("shadowsocks 2022 (multi-user): users must have empty method")
return errors.New("shadowsocks 2022 (multi-user): users must have empty method")
}
account := &shadowsocks_2022.Account{
Key: user.Password,
}
config.Users = append(config.Users, &protocol.User{
config.Users[idx] = &protocol.User{
Email: user.Email,
Level: uint32(user.Level),
Account: serial.ToTypedMessage(account),
})
}
return nil
}
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
return nil, err
}
return config, nil
}
+6 -17
View File
@@ -497,8 +497,8 @@ func (b Bandwidth) Bps() (uint64, error) {
}
type UdpHop struct {
PortList json.RawMessage `json:"ports"`
Interval *Int32Range `json:"interval"`
PortList PortList `json:"ports"`
Interval Int32Range `json:"interval"`
}
type Masquerade struct {
@@ -2142,18 +2142,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
var hop *PortList
if err := json.Unmarshal(c.FinalMask.QuicParams.UdpHop.PortList, &hop); err != nil {
hop = &PortList{}
}
var inertvalMin, inertvalMax int64
if c.FinalMask.QuicParams.UdpHop.Interval != nil {
inertvalMin = int64(c.FinalMask.QuicParams.UdpHop.Interval.From)
inertvalMax = int64(c.FinalMask.QuicParams.UdpHop.Interval.To)
}
if (inertvalMin != 0 && inertvalMin < 5) || (inertvalMax != 0 && inertvalMax < 5) {
if (c.FinalMask.QuicParams.UdpHop.Interval.From != 0 && c.FinalMask.QuicParams.UdpHop.Interval.From < 5) || (c.FinalMask.QuicParams.UdpHop.Interval.To != 0 && c.FinalMask.QuicParams.UdpHop.Interval.To < 5) {
return nil, errors.New("Interval must be at least 5")
}
@@ -2190,9 +2179,9 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
BrutalUp: up,
BrutalDown: down,
UdpHop: &internet.UdpHop{
Ports: hop.Build().Ports(),
IntervalMin: inertvalMin,
IntervalMax: inertvalMax,
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
IntervalMax: int64(c.FinalMask.QuicParams.UdpHop.Interval.To),
},
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
+8 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/trojan"
"google.golang.org/protobuf/proto"
)
@@ -123,9 +124,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
Users: make([]*protocol.User, len(c.Clients)),
}
for idx, rawUser := range c.Clients {
processClient := func(idx int) error {
rawUser := c.Clients[idx]
if rawUser.Flow != "" {
return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
return errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
}
config.Users[idx] = &protocol.User{
@@ -135,6 +137,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
Password: rawUser.Password,
}),
}
return nil
}
if err := task.ParallelForN(len(c.Clients), processClient); err != nil {
return nil, err
}
for _, fb := range c.Fallbacks {
+15 -8
View File
@@ -13,6 +13,7 @@ import (
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vless/inbound"
@@ -46,19 +47,20 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
default:
return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`)
}
for idx, rawUser := range c.Clients {
processClient := func(idx int) error {
rawUser := c.Clients[idx]
user := new(protocol.User)
if err := json.Unmarshal(rawUser, user); err != nil {
return nil, errors.New(`VLESS clients: invalid user`).Base(err)
return errors.New(`VLESS clients: invalid user`).Base(err)
}
account := new(vless.Account)
if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New(`VLESS clients: invalid user`).Base(err)
return errors.New(`VLESS clients: invalid user`).Base(err)
}
u, err := uuid.ParseString(account.Id)
if err != nil {
return nil, err
return err
}
account.Id = u.String()
@@ -67,7 +69,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
account.Flow = c.Flow
case vless.XRV:
default:
return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`)
return errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`)
}
if len(account.Testseed) < 4 {
@@ -75,20 +77,25 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
}
if account.Encryption != "" {
return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
return errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
}
if account.Reverse != nil {
if account.Reverse.Tag == "" {
return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`)
return errors.New(`VLESS clients: "tag" can't be empty for "reverse"`)
}
if account.Reverse.Sniffing != nil { // may not be reached: error json unmarshal
return nil, errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`)
return errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`)
}
}
user.Account = serial.ToTypedMessage(account)
config.Clients[idx] = user
return nil
}
if err := task.ParallelForN(len(c.Clients), processClient); err != nil {
return nil, err
}
config.Decryption = c.Decryption
+10 -4
View File
@@ -7,6 +7,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/proxy/vmess"
"github.com/xtls/xray-core/proxy/vmess/inbound"
@@ -73,24 +74,29 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) {
}
config.User = make([]*protocol.User, len(c.Users))
for idx, rawData := range c.Users {
processUser := func(idx int) error {
rawData := c.Users[idx]
user := new(protocol.User)
if err := json.Unmarshal(rawData, user); err != nil {
return nil, errors.New("invalid VMess user").Base(err)
return errors.New("invalid VMess user").Base(err)
}
account := new(VMessAccount)
if err := json.Unmarshal(rawData, account); err != nil {
return nil, errors.New("invalid VMess user").Base(err)
return errors.New("invalid VMess user").Base(err)
}
u, err := uuid.ParseString(account.ID)
if err != nil {
return nil, err
return err
}
account.ID = u.String()
user.Account = serial.ToTypedMessage(account.Build())
config.User[idx] = user
return nil
}
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
return nil, err
}
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 {
+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"
+7
View File
@@ -41,6 +41,13 @@ func init() {
}
return cf.Build()
case io.Reader:
if serial.UseStrictJSON {
cfg, err := serial.DecodeJSONConfigStrict(v)
if err != nil {
return nil, err
}
return cfg.Build()
}
return serial.LoadJSONConfig(v)
default:
return nil, errors.New("unknown type")
+15 -1
View File
@@ -6,7 +6,11 @@ import (
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
)
@@ -38,7 +42,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
// Sleep a little here to make sure the response is sent to client.
time.Sleep(time.Second)
}
common.Interrupt(link.Writer)
defer common.Interrupt(link.Writer)
defer common.Interrupt(link.Reader)
// wait to drain all the possible incoming UDP data
if ob.Target.Network == net.Network_UDP {
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, func() {
cancel()
}, time.Duration(30+dice.Roll(61))*time.Second)
go buf.Copy(link.Reader, buf.Discard, buf.UpdateActivity(timer))
<-ctx.Done()
}
return nil
}
+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)
}
}
}
+1 -1
View File
@@ -95,7 +95,7 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st
}
}
}
if dest.Port == 0 {
if dest.Port == 0 && port != "" {
dest.Port = net.Port(common.Must2(strconv.Atoi(port)))
}
if d.portMap != nil && d.portMap[port] != "" {
-1
View File
@@ -1 +0,0 @@
package freedom
+200 -49
View File
@@ -8,6 +8,7 @@ package freedom
import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net"
protocol "github.com/xtls/xray-core/common/protocol"
internet "github.com/xtls/xray-core/transport/internet"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
@@ -24,6 +25,52 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type RuleAction int32
const (
RuleAction_Allow RuleAction = 0
RuleAction_Block RuleAction = 1
)
// Enum value maps for RuleAction.
var (
RuleAction_name = map[int32]string{
0: "Allow",
1: "Block",
}
RuleAction_value = map[string]int32{
"Allow": 0,
"Block": 1,
}
)
func (x RuleAction) Enum() *RuleAction {
p := new(RuleAction)
*p = x
return p
}
func (x RuleAction) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (RuleAction) Descriptor() protoreflect.EnumDescriptor {
return file_proxy_freedom_config_proto_enumTypes[0].Descriptor()
}
func (RuleAction) Type() protoreflect.EnumType {
return &file_proxy_freedom_config_proto_enumTypes[0]
}
func (x RuleAction) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use RuleAction.Descriptor instead.
func (RuleAction) EnumDescriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{0}
}
type DestinationOverride struct {
state protoimpl.MessageState `protogen:"open.v1"`
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
@@ -252,27 +299,28 @@ func (x *Noise) GetApplyTo() string {
return ""
}
type IPRules struct {
type Range struct {
state protoimpl.MessageState `protogen:"open.v1"`
Rules []*geodata.IPRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"`
Min uint64 `protobuf:"varint,1,opt,name=min,proto3" json:"min,omitempty"`
Max uint64 `protobuf:"varint,2,opt,name=max,proto3" json:"max,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IPRules) Reset() {
*x = IPRules{}
func (x *Range) Reset() {
*x = Range{}
mi := &file_proxy_freedom_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IPRules) String() string {
func (x *Range) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IPRules) ProtoMessage() {}
func (*Range) ProtoMessage() {}
func (x *IPRules) ProtoReflect() protoreflect.Message {
func (x *Range) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -284,14 +332,97 @@ func (x *IPRules) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use IPRules.ProtoReflect.Descriptor instead.
func (*IPRules) Descriptor() ([]byte, []int) {
// Deprecated: Use Range.ProtoReflect.Descriptor instead.
func (*Range) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3}
}
func (x *IPRules) GetRules() []*geodata.IPRule {
func (x *Range) GetMin() uint64 {
if x != nil {
return x.Rules
return x.Min
}
return 0
}
func (x *Range) GetMax() uint64 {
if x != nil {
return x.Max
}
return 0
}
type FinalRuleConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.freedom.RuleAction" json:"action,omitempty"`
Networks []net.Network `protobuf:"varint,2,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"`
PortList *net.PortList `protobuf:"bytes,3,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"`
Ip []*geodata.IPRule `protobuf:"bytes,4,rep,name=ip,proto3" json:"ip,omitempty"`
BlockDelay *Range `protobuf:"bytes,5,opt,name=block_delay,json=blockDelay,proto3" json:"block_delay,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FinalRuleConfig) Reset() {
*x = FinalRuleConfig{}
mi := &file_proxy_freedom_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FinalRuleConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FinalRuleConfig) ProtoMessage() {}
func (x *FinalRuleConfig) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FinalRuleConfig.ProtoReflect.Descriptor instead.
func (*FinalRuleConfig) Descriptor() ([]byte, []int) {
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{4}
}
func (x *FinalRuleConfig) GetAction() RuleAction {
if x != nil {
return x.Action
}
return RuleAction_Allow
}
func (x *FinalRuleConfig) GetNetworks() []net.Network {
if x != nil {
return x.Networks
}
return nil
}
func (x *FinalRuleConfig) GetPortList() *net.PortList {
if x != nil {
return x.PortList
}
return nil
}
func (x *FinalRuleConfig) GetIp() []*geodata.IPRule {
if x != nil {
return x.Ip
}
return nil
}
func (x *FinalRuleConfig) GetBlockDelay() *Range {
if x != nil {
return x.BlockDelay
}
return nil
}
@@ -304,14 +435,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"`
FinalRules []*FinalRuleConfig `protobuf:"bytes,8,rep,name=final_rules,json=finalRules,proto3" json:"final_rules,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_proxy_freedom_config_proto_msgTypes[4]
mi := &file_proxy_freedom_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -323,7 +454,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_freedom_config_proto_msgTypes[4]
mi := &file_proxy_freedom_config_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -336,7 +467,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{4}
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{5}
}
func (x *Config) GetDomainStrategy() internet.DomainStrategy {
@@ -381,9 +512,9 @@ func (x *Config) GetNoises() []*Noise {
return nil
}
func (x *Config) GetIpsBlocked() *IPRules {
func (x *Config) GetFinalRules() []*FinalRuleConfig {
if x != nil {
return x.IpsBlocked
return x.FinalRules
}
return nil
}
@@ -392,7 +523,7 @@ 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\x1a\x1bcommon/geodata/geodat.proto\"S\n" +
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"S\n" +
"\x13DestinationOverride\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
"\bFragment\x12!\n" +
@@ -415,9 +546,17 @@ 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\"<\n" +
"\aIPRules\x121\n" +
"\x05rules\x18\x01 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x05rules\"\xbc\x03\n" +
"\bapply_to\x18\x06 \x01(\tR\aapplyTo\"+\n" +
"\x05Range\x12\x10\n" +
"\x03min\x18\x01 \x01(\x04R\x03min\x12\x10\n" +
"\x03max\x18\x02 \x01(\x04R\x03max\"\xa0\x02\n" +
"\x0fFinalRuleConfig\x126\n" +
"\x06action\x18\x01 \x01(\x0e2\x1e.xray.proxy.freedom.RuleActionR\x06action\x124\n" +
"\bnetworks\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x126\n" +
"\tport_list\x18\x03 \x01(\v2\x19.xray.common.net.PortListR\bportList\x12+\n" +
"\x02ip\x18\x04 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\x12:\n" +
"\vblock_delay\x18\x05 \x01(\v2\x19.xray.proxy.freedom.RangeR\n" +
"blockDelay\"\xaf\x03\n" +
"\x06Config\x12P\n" +
"\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" +
@@ -425,10 +564,13 @@ 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\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" +
"\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noises\x12D\n" +
"\vfinal_rules\x18\b \x03(\v2#.xray.proxy.freedom.FinalRuleConfigR\n" +
"finalRules*\"\n" +
"\n" +
"RuleAction\x12\t\n" +
"\x05Allow\x10\x00\x12\t\n" +
"\x05Block\x10\x01BX\n" +
"\x16com.xray.proxy.freedomP\x01Z'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3"
var (
@@ -443,30 +585,39 @@ 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, 5)
var file_proxy_freedom_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_proxy_freedom_config_proto_goTypes = []any{
(*DestinationOverride)(nil), // 0: xray.proxy.freedom.DestinationOverride
(*Fragment)(nil), // 1: xray.proxy.freedom.Fragment
(*Noise)(nil), // 2: xray.proxy.freedom.Noise
(*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
(RuleAction)(0), // 0: xray.proxy.freedom.RuleAction
(*DestinationOverride)(nil), // 1: xray.proxy.freedom.DestinationOverride
(*Fragment)(nil), // 2: xray.proxy.freedom.Fragment
(*Noise)(nil), // 3: xray.proxy.freedom.Noise
(*Range)(nil), // 4: xray.proxy.freedom.Range
(*FinalRuleConfig)(nil), // 5: xray.proxy.freedom.FinalRuleConfig
(*Config)(nil), // 6: xray.proxy.freedom.Config
(*protocol.ServerEndpoint)(nil), // 7: xray.common.protocol.ServerEndpoint
(net.Network)(0), // 8: xray.common.net.Network
(*net.PortList)(nil), // 9: xray.common.net.PortList
(*geodata.IPRule)(nil), // 10: xray.common.geodata.IPRule
(internet.DomainStrategy)(0), // 11: xray.transport.internet.DomainStrategy
}
var file_proxy_freedom_config_proto_depIdxs = []int32{
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
7, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
0, // 1: xray.proxy.freedom.FinalRuleConfig.action:type_name -> xray.proxy.freedom.RuleAction
8, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network
9, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList
10, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule
4, // 5: xray.proxy.freedom.FinalRuleConfig.block_delay:type_name -> xray.proxy.freedom.Range
11, // 6: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
1, // 7: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
2, // 8: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
3, // 9: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
5, // 10: xray.proxy.freedom.Config.final_rules:type_name -> xray.proxy.freedom.FinalRuleConfig
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_proxy_freedom_config_proto_init() }
@@ -474,19 +625,19 @@ 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: 5,
NumEnums: 1,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proxy_freedom_config_proto_goTypes,
DependencyIndexes: file_proxy_freedom_config_proto_depIdxs,
EnumInfos: file_proxy_freedom_config_proto_enumTypes,
MessageInfos: file_proxy_freedom_config_proto_msgTypes,
}.Build()
File_proxy_freedom_config_proto = out.File
+20 -3
View File
@@ -8,6 +8,8 @@ option java_multiple_files = true;
import "common/protocol/server_spec.proto";
import "transport/internet/config.proto";
import "common/net/port.proto";
import "common/net/network.proto";
import "common/geodata/geodat.proto";
message DestinationOverride {
@@ -24,6 +26,7 @@ message Fragment {
uint64 max_split_min = 7;
uint64 max_split_max = 8;
}
message Noise {
uint64 length_min = 1;
uint64 length_max = 2;
@@ -33,8 +36,22 @@ message Noise {
string apply_to = 6;
}
message IPRules {
repeated xray.common.geodata.IPRule rules = 1;
message Range {
uint64 min = 1;
uint64 max = 2;
}
enum RuleAction {
Allow = 0;
Block = 1;
}
message FinalRuleConfig {
RuleAction action = 1;
repeated xray.common.net.Network networks = 2;
xray.common.net.PortList port_list = 3;
repeated xray.common.geodata.IPRule ip = 4;
Range block_delay = 5;
}
message Config {
@@ -44,5 +61,5 @@ message Config {
Fragment fragment = 5;
uint32 proxy_protocol = 6;
repeated Noise noises = 7;
optional IPRules ips_blocked = 8;
repeated FinalRuleConfig final_rules = 8;
}
+229 -77
View File
@@ -31,32 +31,9 @@ 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))
}()
var allNetworks [8]bool
var defaultBlockPrivateRule *FinalRule
var defaultBlockAllRule *FinalRule
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
@@ -68,31 +45,184 @@ func init() {
}
return h, nil
}))
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useSplice = true
}
for i := range allNetworks {
allNetworks[i] = true
}
defaultBlockPrivateRule = &FinalRule{
action: RuleAction_Block,
network: allNetworks,
ip: common.Must2(geodata.IPReg.BuildIPMatcher(common.Must2(geodata.ParseIPRules([]string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
"ff00::/8",
})))),
}
defaultBlockAllRule = &FinalRule{
action: RuleAction_Block,
network: allNetworks,
}
}
type FinalRule struct {
action RuleAction
network [8]bool
port net.MemoryPortList
ip geodata.IPMatcher
blockDelay *Range
}
// Handler handles Freedom connections.
type Handler struct {
policyManager policy.Manager
config *Config
blockedIPMatcher geodata.IPMatcher
policyManager policy.Manager
config *Config
finalRules []*FinalRule
}
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
rule := &FinalRule{
action: config.GetAction(),
blockDelay: config.GetBlockDelay(),
}
if len(config.Networks) == 0 {
rule.network = allNetworks
} else {
for _, network := range config.Networks {
rule.network[int(network)] = true
}
}
if config.PortList != nil {
rule.port = net.PortListFromProto(config.PortList)
}
if len(config.Ip) > 0 {
matcher, err := geodata.IPReg.BuildIPMatcher(config.Ip)
if err != nil {
return nil, err
}
rule.ip = matcher
}
return rule, nil
}
func (r *FinalRule) matchNetwork(network net.Network) bool {
return r.network[int(network)]
}
func (r *FinalRule) matchPort(port net.Port) bool {
if len(r.port) == 0 {
return true
}
return r.port.Contains(port)
}
func (r *FinalRule) matchIP(addr net.Address) bool {
if r.ip == nil {
return true
}
return addr != nil && addr.Family().IsIP() && r.ip.Match(addr.IP())
}
func (r *FinalRule) Apply(network net.Network, address net.Address, port net.Port) bool {
if !r.matchNetwork(network) {
return false
}
if !r.matchPort(port) {
return false
}
return r.matchIP(address)
}
func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
if inbound == nil {
return nil
}
switch inbound.Name {
case "vless-reverse":
return defaultBlockAllRule
case "vless", "vmess", "trojan", "hysteria", "wireguard":
return defaultBlockPrivateRule
default:
if strings.HasPrefix(inbound.Name, "shadowsocks") {
return defaultBlockPrivateRule
}
}
return nil
}
func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool {
if !dialDest.Address.Family().IsDomain() {
return false
}
if len(h.finalRules) > 0 {
rule := h.finalRules[0]
if rule.action == RuleAction_Allow && rule.network[dialDest.Network] && len(rule.port) == 0 && rule.ip == nil {
return false
}
}
if defaultRule != nil || len(h.finalRules) > 0 {
return true
}
return false
}
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
for _, rule := range h.finalRules {
if rule.Apply(network, address, port) {
return rule
}
}
if defaultRule != nil && defaultRule.Apply(network, address, port) {
return defaultRule
}
return nil
}
func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction {
if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil {
return rule.action
}
return RuleAction_Allow
}
// Init initializes the Handler with necessary parameters.
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)
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
for _, rc := range config.FinalRules {
rule, err := buildFinalRule(rc)
if err != nil {
return errors.New("failed to build blocked ip matcher").Base(err)
return errors.New("failed to build final rule").Base(err)
}
h.blockedIPMatcher = m
h.finalRules = append(h.finalRules, rule)
}
return nil
}
@@ -102,6 +232,20 @@ func (h *Handler) policy() policy.Session {
return p
}
func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
min := uint64(30)
max := uint64(90)
if rule.blockDelay != nil {
min = rule.blockDelay.Min
max = rule.blockDelay.Max
}
abs := max - min
if max < min {
abs = min - max
}
return time.Duration(min+uint64(dice.Roll(int(abs+1)))) * time.Second
}
func isValidAddress(addr *net.IPOrDomain) bool {
if addr == nil {
return false
@@ -111,32 +255,6 @@ 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)
@@ -147,7 +265,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)
defaultRule := getDefaultFinalRule(inbound)
destination := ob.Target
origTargetAddr := ob.OriginalTarget.Address
@@ -173,6 +291,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
output := link.Writer
var conn stat.Connection
var blockedDest *net.Destination
var blockedRule *FinalRule
err := retry.ExponentialBackoff(5, 100).On(func() error {
dialDest := destination
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
@@ -194,6 +314,21 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
}
errors.LogInfo(ctx, "dialing to ", dialDest)
}
} else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, dialDest.Address.Domain())
if err != nil {
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
} else if len(addrs) > 0 {
if addr := net.IPAddress(addrs[dice.Roll(len(addrs))].IP); addr != nil {
dialDest.Address = addr
errors.LogInfo(ctx, "dialing to ", dialDest)
}
}
}
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &dialDest
blockedRule = rule
return nil
}
rawConn, err := dialer.Dial(ctx, dialDest)
@@ -207,10 +342,26 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
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 blockedDest != nil {
delay := h.blockDelay(blockedRule)
errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() {
common.Interrupt(input)
common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest)
})
defer timer.Stop()
defer common.Close(output)
if err := buf.Copy(input, buf.Discard); err != nil {
return nil
}
return nil
}
// TODO: SRV/TXT
// if remoteDest := net.DestinationFromAddr(conn.RemoteAddr()); h.applyFinalRules(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule) == RuleAction_Block {
// conn.Close()
// return blackhole(remoteDest)
// }
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr()
@@ -255,7 +406,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
writer = buf.NewWriter(conn)
}
} else {
writer = NewPacketWriter(conn, h, UDPOverride, destination, blockedIPMatcher)
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination)
if h.config.Noises != nil {
errors.LogDebug(ctx, "NOISE", h.config.Noises)
writer = &NoisePacketWriter{
@@ -290,7 +441,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, blockedIPMatcher)
reader = NewPacketReader(conn, h, defaultRule, UDPOverride, destination)
}
if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to process response").Base(err)
@@ -309,7 +460,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, blockedIPMatcher geodata.IPMatcher) buf.Reader {
func NewPacketReader(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {
iConn := conn
statConn, ok := iConn.(*stat.CounterConnection)
if ok {
@@ -328,7 +479,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
return &PacketReader{
PacketConnWrapper: c,
Counter: counter,
BlockedIPMatcher: blockedIPMatcher,
Handler: h,
DefaultRule: defaultRule,
IsOverridden: isOverridden,
InitUnchangedAddr: DialDest.Address,
InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address,
@@ -340,7 +492,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
type PacketReader struct {
*internet.PacketConnWrapper
stats.Counter
BlockedIPMatcher geodata.IPMatcher
Handler *Handler
DefaultRule *FinalRule
IsOverridden bool
InitUnchangedAddr net.Address
InitChangedAddr net.Address
@@ -348,20 +501,19 @@ type PacketReader struct {
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
b := buf.New()
b.Resize(0, buf.Size)
for {
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))
udpAddr := d.(*net.UDPAddr)
sourceAddr := net.IPAddress(udpAddr.IP)
if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) {
if r.Handler.applyFinalRules(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule) == RuleAction_Block {
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
@@ -383,7 +535,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
}
// 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, blockedIPMatcher geodata.IPMatcher) buf.Writer {
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Writer {
iConn := conn
statConn, ok := iConn.(*stat.CounterConnection)
if ok {
@@ -404,7 +556,7 @@ func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, Dia
PacketConnWrapper: c,
Counter: counter,
Handler: h,
BlockedIPMatcher: blockedIPMatcher,
DefaultRule: defaultRule,
UDPOverride: UDPOverride,
ResolvedUDPAddr: resolvedUDPAddr,
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
@@ -418,8 +570,8 @@ type PacketWriter struct {
*internet.PacketConnWrapper
stats.Counter
*Handler
BlockedIPMatcher geodata.IPMatcher
UDPOverride net.Destination
DefaultRule *FinalRule
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
@@ -477,7 +629,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
}
}
}
if isBlockedAddress(w.BlockedIPMatcher, b.UDP.Address) {
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block {
b.Release()
continue
}
+44 -54
View File
@@ -17,7 +17,6 @@ import (
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria"
@@ -56,7 +55,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
ob.CanSpliceCopy = 3
target := ob.Target
conn, err := dialer.Dial(hyCtx.ContextWithRequireDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination)
conn, err := dialer.Dial(hysteria.ContextWithDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination)
if err != nil {
return errors.New("failed to find an available destination").AtWarning().Base(err)
}
@@ -118,7 +117,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
if target.Network == net.Network_UDP {
iConn := stat.TryUnwrapStatsConn(conn)
_, ok := iConn.(*hysteria.InterUdpConn)
_, ok := iConn.(*hysteria.InterConn)
if !ok {
return errors.New("udp requires hysteria udp transport")
}
@@ -127,8 +126,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
writer := &UDPWriter{
Writer: conn,
buf: make([]byte, MaxUDPSize),
writer: conn,
addr: target.NetAddr(),
}
@@ -143,8 +141,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
reader := &UDPReader{
Reader: conn,
buf: make([]byte, MaxUDPSize),
reader: conn,
df: &Defragger{},
}
@@ -173,28 +170,22 @@ func init() {
}
type UDPWriter struct {
Writer io.Writer
buf []byte
writer io.Writer
addr string
buf [buf.Size]byte
}
func (w *UDPWriter) sendMsg(msg *UDPMessage) error {
msgN := msg.Serialize(w.buf)
func (w *UDPWriter) SendMessage(msg *UDPMessage) error {
msgN := msg.Serialize(w.buf[:])
if msgN < 0 {
return nil
}
_, err := w.Writer.Write(w.buf[:msgN])
_, err := w.writer.Write(w.buf[:msgN])
return err
}
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
for {
mb2, b := buf.SplitFirst(mb)
mb = mb2
if b == nil {
break
}
for i, b := range mb {
addr := w.addr
if b.UDP != nil {
addr = b.UDP.NetAddr()
@@ -209,22 +200,20 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
Data: b.Bytes(),
}
err := w.sendMsg(msg)
err := w.SendMessage(msg)
var errTooLarge *quic.DatagramTooLargeError
if go_errors.As(err, &errTooLarge) {
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs {
err := w.sendMsg(&fMsg)
err := w.SendMessage(&fMsg)
if err != nil {
b.Release()
buf.ReleaseMulti(mb)
buf.ReleaseMulti(mb[i:])
return err
}
}
} else if err != nil {
b.Release()
buf.ReleaseMulti(mb)
buf.ReleaseMulti(mb[i:])
return err
}
@@ -235,34 +224,21 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
}
type UDPReader struct {
Reader io.Reader
buf []byte
df *Defragger
firstMsg *UDPMessage
firstDest *net.Destination
reader io.Reader
df *Defragger
firstBuf *buf.Buffer
}
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if r.firstMsg != nil {
buffer := buf.New()
_, 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
}
func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err error) {
for {
n, err := r.Reader.Read(r.buf)
var buf [hysteria.MaxDatagramFrameSize]byte
n, err := r.reader.Read(buf[:])
if err != nil {
return nil, err
return 0, nil, err
}
msg, err := ParseUDPMessage(r.buf[:n])
msg, err := ParseUDPMessage(buf[:n])
if err != nil {
continue
}
@@ -274,17 +250,31 @@ func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
dest, err := net.ParseDestination("udp:" + dfMsg.Addr)
if err != nil {
errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err)
continue
}
buffer := buf.New()
if _, err := buffer.Write(dfMsg.Data); err != nil {
return nil, err
if len(p) < len(dfMsg.Data) {
continue
}
buffer.UDP = &dest
return buf.MultiBuffer{buffer}, nil
return copy(p, dfMsg.Data), &dest, nil
}
}
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if r.firstBuf != nil {
mb := buf.MultiBuffer{r.firstBuf}
r.firstBuf = nil
return mb, nil
}
b := buf.New()
b.Resize(0, buf.Size)
n, addr, err := r.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return nil, err
}
b.Resize(0, int32(n))
b.UDP = addr
return buf.MultiBuffer{b}, nil
}
-9
View File
@@ -1,10 +1 @@
package hysteria
import (
"github.com/xtls/xray-core/transport/internet/hysteria/padding"
)
var (
tcpRequestPadding = padding.Padding{Min: 64, Max: 512}
tcpResponsePadding = padding.Padding{Min: 128, Max: 1024}
)
-35
View File
@@ -1,35 +0,0 @@
package ctx
import (
"context"
"github.com/xtls/xray-core/proxy/hysteria/account"
)
type key int
const (
requireDatagram key = iota
validator
)
func ContextWithRequireDatagram(ctx context.Context, udp bool) context.Context {
if !udp {
return ctx
}
return context.WithValue(ctx, requireDatagram, struct{}{})
}
func RequireDatagramFromContext(ctx context.Context) bool {
_, ok := ctx.Value(requireDatagram).(struct{})
return ok
}
func ContextWithValidator(ctx context.Context, v *account.Validator) context.Context {
return context.WithValue(ctx, validator, v)
}
func ValidatorFromContext(ctx context.Context) *account.Validator {
v, _ := ctx.Value(validator).(*account.Validator)
return v
}
-73
View File
@@ -1,73 +0,0 @@
package hysteria
func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage {
if m.Size() <= maxSize {
return []UDPMessage{*m}
}
fullPayload := m.Data
maxPayloadSize := maxSize - m.HeaderSize()
off := 0
fragID := uint8(0)
fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
frags := make([]UDPMessage, fragCount)
for off < len(fullPayload) {
payloadSize := len(fullPayload) - off
if payloadSize > maxPayloadSize {
payloadSize = maxPayloadSize
}
frag := *m
frag.FragID = fragID
frag.FragCount = fragCount
frag.Data = fullPayload[off : off+payloadSize]
frags[fragID] = frag
off += payloadSize
fragID++
}
return frags
}
// Defragger handles the defragmentation of UDP messages.
// The current implementation can only handle one packet ID at a time.
// If another packet arrives before a packet has received all fragments
// in their entirety, any previous state is discarded.
type Defragger struct {
pktID uint16
frags []*UDPMessage
count uint8
size int // data size
}
func (d *Defragger) Feed(m *UDPMessage) *UDPMessage {
if m.FragCount <= 1 {
return m
}
if m.FragID >= m.FragCount {
// wtf is this?
return nil
}
if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
// new message, clear previous state
d.pktID = m.PacketID
d.frags = make([]*UDPMessage, m.FragCount)
d.frags[m.FragID] = m
d.count = 1
d.size = len(m.Data)
} else if d.frags[m.FragID] == nil {
d.frags[m.FragID] = m
d.count++
d.size += len(m.Data)
if int(d.count) == len(d.frags) {
// all fragments received, assemble
data := make([]byte, d.size)
off := 0
for _, frag := range d.frags {
off += copy(data[off:], frag.Data)
}
m.Data = data
m.FragID = 0
m.FragCount = 1
return m
}
}
return nil
}
+75 -4
View File
@@ -8,6 +8,7 @@ import (
"github.com/apernet/quic-go/quicvarint"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet/hysteria"
)
const (
@@ -17,8 +18,6 @@ const (
MaxMessageLength = 2048
MaxPaddingLength = 4096
MaxUDPSize = 4096
maxVarInt1 = 63
maxVarInt2 = 16383
maxVarInt4 = 1073741823
@@ -62,7 +61,7 @@ func ReadTCPRequest(r io.Reader) (string, error) {
}
func WriteTCPRequest(w io.Writer, addr string) error {
padding := tcpRequestPadding.String()
padding := hysteria.TcpRequestPadding.String()
paddingLen := len(padding)
addrLen := len(addr)
sz := int(quicvarint.Len(uint64(addrLen))) + addrLen +
@@ -122,7 +121,7 @@ func ReadTCPResponse(r io.Reader) (bool, string, error) {
}
func WriteTCPResponse(w io.Writer, ok bool, msg string) error {
padding := tcpResponsePadding.String()
padding := hysteria.TcpResponsePadding.String()
paddingLen := len(padding)
msgLen := len(msg)
sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +
@@ -247,3 +246,75 @@ func varintPut(b []byte, i uint64) int {
}
panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i))
}
func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage {
if m.Size() <= maxSize {
return []UDPMessage{*m}
}
fullPayload := m.Data
maxPayloadSize := maxSize - m.HeaderSize()
off := 0
fragID := uint8(0)
fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
frags := make([]UDPMessage, fragCount)
for off < len(fullPayload) {
payloadSize := len(fullPayload) - off
if payloadSize > maxPayloadSize {
payloadSize = maxPayloadSize
}
frag := *m
frag.FragID = fragID
frag.FragCount = fragCount
frag.Data = fullPayload[off : off+payloadSize]
frags[fragID] = frag
off += payloadSize
fragID++
}
return frags
}
// Defragger handles the defragmentation of UDP messages.
// The current implementation can only handle one packet ID at a time.
// If another packet arrives before a packet has received all fragments
// in their entirety, any previous state is discarded.
type Defragger struct {
pktID uint16
frags []*UDPMessage
count uint8
size int // data size
}
func (d *Defragger) Feed(m *UDPMessage) *UDPMessage {
if m.FragCount <= 1 {
return m
}
if m.FragID >= m.FragCount {
// wtf is this?
return nil
}
if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
// new message, clear previous state
d.pktID = m.PacketID
d.frags = make([]*UDPMessage, m.FragCount)
d.frags[m.FragID] = m
d.count = 1
d.size = len(m.Data)
} else if d.frags[m.FragID] == nil {
d.frags[m.FragID] = m
d.count++
d.size += len(m.Data)
if int(d.count) == len(d.frags) {
// all fragments received, assemble
data := make([]byte, d.size)
off := 0
for _, frag := range d.frags {
off += copy(data[off:], frag.Data)
}
m.Data = data
m.FragID = 0
m.FragCount = 1
return m
}
}
return nil
}
+18 -43
View File
@@ -2,7 +2,6 @@ package hysteria
import (
"context"
"io"
"time"
"github.com/xtls/xray-core/common"
@@ -91,54 +90,30 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
inbound.User = v.User()
}
if _, ok := iConn.(*hysteria.InterUdpConn); ok {
r := io.Reader(conn)
b := make([]byte, MaxUDPSize)
df := &Defragger{}
var firstMsg *UDPMessage
var firstDest net.Destination
for {
n, err := r.Read(b)
if err != nil {
return err
}
msg, err := ParseUDPMessage(b[:n])
if err != nil {
continue
}
dfMsg := df.Feed(msg)
if dfMsg == nil {
continue
}
firstMsg = dfMsg
firstDest, err = net.ParseDestination("udp:" + firstMsg.Addr)
if err != nil {
errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err)
continue
}
break
}
if _, ok := iConn.(*hysteria.InterConn); ok {
reader := &UDPReader{
Reader: r,
buf: b,
df: df,
firstMsg: firstMsg,
firstDest: &firstDest,
reader: conn,
df: &Defragger{},
}
b := buf.New()
b.Resize(0, buf.Size)
n, addr, err := reader.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return err
}
b.Resize(0, int32(n))
b.UDP = addr
reader.firstBuf = b
writer := &UDPWriter{
Writer: conn,
buf: make([]byte, MaxUDPSize),
addr: firstMsg.Addr,
writer: conn,
addr: addr.NetAddr(),
}
return dispatcher.DispatchLink(ctx, firstDest, &transport.Link{
return dispatcher.DispatchLink(ctx, *addr, &transport.Link{
Reader: reader,
Writer: writer,
})
+14 -78
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,83 +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.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)
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
}
+4 -2
View File
@@ -41,10 +41,12 @@ Here is simple Xray config snippet to enable the inbound:
- IPv4 and IPv6
- TCP and UDP
- ICMP Echo (ping)
## LIMITATION
- No ICMP support
- Only ICMP Echo request/reply is supported; other ICMP message types are ignored
- ICMP Echo replies are generated locally by the TUN stack; they do not validate real remote ICMP reachability
- Connections are established to any host, as connection success is only a mark of successful accepting packet for proxying. Hosts that are not accepting connections or don't even exists, will look like they opened a connection (SYN-ACK), and never send back a single byte, closing connection (RST) after some time. This is the side effect of the whole process actually being a proxy, and not real network layer 3 vpn
## CONSIDERATIONS
@@ -248,4 +250,4 @@ Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number b
Build using gomobile for iOS framework integration:
```
gomobile bind -target=ios
```
```
+60 -13
View File
@@ -3,6 +3,8 @@ package tun
import (
"context"
"net"
"sort"
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
@@ -45,26 +47,53 @@ func (updater *InterfaceUpdater) Update() {
}
var got *net.Interface
for _, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if updater.fixedName != "" {
if updater.fixedName != "" {
for _, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if iface.Name == updater.fixedName {
got = &iface
break
}
} else {
addrs, err := iface.Addrs()
if err != nil {
}
} else {
var ifs []struct {
index int
score int
}
for i, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if (iface.Flags&net.FlagUp != 0) &&
(iface.Flags&net.FlagLoopback == 0) &&
len(addrs) > 0 {
got = &iface
break
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
ifs = append(ifs, struct {
index int
score int
}{i, score(&iface, addrs)})
}
sort.Slice(ifs, func(i, j int) bool {
if ifs[i].score != ifs[j].score {
return ifs[i].score > ifs[j].score
}
return interfaces[ifs[i].index].Name < interfaces[ifs[j].index].Name
})
if len(ifs) > 0 {
iface := interfaces[ifs[0].index]
got = &iface
}
}
@@ -76,3 +105,21 @@ func (updater *InterfaceUpdater) Update() {
updater.iface = got
errors.LogInfo(context.Background(), "[tun] update interface ", got.Name, " ", got.Index)
}
func score(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score += 1
break
}
}
return score
}
+106
View File
@@ -0,0 +1,106 @@
package icmp
import (
"github.com/xtls/xray-core/common/errors"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func ProtocolLabel(netProto tcpip.NetworkProtocolNumber) string {
switch netProto {
case header.IPv4ProtocolNumber:
return "ipv4"
case header.IPv6ProtocolNumber:
return "ipv6"
default:
return "unknown"
}
}
func ParseEchoRequest(netProto tcpip.NetworkProtocolNumber, message []byte) (uint16, uint16, bool) {
switch netProto {
case header.IPv4ProtocolNumber:
if len(message) < header.ICMPv4MinimumSize {
return 0, 0, false
}
icmpHdr := header.ICMPv4(message)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
return 0, 0, false
}
return icmpHdr.Ident(), icmpHdr.Sequence(), true
case header.IPv6ProtocolNumber:
if len(message) < header.ICMPv6MinimumSize {
return 0, 0, false
}
icmpHdr := header.ICMPv6(message)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
return 0, 0, false
}
return icmpHdr.Ident(), icmpHdr.Sequence(), true
default:
return 0, 0, false
}
}
func RewriteChecksum(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
switch netProto {
case header.IPv4ProtocolNumber:
if len(message) < header.ICMPv4MinimumSize {
return errors.New("invalid icmpv4 packet")
}
icmpHdr := header.ICMPv4(message)
icmpHdr.SetChecksum(0)
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0)))
return nil
case header.IPv6ProtocolNumber:
if len(message) < header.ICMPv6MinimumSize {
return errors.New("invalid icmpv6 packet")
}
icmpHdr := header.ICMPv6(message)
icmpHdr.SetChecksum(0)
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr[:header.ICMPv6MinimumSize],
Src: srcIP,
Dst: dstIP,
PayloadCsum: checksum.Checksum(icmpHdr.Payload(), 0),
PayloadLen: len(icmpHdr.Payload()),
}))
return nil
default:
return errors.New("unsupported icmp network protocol")
}
}
func BuildLocalEchoReply(netProto tcpip.NetworkProtocolNumber, request []byte, srcIP, dstIP tcpip.Address) ([]byte, error) {
reply := append([]byte(nil), request...)
switch netProto {
case header.IPv4ProtocolNumber:
if len(reply) < header.ICMPv4MinimumSize {
return nil, errors.New("invalid icmpv4 echo packet")
}
icmpHdr := header.ICMPv4(reply)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
return nil, errors.New("not an icmpv4 echo request")
}
reply[0] = byte(header.ICMPv4EchoReply)
case header.IPv6ProtocolNumber:
if len(reply) < header.ICMPv6MinimumSize {
return nil, errors.New("invalid icmpv6 echo packet")
}
icmpHdr := header.ICMPv6(reply)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
return nil, errors.New("not an icmpv6 echo request")
}
reply[0] = byte(header.ICMPv6EchoReply)
default:
return nil, errors.New("unsupported icmp network protocol")
}
if err := RewriteChecksum(netProto, reply, srcIP, dstIP); err != nil {
return nil, err
}
return reply, nil
}
+174
View File
@@ -0,0 +1,174 @@
package icmp
import (
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func TestParseEchoRequest(t *testing.T) {
t.Run("ipv4 echo", func(t *testing.T) {
var zero tcpip.Address
packet := []byte{
byte(header.ICMPv4Echo), 0,
0, 0,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb,
}
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
t.Fatal(err)
}
ident, sequence, ok := ParseEchoRequest(header.IPv4ProtocolNumber, packet)
if !ok {
t.Fatal("expected ipv4 echo request to parse")
}
if ident != 0x1234 || sequence != 0x5678 {
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
}
})
t.Run("ipv6 echo", func(t *testing.T) {
packet := []byte{
byte(header.ICMPv6EchoRequest), 0,
0, 0,
0xab, 0xcd,
0xef, 0x01,
0xaa, 0xbb,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
t.Fatal(err)
}
ident, sequence, ok := ParseEchoRequest(header.IPv6ProtocolNumber, packet)
if !ok {
t.Fatal("expected ipv6 echo request to parse")
}
if ident != 0xabcd || sequence != 0xef01 {
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
}
})
}
func TestRewriteChecksum(t *testing.T) {
t.Run("ipv4", func(t *testing.T) {
var zero tcpip.Address
packet := []byte{
byte(header.ICMPv4Echo), 0,
0xff, 0xff,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
t.Fatal(err)
}
icmpHdr := header.ICMPv4(packet)
if got, want := icmpHdr.Checksum(), header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksumPayloadV4(icmpHdr.Payload())); got != want {
t.Fatalf("unexpected ipv4 checksum: got %x want %x", got, want)
}
})
t.Run("ipv6", func(t *testing.T) {
packet := []byte{
byte(header.ICMPv6EchoReply), 0,
0xff, 0xff,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
t.Fatal(err)
}
icmpHdr := header.ICMPv6(packet)
want := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr[:header.ICMPv6MinimumSize],
Src: src,
Dst: dst,
PayloadLen: len(icmpHdr.Payload()),
PayloadCsum: checksumPayloadV6(icmpHdr.Payload()),
})
if got := icmpHdr.Checksum(); got != want {
t.Fatalf("unexpected ipv6 checksum: got %x want %x", got, want)
}
})
}
func TestBuildLocalEchoReply(t *testing.T) {
t.Run("ipv4", func(t *testing.T) {
request := []byte{
byte(header.ICMPv4Echo), 0,
0, 0,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
src := tcpip.Address{}
dst := tcpip.Address{}
if err := RewriteChecksum(header.IPv4ProtocolNumber, request, src, dst); err != nil {
t.Fatal(err)
}
reply, err := BuildLocalEchoReply(header.IPv4ProtocolNumber, request, dst, src)
if err != nil {
t.Fatal(err)
}
if request[0] != byte(header.ICMPv4Echo) {
t.Fatal("request mutated")
}
icmpHdr := header.ICMPv4(reply)
if icmpHdr.Type() != header.ICMPv4EchoReply || icmpHdr.Code() != header.ICMPv4UnusedCode {
t.Fatalf("unexpected ipv4 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
}
if icmpHdr.Ident() != 0x1234 || icmpHdr.Sequence() != 0x5678 {
t.Fatalf("unexpected ipv4 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
}
})
t.Run("ipv6", func(t *testing.T) {
request := []byte{
byte(header.ICMPv6EchoRequest), 0,
0, 0,
0xab, 0xcd,
0xef, 0x01,
0xaa, 0xbb, 0xcc,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, request, src, dst); err != nil {
t.Fatal(err)
}
reply, err := BuildLocalEchoReply(header.IPv6ProtocolNumber, request, dst, src)
if err != nil {
t.Fatal(err)
}
if request[0] != byte(header.ICMPv6EchoRequest) {
t.Fatal("request mutated")
}
icmpHdr := header.ICMPv6(reply)
if icmpHdr.Type() != header.ICMPv6EchoReply || icmpHdr.Code() != header.ICMPv6UnusedCode {
t.Fatalf("unexpected ipv6 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
}
if icmpHdr.Ident() != 0xabcd || icmpHdr.Sequence() != 0xef01 {
t.Fatalf("unexpected ipv6 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
}
})
}
func checksumPayloadV4(payload []byte) uint16 {
return checksum.Checksum(payload, 0)
}
func checksumPayloadV6(payload []byte) uint16 {
return checksum.Checksum(payload, 0)
}
+4 -1
View File
@@ -14,6 +14,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
@@ -117,6 +118,8 @@ func (t *stackGVisor) Start() error {
udpForwarder.HandlePacket(src, dst, data)
return true
})
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, t.handleICMPv4Packet)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, t.handleICMPv6Packet)
t.stack = ipStack
t.endpoint = linkEndpoint
@@ -205,7 +208,7 @@ func (t *stackGVisor) Close() error {
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
HandleLocal: false,
}
gStack := stack.New(opts)
+98
View File
@@ -0,0 +1,98 @@
package tun
import (
"github.com/xtls/xray-core/common/errors"
tunicmp "github.com/xtls/xray-core/proxy/tun/icmp"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
func (t *stackGVisor) handleICMPv4Packet(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
return t.handleICMPEchoPacket(header.IPv4ProtocolNumber, id, pkt)
}
func (t *stackGVisor) handleICMPv6Packet(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
return t.handleICMPEchoPacket(header.IPv6ProtocolNumber, id, pkt)
}
func (t *stackGVisor) handleICMPEchoPacket(netProto tcpip.NetworkProtocolNumber, id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
srcIP := id.RemoteAddress
dstIP := id.LocalAddress
if srcIP.Len() == 0 || dstIP.Len() == 0 {
return true
}
message := transportPacketBytes(pkt)
ident, sequence, ok := tunicmp.ParseEchoRequest(netProto, message)
if !ok {
return true
}
reply, err := tunicmp.BuildLocalEchoReply(netProto, message, dstIP, srcIP)
if err != nil {
errors.LogInfoInner(t.ctx, err, "[tun] failed to build local icmp echo reply")
return true
}
errors.LogDebug(t.ctx, "[tun][icmp] ", tunicmp.ProtocolLabel(netProto), " local echo reply ", dstIP, " -> ", srcIP, " id=", ident, " seq=", sequence)
if err := t.writeRawICMPPacket(netProto, reply, dstIP, srcIP); err != nil {
errors.LogInfoInner(t.ctx, err, "[tun] failed to write local icmp echo reply")
}
return true
}
func (t *stackGVisor) writeRawICMPPacket(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
ipHeaderSize := header.IPv6MinimumSize
ipProtocol := header.IPv6ProtocolNumber
transportProtocol := header.ICMPv6ProtocolNumber
if netProto == header.IPv4ProtocolNumber {
ipHeaderSize = header.IPv4MinimumSize
ipProtocol = header.IPv4ProtocolNumber
transportProtocol = header.ICMPv4ProtocolNumber
}
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: ipHeaderSize,
Payload: buffer.MakeWithData(message),
})
defer pkt.DecRef()
if netProto == header.IPv4ProtocolNumber {
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
ipHdr.Encode(&header.IPv4Fields{
TotalLength: uint16(header.IPv4MinimumSize + len(message)),
TTL: 64,
Protocol: uint8(transportProtocol),
SrcAddr: srcIP,
DstAddr: dstIP,
})
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
} else {
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
ipHdr.Encode(&header.IPv6Fields{
PayloadLength: uint16(len(message)),
TransportProtocol: transportProtocol,
HopLimit: 64,
SrcAddr: srcIP,
DstAddr: dstIP,
})
}
if err := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView())); err != nil {
return errors.New("failed to write raw icmp packet back to stack", err)
}
return nil
}
func transportPacketBytes(pkt *stack.PacketBuffer) []byte {
headerBytes := pkt.TransportHeader().Slice()
payloadBytes := pkt.Data().AsRange().ToSlice()
message := make([]byte, len(headerBytes)+len(payloadBytes))
copy(message, headerBytes)
copy(message[len(headerBytes):], payloadBytes)
return message
}
-3
View File
@@ -177,9 +177,6 @@ func (t *WindowsTun) Start() error {
if updater != nil {
t.changeCallback, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
if notificationType != winipcfg.MibDeleteInstance {
return
}
updater.Update()
})
if err != nil {
+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
+7 -7
View File
@@ -71,7 +71,7 @@ func TestCommanderListenConfigurationItem(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
Tag: "default-outbound",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
}
@@ -166,7 +166,7 @@ func TestCommanderRemoveHandler(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
Tag: "default-outbound",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
}
@@ -262,7 +262,7 @@ func TestCommanderListHandlers(t *testing.T) {
{
Tag: "default-outbound",
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
}
@@ -388,7 +388,7 @@ func TestCommanderAddRemoveUser(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -428,7 +428,7 @@ func TestCommanderAddRemoveUser(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: u2.String(),
SecuritySettings: &protocol.SecurityConfig{
@@ -576,7 +576,7 @@ func TestCommanderStats(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -603,7 +603,7 @@ func TestCommanderStats(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{
+2 -2
View File
@@ -60,7 +60,7 @@ func TestDokodemoTCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -160,7 +160,7 @@ func TestDokodemoUDP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+9 -9
View File
@@ -62,7 +62,7 @@ func TestPassiveConnection(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -123,7 +123,7 @@ func TestProxy(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -152,7 +152,7 @@ func TestProxy(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -252,7 +252,7 @@ func TestProxyOverKCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -281,7 +281,7 @@ func TestProxyOverKCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
StreamSettings: &internet.StreamConfig{
@@ -400,7 +400,7 @@ func TestBlackhole(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
Tag: "direct",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
{
Tag: "blocked",
@@ -515,7 +515,7 @@ func TestUDPConnection(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -580,7 +580,7 @@ func TestDomainSniffing(t *testing.T) {
},
{
Tag: "direct",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
App: []*serial.TypedMessage{
@@ -667,7 +667,7 @@ func TestDialXray(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+5 -5
View File
@@ -48,7 +48,7 @@ func TestHttpConformance(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -111,7 +111,7 @@ func TestHttpError(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -161,7 +161,7 @@ func TestHTTPConnectMethod(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -243,7 +243,7 @@ func TestHttpPost(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -315,7 +315,7 @@ func TestHttpBasicAuth(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+1 -1
View File
@@ -63,7 +63,7 @@ func TestMetrics(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
Tag: "default-outbound",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
}
+2 -2
View File
@@ -85,7 +85,7 @@ func TestVMessClosing(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -193,7 +193,7 @@ func TestZeroBuffer(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+2 -2
View File
@@ -151,7 +151,7 @@ func TestReverseProxy(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
Tag: "freedom",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
{
Tag: "reverse",
@@ -340,7 +340,7 @@ func TestReverseProxyLongRunning(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
Tag: "freedom",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
{
Tag: "reverse",
+2 -2
View File
@@ -82,7 +82,7 @@ func testShadowsocks2022Tcp(t *testing.T, method string, password string) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -167,7 +167,7 @@ func testShadowsocks2022Udp(t *testing.T, method string, password string) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+5 -5
View File
@@ -53,7 +53,7 @@ func TestShadowsocksChaCha20Poly1305TCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -141,7 +141,7 @@ func TestShadowsocksAES256GCMTCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -236,7 +236,7 @@ func TestShadowsocksAES128GCMUDP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -330,7 +330,7 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -425,7 +425,7 @@ func TestShadowsocksNone(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+5 -5
View File
@@ -51,7 +51,7 @@ func TestSocksBridgeTCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -128,7 +128,7 @@ func TestSocksWithHttpRequest(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -217,7 +217,7 @@ func TestSocksBridageUDP(t *testing.T) {
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
}
@@ -332,7 +332,7 @@ func TestSocksBridageUDPWithRouting(t *testing.T) {
},
{
Tag: "out",
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}),
},
},
}
@@ -429,7 +429,7 @@ func TestSocksConformanceMod(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+10 -10
View File
@@ -69,7 +69,7 @@ func TestSimpleTLSConnection(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -182,7 +182,7 @@ func TestAutoIssuingCertificate(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -287,7 +287,7 @@ func TestTLSOverKCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -387,7 +387,7 @@ func TestTLSOverWebSocket(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -503,7 +503,7 @@ func TestGRPC(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -619,7 +619,7 @@ func TestGRPCMultiMode(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -728,7 +728,7 @@ func TestSimpleTLSConnectionPinned(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -827,7 +827,7 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -925,7 +925,7 @@ func TestUTLSConnectionPinned(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -1025,7 +1025,7 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+1 -1
View File
@@ -63,7 +63,7 @@ func TestHTTPConnectionHeader(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+5 -5
View File
@@ -67,7 +67,7 @@ func TestVless(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -172,7 +172,7 @@ func TestVlessTls(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -295,7 +295,7 @@ func TestVlessXtlsVision(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -427,7 +427,7 @@ func TestVlessXtlsVisionReality(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -566,7 +566,7 @@ func TestVlessRealityFingerprints(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+12 -12
View File
@@ -62,7 +62,7 @@ func TestVMessGCM(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -162,7 +162,7 @@ func TestVMessGCMReadv(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -265,7 +265,7 @@ func TestVMessGCMUDP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -362,7 +362,7 @@ func TestVMessChacha20(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -460,7 +460,7 @@ func TestVMessNone(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -560,7 +560,7 @@ func TestVMessKCP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -665,7 +665,7 @@ func TestVMessKCPLarge(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -771,7 +771,7 @@ func TestVMessGCMMux(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -884,7 +884,7 @@ func TestVMessGCMMuxUDP(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -1007,7 +1007,7 @@ func TestVMessZero(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -1104,7 +1104,7 @@ func TestVMessGCMLengthAuth(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
@@ -1206,7 +1206,7 @@ func TestVMessGCMLengthAuthPlusNoTerminationSignal(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+1 -1
View File
@@ -63,7 +63,7 @@ func TestWireguard(t *testing.T) {
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
IpsBlocked: &freedom.IPRules{},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
+56 -30
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
@@ -19,13 +20,15 @@ import (
var webpage []byte
type task struct {
Method string `json:"method"`
URL string `json:"url"`
Extra any `json:"extra,omitempty"`
StreamResponse bool `json:"streamResponse"`
Method string `json:"method"`
URL string `json:"url"`
Extra any `json:"extra,omitempty"`
StreamResponse bool `json:"streamResponse"`
}
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()
}
}
@@ -70,15 +94,13 @@ type webSocketExtra struct {
func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
task := task{
Method: "WS",
URL: uri,
Method: "WS",
URL: uri,
StreamResponse: true,
}
if ed != nil {
task.Extra = webSocketExtra{
Protocol: base64.RawURLEncoding.EncodeToString(ed),
}
task.Extra = webSocketExtra{
Protocol: base64.RawURLEncoding.EncodeToString(ed),
}
return dialTask(task)
@@ -120,9 +142,9 @@ func httpExtraFromHeadersAndCookies(headers http.Header, cookies []*http.Cookie)
func DialGet(uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) {
task := task{
Method: "GET",
URL: uri,
Extra: httpExtraFromHeadersAndCookies(headers, cookies),
Method: "GET",
URL: uri,
Extra: httpExtraFromHeadersAndCookies(headers, cookies),
StreamResponse: true,
}
@@ -135,9 +157,9 @@ func DialPacket(method string, uri string, headers http.Header, cookies []*http.
func dialWithBody(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error {
task := task{
Method: method,
URL: uri,
Extra: httpExtraFromHeadersAndCookies(headers, cookies),
Method: method,
URL: uri,
Extra: httpExtraFromHeadersAndCookies(headers, cookies),
StreamResponse: false,
}
@@ -194,3 +216,7 @@ func CheckOK(conn *websocket.Conn) error {
return nil
}
func init() {
Reload()
}

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