mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-08 14:13:22 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbafe629a | ||
|
|
c42deab55c | ||
|
|
906d49a271 | ||
|
|
4192ca0827 | ||
|
|
228f1e13aa | ||
|
|
15968585f3 | ||
|
|
4951994ebe | ||
|
|
756a2d1327 | ||
|
|
b279076ba1 | ||
|
|
e61eeae258 | ||
|
|
958eb9ea8f | ||
|
|
8381a5a8a6 | ||
|
|
1ead940a71 | ||
|
|
bdff2fa72e | ||
|
|
1d62941bd2 | ||
|
|
52cf9ef5d6 | ||
|
|
16568314d8 | ||
|
|
1fc6850dc4 | ||
|
|
4e87f59628 | ||
|
|
7ab0a3ccb7 | ||
|
|
2fff03720d | ||
|
|
7f7fc5a829 | ||
|
|
ff6c060168 | ||
|
|
5b552db781 | ||
|
|
108bf7ff82 | ||
|
|
1836b1c6e4 | ||
|
|
b4f08981be | ||
|
|
cd4d0baacd | ||
|
|
3bc24a3d5d | ||
|
|
fa07b34956 | ||
|
|
85a8bf5f39 | ||
|
|
d0f533f94a | ||
|
|
d1db1d6a27 | ||
|
|
1a14ffcec6 | ||
|
|
454c930d13 | ||
|
|
bc590bcb56 | ||
|
|
7cf25970de | ||
|
|
d837687368 | ||
|
|
b4650360d6 | ||
|
|
d52f15060b | ||
|
|
31ab22c33d | ||
|
|
d42c981f9c | ||
|
|
cb1106c2fb | ||
|
|
df4b97097c | ||
|
|
a9cec25b8d |
4
.github/docker/Dockerfile
vendored
4
.github/docker/Dockerfile
vendored
@@ -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
|
||||
|
||||
4
.github/docker/Dockerfile.usa
vendored
4
.github/docker/Dockerfile.usa
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
119
app/dns/dns.go
119
app/dns/dns.go
@@ -5,18 +5,16 @@ import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
)
|
||||
|
||||
@@ -158,9 +156,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var domainMatcher geodata.DomainMatcher
|
||||
if len(effectiveRules) > 0 {
|
||||
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no DNS client in config, add a `localhost` DNS client
|
||||
@@ -220,7 +221,7 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
|
||||
}
|
||||
|
||||
if s.checkSystem {
|
||||
supportIPv4, supportIPv6 := checkRoutes()
|
||||
supportIPv4, supportIPv6 := utils.CheckRoutes()
|
||||
option.IPv4Enable = option.IPv4Enable && supportIPv4
|
||||
option.IPv6Enable = option.IPv6Enable && supportIPv6
|
||||
} else {
|
||||
@@ -271,25 +272,27 @@ func (s *DNS) sortClients(domain string) []*Client {
|
||||
|
||||
// Priority domain matching
|
||||
hasMatch := false
|
||||
MatchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||
sort.Slice(MatchSlice, func(i, j int) bool {
|
||||
return MatchSlice[i] < MatchSlice[j]
|
||||
})
|
||||
for _, match := range MatchSlice {
|
||||
info := s.matcherInfos[match]
|
||||
client := s.clients[info.clientIdx]
|
||||
domainRule := info.domainRule
|
||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||
if clientUsed[info.clientIdx] {
|
||||
continue
|
||||
}
|
||||
clientUsed[info.clientIdx] = true
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
hasMatch = true
|
||||
if client.finalQuery {
|
||||
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||
return clients
|
||||
if s.domainMatcher != nil {
|
||||
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||
sort.Slice(matchSlice, func(i, j int) bool {
|
||||
return matchSlice[i] < matchSlice[j]
|
||||
})
|
||||
for _, match := range matchSlice {
|
||||
info := s.matcherInfos[match]
|
||||
client := s.clients[info.clientIdx]
|
||||
domainRule := info.domainRule
|
||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||
if clientUsed[info.clientIdx] {
|
||||
continue
|
||||
}
|
||||
clientUsed[info.clientIdx] = true
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
hasMatch = true
|
||||
if client.finalQuery {
|
||||
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||
return clients
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,67 +537,3 @@ func init() {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
}
|
||||
|
||||
func probeRoutes() (ipv4 bool, ipv6 bool) {
|
||||
if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil {
|
||||
ipv4 = true
|
||||
conn.Close()
|
||||
}
|
||||
if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil {
|
||||
ipv6 = true
|
||||
conn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var routeCache struct {
|
||||
sync.Once
|
||||
sync.RWMutex
|
||||
expire time.Time
|
||||
ipv4, ipv6 bool
|
||||
}
|
||||
|
||||
func checkRoutes() (bool, bool) {
|
||||
if !isGUIPlatform {
|
||||
routeCache.Once.Do(func() {
|
||||
routeCache.ipv4, routeCache.ipv6 = probeRoutes()
|
||||
})
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
|
||||
routeCache.RWMutex.RLock()
|
||||
now := time.Now()
|
||||
if routeCache.expire.After(now) {
|
||||
routeCache.RWMutex.RUnlock()
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
routeCache.RWMutex.RUnlock()
|
||||
|
||||
routeCache.RWMutex.Lock()
|
||||
defer routeCache.RWMutex.Unlock()
|
||||
|
||||
now = time.Now()
|
||||
if routeCache.expire.After(now) { // double-check
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms
|
||||
routeCache.expire = now.Add(100 * time.Millisecond) // ttl
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
|
||||
var isGUIPlatform = detectGUIPlatform()
|
||||
|
||||
func detectGUIPlatform() bool {
|
||||
switch runtime.GOOS {
|
||||
case "android", "ios", "windows", "darwin":
|
||||
return true
|
||||
case "linux", "freebsd", "openbsd":
|
||||
if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" {
|
||||
return true
|
||||
}
|
||||
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -548,15 +548,8 @@ func TestIPMatch(t *testing.T) {
|
||||
Port: uint32(port),
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
// inner ip, will not match
|
||||
Ip: []byte{192, 168, 11, 1},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
// inner ip, will not match
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
// second dns, match ip
|
||||
@@ -571,22 +564,8 @@ func TestIPMatch(t *testing.T) {
|
||||
Port: uint32(port),
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 4},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -598,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}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -676,9 +655,9 @@ func TestLocalDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will match localhost, localhost-a and localhost-b,
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -717,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}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -901,22 +880,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will only match 8.8.8.8 and 8.8.4.4
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 4, 4},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -936,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will match 8.8.8.8 and 8.8.8.7, etc
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 7},
|
||||
Prefix: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -963,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will only match 8.8.7.7 (api.google.com)
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 7, 7},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -990,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will only match 8.8.7.8 (v2.api.google.com)
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 7, 8},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1009,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}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
@@ -166,7 +167,7 @@ func (c *Client) Name() string {
|
||||
// QueryIP sends DNS query to the name server with the client's IP.
|
||||
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) {
|
||||
if c.checkSystem {
|
||||
supportIPv4, supportIPv6 := checkRoutes()
|
||||
supportIPv4, supportIPv6 := utils.CheckRoutes()
|
||||
option.IPv4Enable = option.IPv4Enable && supportIPv4
|
||||
option.IPv6Enable = option.IPv6Enable && supportIPv6
|
||||
} else {
|
||||
|
||||
198
app/geodata/config.pb.go
Normal file
198
app/geodata/config.pb.go
Normal 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
app/geodata/config.proto
Normal file
21
app/geodata/config.proto
Normal 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
app/geodata/download.go
Normal file
304
app/geodata/download.go
Normal 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
app/geodata/geodata.go
Normal file
134
app/geodata/geodata.go
Normal 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))
|
||||
}))
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -308,7 +308,7 @@ func TestServiceTestRoute(t *testing.T) {
|
||||
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
|
||||
},
|
||||
{
|
||||
SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
|
||||
SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}},
|
||||
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -92,25 +92,22 @@ func TestRoutingRule(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
Prefix: 32,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
Prefix: 32,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
|
||||
Prefix: 128,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -140,9 +137,8 @@ func TestRoutingRule(t *testing.T) {
|
||||
SourceIp: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{192, 168, 0, 0},
|
||||
Prefix: 16,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -159,9 +159,8 @@ func TestIPOnDemand(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{192, 168, 0, 0},
|
||||
Prefix: 16,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -204,9 +203,8 @@ func TestIPIfNonMatchDomain(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{192, 168, 0, 0},
|
||||
Prefix: 16,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -249,9 +247,8 @@ func TestIPIfNonMatchIP(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{127, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,7 +11,11 @@ import (
|
||||
)
|
||||
|
||||
type DomainMatcher interface {
|
||||
// Match returns the indices of all rules that match the input domain.
|
||||
// The returned slice is owned by the caller and may be safely modified.
|
||||
// Note: the slice may contain duplicates and the order is unspecified.
|
||||
Match(input string) []uint32
|
||||
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
|
||||
@@ -19,10 +23,54 @@ type DomainMatcherFactory interface {
|
||||
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
|
||||
}
|
||||
|
||||
type MphDomainMatcherFactory struct{}
|
||||
type MphDomainMatcherFactory struct {
|
||||
sync.Mutex
|
||||
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
|
||||
}
|
||||
|
||||
func buildDomainRulesKey(rules []*DomainRule) string {
|
||||
var sb strings.Builder
|
||||
cache := false
|
||||
for _, r := range rules {
|
||||
switch v := r.Value.(type) {
|
||||
case *DomainRule_Custom:
|
||||
sb.WriteString(v.Custom.Type.String())
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(v.Custom.Value)
|
||||
sb.WriteString(",")
|
||||
case *DomainRule_Geosite:
|
||||
cache = true
|
||||
sb.WriteString(v.Geosite.File)
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(v.Geosite.Code)
|
||||
sb.WriteString("@")
|
||||
sb.WriteString(v.Geosite.Attrs)
|
||||
sb.WriteString(",")
|
||||
default:
|
||||
panic("unknown domain rule type")
|
||||
}
|
||||
}
|
||||
if !cache {
|
||||
return ""
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// BuildMatcher implements DomainMatcherFactory.
|
||||
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||
if len(rules) == 0 {
|
||||
return nil, errors.New("empty domain rule list")
|
||||
}
|
||||
key := buildDomainRulesKey(rules)
|
||||
if key != "" {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
if g := f.shared[key]; g != nil {
|
||||
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
|
||||
return g, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
|
||||
}
|
||||
g := strmatcher.NewMphValueMatcher()
|
||||
for i, r := range rules {
|
||||
switch v := r.Value.(type) {
|
||||
@@ -53,25 +101,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
|
||||
if err := g.Build(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key != "" {
|
||||
f.shared[key] = g
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
type CompactDomainMatcherFactory struct {
|
||||
sync.Mutex
|
||||
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
|
||||
shared map[string]strmatcher.MatcherSet // TODO: cleanup
|
||||
}
|
||||
|
||||
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, error) {
|
||||
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
|
||||
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
if m := f.shared[key]; m != nil {
|
||||
return m, nil
|
||||
if s := f.shared[key]; s != nil {
|
||||
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
|
||||
return s, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
|
||||
|
||||
g := strmatcher.NewLinearValueMatcher()
|
||||
s := strmatcher.NewLinearAnyMatcher()
|
||||
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -83,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 {
|
||||
@@ -122,7 +178,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
|
||||
|
||||
type CompactDomainMatcher struct {
|
||||
custom strmatcher.ValueMatcher
|
||||
matchers []strmatcher.MatcherGroup
|
||||
matchers []strmatcher.MatcherSet
|
||||
values []uint32
|
||||
}
|
||||
|
||||
@@ -174,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)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}}},
|
||||
@@ -48,3 +48,25 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
||||
t.Fatalf("Match() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
||||
matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildMatcher() failed: %v", err)
|
||||
}
|
||||
|
||||
got := matcher.Match("example.com")
|
||||
if !reflect.DeepEqual(got, []uint32{0, 1}) {
|
||||
t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1})
|
||||
}
|
||||
|
||||
got[0] = 1
|
||||
|
||||
gotAgain := matcher.Match("example.com")
|
||||
if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) {
|
||||
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -433,6 +433,58 @@ func (x *CIDR) GetPrefix() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type CIDRRule struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Cidr *CIDR `protobuf:"bytes,1,opt,name=cidr,proto3" json:"cidr,omitempty"`
|
||||
ReverseMatch bool `protobuf:"varint,2,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CIDRRule) Reset() {
|
||||
*x = CIDRRule{}
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CIDRRule) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CIDRRule) ProtoMessage() {}
|
||||
|
||||
func (x *CIDRRule) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CIDRRule.ProtoReflect.Descriptor instead.
|
||||
func (*CIDRRule) Descriptor() ([]byte, []int) {
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *CIDRRule) GetCidr() *CIDR {
|
||||
if x != nil {
|
||||
return x.Cidr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CIDRRule) GetReverseMatch() bool {
|
||||
if x != nil {
|
||||
return x.ReverseMatch
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type GeoIP struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
@@ -444,7 +496,7 @@ type GeoIP struct {
|
||||
|
||||
func (x *GeoIP) Reset() {
|
||||
*x = GeoIP{}
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[6]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -456,7 +508,7 @@ func (x *GeoIP) String() string {
|
||||
func (*GeoIP) ProtoMessage() {}
|
||||
|
||||
func (x *GeoIP) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[6]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -469,7 +521,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
|
||||
func (*GeoIP) Descriptor() ([]byte, []int) {
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *GeoIP) GetCode() string {
|
||||
@@ -502,7 +554,7 @@ type GeoIPList struct {
|
||||
|
||||
func (x *GeoIPList) Reset() {
|
||||
*x = GeoIPList{}
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[7]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -514,7 +566,7 @@ func (x *GeoIPList) String() string {
|
||||
func (*GeoIPList) ProtoMessage() {}
|
||||
|
||||
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[7]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -527,7 +579,7 @@ func (x *GeoIPList) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
|
||||
func (*GeoIPList) Descriptor() ([]byte, []int) {
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *GeoIPList) GetEntry() []*GeoIP {
|
||||
@@ -548,7 +600,7 @@ type GeoIPRule struct {
|
||||
|
||||
func (x *GeoIPRule) Reset() {
|
||||
*x = GeoIPRule{}
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[8]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -560,7 +612,7 @@ func (x *GeoIPRule) String() string {
|
||||
func (*GeoIPRule) ProtoMessage() {}
|
||||
|
||||
func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[8]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -573,7 +625,7 @@ func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
|
||||
func (*GeoIPRule) Descriptor() ([]byte, []int) {
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *GeoIPRule) GetFile() string {
|
||||
@@ -610,7 +662,7 @@ type IPRule struct {
|
||||
|
||||
func (x *IPRule) Reset() {
|
||||
*x = IPRule{}
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[9]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -622,7 +674,7 @@ func (x *IPRule) String() string {
|
||||
func (*IPRule) ProtoMessage() {}
|
||||
|
||||
func (x *IPRule) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[9]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -635,7 +687,7 @@ func (x *IPRule) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
|
||||
func (*IPRule) Descriptor() ([]byte, []int) {
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
|
||||
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *IPRule) GetValue() isIPRule_Value {
|
||||
@@ -654,7 +706,7 @@ func (x *IPRule) GetGeoip() *GeoIPRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *IPRule) GetCustom() *CIDR {
|
||||
func (x *IPRule) GetCustom() *CIDRRule {
|
||||
if x != nil {
|
||||
if x, ok := x.Value.(*IPRule_Custom); ok {
|
||||
return x.Custom
|
||||
@@ -672,7 +724,7 @@ type IPRule_Geoip struct {
|
||||
}
|
||||
|
||||
type IPRule_Custom struct {
|
||||
Custom *CIDR `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
|
||||
Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*IPRule_Geoip) isIPRule_Value() {}
|
||||
@@ -693,7 +745,7 @@ type Domain_Attribute struct {
|
||||
|
||||
func (x *Domain_Attribute) Reset() {
|
||||
*x = Domain_Attribute{}
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[10]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -705,7 +757,7 @@ func (x *Domain_Attribute) String() string {
|
||||
func (*Domain_Attribute) ProtoMessage() {}
|
||||
|
||||
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[10]
|
||||
mi := &file_common_geodata_geodat_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -807,7 +859,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
|
||||
"\x05value\".\n" +
|
||||
"\x04CIDR\x12\x0e\n" +
|
||||
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
|
||||
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"o\n" +
|
||||
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" +
|
||||
"\bCIDRRule\x12-\n" +
|
||||
"\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
|
||||
"\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" +
|
||||
"\x05GeoIP\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
|
||||
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
|
||||
@@ -817,10 +872,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
|
||||
"\tGeoIPRule\x12\x12\n" +
|
||||
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
|
||||
"\x04code\x18\x02 \x01(\tR\x04code\x12#\n" +
|
||||
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"~\n" +
|
||||
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" +
|
||||
"\x06IPRule\x126\n" +
|
||||
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x123\n" +
|
||||
"\x06custom\x18\x02 \x01(\v2\x19.xray.common.geodata.CIDRH\x00R\x06customB\a\n" +
|
||||
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" +
|
||||
"\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" +
|
||||
"\x05valueB[\n" +
|
||||
"\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3"
|
||||
|
||||
@@ -837,7 +892,7 @@ func file_common_geodata_geodat_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_common_geodata_geodat_proto_goTypes = []any{
|
||||
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
|
||||
(*Domain)(nil), // 1: xray.common.geodata.Domain
|
||||
@@ -846,28 +901,30 @@ var file_common_geodata_geodat_proto_goTypes = []any{
|
||||
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
|
||||
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
|
||||
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
|
||||
(*GeoIP)(nil), // 7: xray.common.geodata.GeoIP
|
||||
(*GeoIPList)(nil), // 8: xray.common.geodata.GeoIPList
|
||||
(*GeoIPRule)(nil), // 9: xray.common.geodata.GeoIPRule
|
||||
(*IPRule)(nil), // 10: xray.common.geodata.IPRule
|
||||
(*Domain_Attribute)(nil), // 11: xray.common.geodata.Domain.Attribute
|
||||
(*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule
|
||||
(*GeoIP)(nil), // 8: xray.common.geodata.GeoIP
|
||||
(*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList
|
||||
(*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule
|
||||
(*IPRule)(nil), // 11: xray.common.geodata.IPRule
|
||||
(*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute
|
||||
}
|
||||
var file_common_geodata_geodat_proto_depIdxs = []int32{
|
||||
0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type
|
||||
11, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
|
||||
12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
|
||||
1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain
|
||||
2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite
|
||||
4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule
|
||||
1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain
|
||||
6, // 6: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
|
||||
7, // 7: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
|
||||
9, // 8: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
|
||||
6, // 9: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDR
|
||||
10, // [10:10] is the sub-list for method output_type
|
||||
10, // [10:10] is the sub-list for method input_type
|
||||
10, // [10:10] is the sub-list for extension type_name
|
||||
10, // [10:10] is the sub-list for extension extendee
|
||||
0, // [0:10] is the sub-list for field type_name
|
||||
6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR
|
||||
6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
|
||||
8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
|
||||
10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
|
||||
7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule
|
||||
11, // [11:11] is the sub-list for method output_type
|
||||
11, // [11:11] is the sub-list for method input_type
|
||||
11, // [11:11] is the sub-list for extension type_name
|
||||
11, // [11:11] is the sub-list for extension extendee
|
||||
0, // [0:11] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_common_geodata_geodat_proto_init() }
|
||||
@@ -879,11 +936,11 @@ func file_common_geodata_geodat_proto_init() {
|
||||
(*DomainRule_Geosite)(nil),
|
||||
(*DomainRule_Custom)(nil),
|
||||
}
|
||||
file_common_geodata_geodat_proto_msgTypes[9].OneofWrappers = []any{
|
||||
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
|
||||
(*IPRule_Geoip)(nil),
|
||||
(*IPRule_Custom)(nil),
|
||||
}
|
||||
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
|
||||
file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{
|
||||
(*Domain_Attribute_BoolValue)(nil),
|
||||
(*Domain_Attribute_IntValue)(nil),
|
||||
}
|
||||
@@ -893,7 +950,7 @@ func file_common_geodata_geodat_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 11,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -66,6 +66,11 @@ message CIDR {
|
||||
uint32 prefix = 2;
|
||||
}
|
||||
|
||||
message CIDRRule {
|
||||
CIDR cidr = 1;
|
||||
bool reverse_match = 2;
|
||||
}
|
||||
|
||||
message GeoIP {
|
||||
string code = 1;
|
||||
repeated CIDR cidr = 2;
|
||||
@@ -85,6 +90,6 @@ message GeoIPRule {
|
||||
message IPRule {
|
||||
oneof value {
|
||||
GeoIPRule geoip = 1;
|
||||
CIDR custom = 2;
|
||||
CIDRRule custom = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,8 +816,10 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
|
||||
defer f.Unlock()
|
||||
|
||||
if ipset := f.shared[key]; ipset != nil {
|
||||
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
|
||||
return ipset, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
|
||||
|
||||
ipset, err := f.createFrom(func(add func(*CIDR)) error {
|
||||
for _, r := range rules {
|
||||
@@ -915,24 +917,31 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
|
||||
return nil, errors.New("failed to build IPv6 set").Base(err)
|
||||
}
|
||||
|
||||
var has4, has6 bool
|
||||
var max4, max6 int
|
||||
|
||||
for _, p := range ipv4.Prefixes() {
|
||||
has4 = true
|
||||
if b := p.Bits(); b > max4 {
|
||||
max4 = b
|
||||
}
|
||||
}
|
||||
for _, p := range ipv6.Prefixes() {
|
||||
has6 = true
|
||||
if b := p.Bits(); b > max6 {
|
||||
max6 = b
|
||||
}
|
||||
}
|
||||
|
||||
if max4 == 0 {
|
||||
if !has4 {
|
||||
max4 = 0xff
|
||||
} else if max4 == 0 {
|
||||
max4 = 0xfe
|
||||
}
|
||||
if max6 == 0 {
|
||||
if !has6 {
|
||||
max6 = 0xff
|
||||
} else if max6 == 0 {
|
||||
max6 = 0xfe
|
||||
}
|
||||
|
||||
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
|
||||
@@ -940,45 +949,58 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
|
||||
|
||||
func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) {
|
||||
n := len(rules)
|
||||
custom := make([]*CIDR, 0, n)
|
||||
pos := make([]*GeoIPRule, 0, n)
|
||||
neg := make([]*GeoIPRule, 0, n)
|
||||
posCustom := make([]*CIDR, 0, n)
|
||||
negCustom := make([]*CIDR, 0, n)
|
||||
posGeoip := make([]*GeoIPRule, 0, n)
|
||||
negGeoip := make([]*GeoIPRule, 0, n)
|
||||
|
||||
for _, r := range rules {
|
||||
switch v := r.Value.(type) {
|
||||
case *IPRule_Custom:
|
||||
custom = append(custom, v.Custom)
|
||||
if !v.Custom.ReverseMatch {
|
||||
posCustom = append(posCustom, v.Custom.Cidr)
|
||||
} else {
|
||||
negCustom = append(negCustom, v.Custom.Cidr)
|
||||
}
|
||||
case *IPRule_Geoip:
|
||||
if !v.Geoip.ReverseMatch {
|
||||
pos = append(pos, v.Geoip)
|
||||
posGeoip = append(posGeoip, v.Geoip)
|
||||
} else {
|
||||
neg = append(neg, v.Geoip)
|
||||
negGeoip = append(negGeoip, v.Geoip)
|
||||
}
|
||||
default:
|
||||
panic("unknown ip rule type")
|
||||
}
|
||||
}
|
||||
|
||||
subs := make([]*HeuristicIPMatcher, 0, 3)
|
||||
subs := make([]*HeuristicIPMatcher, 0, 4)
|
||||
|
||||
if len(custom) > 0 {
|
||||
ipset, err := f.CreateFromCIDRs(custom)
|
||||
if len(posCustom) > 0 {
|
||||
ipset, err := f.CreateFromCIDRs(posCustom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
|
||||
}
|
||||
|
||||
if len(pos) > 0 {
|
||||
ipset, err := f.GetOrCreateFromGeoIPRules(pos)
|
||||
if len(negCustom) > 0 {
|
||||
ipset, err := f.CreateFromCIDRs(negCustom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
|
||||
}
|
||||
|
||||
if len(posGeoip) > 0 {
|
||||
ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
|
||||
}
|
||||
|
||||
if len(neg) > 0 {
|
||||
ipset, err := f.GetOrCreateFromGeoIPRules(neg)
|
||||
if len(negGeoip) > 0 {
|
||||
ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -994,3 +1016,7 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
|
||||
return &HeuristicMultiIPMatcher{matchers: subs}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newIPSetFactory() *IPSetFactory {
|
||||
return &IPSetFactory{shared: make(map[string]*IPSet)}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,90 @@ func TestIPMatcher(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPMatcherFullCIDR4(t *testing.T) {
|
||||
matcher := buildIPMatcher(
|
||||
"0.0.0.0/0",
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Output bool
|
||||
}{
|
||||
{
|
||||
Input: "192.168.1.1",
|
||||
Output: true,
|
||||
},
|
||||
{
|
||||
Input: "0.0.0.0",
|
||||
Output: true,
|
||||
},
|
||||
{
|
||||
Input: "255.255.255.255",
|
||||
Output: true,
|
||||
},
|
||||
{
|
||||
Input: "2001:cdba::3257:9652",
|
||||
Output: false,
|
||||
},
|
||||
{
|
||||
Input: "::0",
|
||||
Output: false,
|
||||
},
|
||||
{
|
||||
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
Output: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
|
||||
t.Error("unexpected output: ", v, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPMatcherFullCIDR6(t *testing.T) {
|
||||
matcher := buildIPMatcher(
|
||||
"::0/0",
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Output bool
|
||||
}{
|
||||
{
|
||||
Input: "192.168.1.1",
|
||||
Output: false,
|
||||
},
|
||||
{
|
||||
Input: "0.0.0.0",
|
||||
Output: false,
|
||||
},
|
||||
{
|
||||
Input: "255.255.255.255",
|
||||
Output: false,
|
||||
},
|
||||
{
|
||||
Input: "2001:cdba::3257:9652",
|
||||
Output: true,
|
||||
},
|
||||
{
|
||||
Input: "::0",
|
||||
Output: true,
|
||||
},
|
||||
{
|
||||
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
Output: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
|
||||
t.Error("unexpected output: ", v, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPMatcherRegression(t *testing.T) {
|
||||
matcher := buildIPMatcher(
|
||||
"98.108.20.0/22",
|
||||
@@ -189,6 +273,34 @@ func TestIPReverseMatcher2(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPCustomReverseMatcher(t *testing.T) {
|
||||
matcher := buildIPMatcher("!8.8.8.8/32")
|
||||
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Output bool
|
||||
}{
|
||||
{
|
||||
Input: "8.8.8.8",
|
||||
Output: false,
|
||||
},
|
||||
{
|
||||
Input: "1.1.1.1",
|
||||
Output: true,
|
||||
},
|
||||
{
|
||||
Input: "2001:cdba::3257:9652",
|
||||
Output: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
|
||||
t.Error("unexpected output: ", v, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
|
||||
matcher := buildIPMatcher(
|
||||
"8.8.8.8/32",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
|
||||
var ipRules []*IPRule
|
||||
|
||||
for i, r := range rules {
|
||||
r, reverse := cutReversePrefix(r)
|
||||
|
||||
if strings.HasPrefix(r, "geoip:") {
|
||||
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
|
||||
}
|
||||
@@ -32,9 +34,9 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
|
||||
var rule isIPRule_Value
|
||||
var err error
|
||||
if prefix > 0 {
|
||||
rule, err = parseGeoIPRule(r[prefix:])
|
||||
rule, err = parseGeoIPRule(r[prefix:], reverse)
|
||||
} else {
|
||||
rule, err = parseCustomIPRule(r)
|
||||
rule, err = parseCustomIPRule(r, reverse)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
|
||||
@@ -45,7 +47,16 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
|
||||
return ipRules, nil
|
||||
}
|
||||
|
||||
func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
|
||||
func cutReversePrefix(s string) (string, bool) {
|
||||
reverse := false
|
||||
for strings.HasPrefix(s, "!") {
|
||||
s = s[1:]
|
||||
reverse = !reverse
|
||||
}
|
||||
return s, reverse
|
||||
}
|
||||
|
||||
func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
|
||||
file, code, ok := strings.Cut(rule, ":")
|
||||
if !ok {
|
||||
return nil, errors.New("syntax error")
|
||||
@@ -55,11 +66,8 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
|
||||
return nil, errors.New("empty file")
|
||||
}
|
||||
|
||||
reverse := false
|
||||
if strings.HasPrefix(code, "!") {
|
||||
code = code[1:]
|
||||
reverse = true
|
||||
}
|
||||
code, codeReverse := cutReversePrefix(code)
|
||||
reverse = reverse != codeReverse
|
||||
if code == "" {
|
||||
return nil, errors.New("empty code")
|
||||
}
|
||||
@@ -78,13 +86,16 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseCustomIPRule(rule string) (*IPRule_Custom, error) {
|
||||
func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
|
||||
cidr, err := parseCIDR(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &IPRule_Custom{
|
||||
Custom: cidr,
|
||||
Custom: &CIDRRule{
|
||||
Cidr: cidr,
|
||||
ReverseMatch: reverse,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,20 @@ func TestParseIPRules(t *testing.T) {
|
||||
rules := []string{
|
||||
"geoip:us",
|
||||
"geoip:cn",
|
||||
"!geoip:cn",
|
||||
"!!geoip:cn",
|
||||
"geoip:!cn",
|
||||
"geoip:!!cn",
|
||||
"!geoip:!cn",
|
||||
"ext:geoip.dat:!cn",
|
||||
"ext:geoip.dat:!!cn",
|
||||
"ext:geoip.dat:ca",
|
||||
"ext-ip:geoip.dat:!cn",
|
||||
"ext-ip:geoip.dat:!ca",
|
||||
"192.168.0.0/24",
|
||||
"!192.168.0.0/24",
|
||||
"!!192.168.0.0/24",
|
||||
"!!!192.168.0.0/24",
|
||||
"192.168.0.1",
|
||||
"fe80::/64",
|
||||
"fe80::",
|
||||
@@ -30,6 +38,53 @@ func TestParseIPRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIPRuleReverse(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
for _, tt := range []struct {
|
||||
rule string
|
||||
reverse bool
|
||||
}{
|
||||
{rule: "!192.168.0.0/24", reverse: true},
|
||||
{rule: "!!192.168.0.0/24", reverse: false},
|
||||
{rule: "!!!192.168.0.0/24", reverse: true},
|
||||
{rule: "!!!!192.168.0.0/24", reverse: false},
|
||||
{rule: "geoip:cn", reverse: false},
|
||||
{rule: "!geoip:cn", reverse: true},
|
||||
{rule: "!!geoip:cn", reverse: false},
|
||||
{rule: "geoip:!cn", reverse: true},
|
||||
{rule: "geoip:!!cn", reverse: false},
|
||||
{rule: "!geoip:!cn", reverse: false},
|
||||
{rule: "!!geoip:!cn", reverse: true},
|
||||
{rule: "!geoip:!!cn", reverse: true},
|
||||
{rule: "ext:geoip.dat:!!!cn", reverse: true},
|
||||
} {
|
||||
t.Run(tt.rule, func(t *testing.T) {
|
||||
rules, err := geodata.ParseIPRules([]string{tt.rule})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse ip rules, got %s", err)
|
||||
}
|
||||
|
||||
if len(rules) != 1 {
|
||||
t.Fatalf("Expected 1 rule, got %d", len(rules))
|
||||
}
|
||||
|
||||
switch rule := rules[0]; {
|
||||
case rule.GetGeoip() != nil:
|
||||
if rule.GetGeoip().GetReverseMatch() != tt.reverse {
|
||||
t.Fatalf("Expected geoip reverse match to be %t", tt.reverse)
|
||||
}
|
||||
case rule.GetCustom() != nil:
|
||||
if rule.GetCustom().GetReverseMatch() != tt.reverse {
|
||||
t.Fatalf("Expected custom reverse match to be %t", tt.reverse)
|
||||
}
|
||||
default:
|
||||
t.Fatal("Expected ip rule")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainRules(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
|
||||
53
common/geodata/strmatcher/anymatcher_linear.go
Normal file
53
common/geodata/strmatcher/anymatcher_linear.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package strmatcher
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -99,10 +100,6 @@ func (t Type) New(pattern string) (Matcher, error) {
|
||||
case Substr:
|
||||
return SubstrMatcher(pattern), nil
|
||||
case Domain:
|
||||
pattern, err := ToDomain(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DomainMatcher(pattern), nil
|
||||
case Regex: // 1. regex matching is case-sensitive
|
||||
regex, err := regexp.Compile(pattern)
|
||||
@@ -253,13 +250,12 @@ func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error {
|
||||
}
|
||||
|
||||
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
|
||||
// It is designed to avoid new memory allocation as possible.
|
||||
func CompositeMatches(matches [][]uint32) []uint32 {
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return matches[0]
|
||||
return slices.Clone(matches[0])
|
||||
default:
|
||||
result := make([]uint32, 0, 5)
|
||||
for i := 0; i < len(matches); i++ {
|
||||
@@ -288,3 +284,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
|
||||
type MatcherSetForAll interface {
|
||||
AddMatcher(matcher Matcher)
|
||||
}
|
||||
|
||||
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
|
||||
type MatcherSetForFull interface {
|
||||
AddFullMatcher(matcher FullMatcher)
|
||||
}
|
||||
|
||||
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
|
||||
type MatcherSetForDomain interface {
|
||||
AddDomainMatcher(matcher DomainMatcher)
|
||||
}
|
||||
|
||||
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
|
||||
type MatcherSetForSubstr interface {
|
||||
AddSubstrMatcher(matcher SubstrMatcher)
|
||||
}
|
||||
|
||||
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
|
||||
type MatcherSetForRegex interface {
|
||||
AddRegexMatcher(matcher *RegexMatcher)
|
||||
}
|
||||
|
||||
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
|
||||
// It returns error if the MatcherSet does not accept the provided Matcher's type.
|
||||
// This function is provided to help writing code to test a MatcherSet.
|
||||
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
|
||||
if s, ok := s.(IndexMatcher); ok {
|
||||
s.Add(matcher)
|
||||
return nil
|
||||
}
|
||||
if s, ok := s.(MatcherSetForAll); ok {
|
||||
s.AddMatcher(matcher)
|
||||
return nil
|
||||
}
|
||||
switch matcher := matcher.(type) {
|
||||
case FullMatcher:
|
||||
if s, ok := s.(MatcherSetForFull); ok {
|
||||
s.AddFullMatcher(matcher)
|
||||
return nil
|
||||
}
|
||||
case DomainMatcher:
|
||||
if s, ok := s.(MatcherSetForDomain); ok {
|
||||
s.AddDomainMatcher(matcher)
|
||||
return nil
|
||||
}
|
||||
case SubstrMatcher:
|
||||
if s, ok := s.(MatcherSetForSubstr); ok {
|
||||
s.AddSubstrMatcher(matcher)
|
||||
return nil
|
||||
}
|
||||
case *RegexMatcher:
|
||||
if s, ok := s.(MatcherSetForRegex); ok {
|
||||
s.AddRegexMatcher(matcher)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("cannot add matcher to matcher set")
|
||||
}
|
||||
|
||||
79
common/geodata/strmatcher/matcherset_domain.go
Normal file
79
common/geodata/strmatcher/matcherset_domain.go
Normal file
@@ -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
|
||||
}
|
||||
95
common/geodata/strmatcher/matcherset_domain_test.go
Normal file
95
common/geodata/strmatcher/matcherset_domain_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
24
common/geodata/strmatcher/matcherset_full.go
Normal file
24
common/geodata/strmatcher/matcherset_full.go
Normal file
@@ -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
|
||||
}
|
||||
65
common/geodata/strmatcher/matcherset_full_test.go
Normal file
65
common/geodata/strmatcher/matcherset_full_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
22
common/geodata/strmatcher/matcherset_simple.go
Normal file
22
common/geodata/strmatcher/matcherset_simple.go
Normal file
@@ -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
|
||||
}
|
||||
69
common/geodata/strmatcher/matcherset_simple_test.go
Normal file
69
common/geodata/strmatcher/matcherset_simple_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
24
common/geodata/strmatcher/matcherset_substr.go
Normal file
24
common/geodata/strmatcher/matcherset_substr.go
Normal file
@@ -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
|
||||
}
|
||||
77
common/geodata/strmatcher/matcherset_substr_test.go
Normal file
77
common/geodata/strmatcher/matcherset_substr_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
)
|
||||
|
||||
// Matcher is the interface to determine a string matches a pattern.
|
||||
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
|
||||
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
|
||||
type Matcher interface {
|
||||
// Type returns the matcher's type.
|
||||
Type() Type
|
||||
@@ -62,6 +62,7 @@ type IndexMatcher interface {
|
||||
// Match returns the indices of all matchers that matches the input.
|
||||
// * Empty array is returned if no such matcher exists.
|
||||
// * The order of returned matchers should follow priority specification.
|
||||
// * The returned slice is owned by the caller and may be safely modified.
|
||||
// Priority specification:
|
||||
// 1. Priority between matcher types: full > domain > substr > regex.
|
||||
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
|
||||
@@ -89,6 +90,7 @@ type ValueMatcher interface {
|
||||
// * Empty array is returned if no such matcher exists.
|
||||
// * The order of returned values should follow priority specification.
|
||||
// * Same value may appear multiple times if multiple matched matchers were added with that value.
|
||||
// * The returned slice is owned by the caller and may be safely modified.
|
||||
// Priority specification:
|
||||
// 1. Priority between matcher types: full > domain > substr > regex.
|
||||
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
|
||||
@@ -99,3 +101,21 @@ type ValueMatcher interface {
|
||||
// MatchAny returns true as soon as one matching matcher is found.
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
|
||||
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
|
||||
// For example:
|
||||
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
|
||||
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
|
||||
type MatcherSet interface {
|
||||
// MatchAny returns true as soon as one matching matcher is found.
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
|
||||
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
|
||||
type AnyMatcher interface {
|
||||
// Add adds a new Matcher to AnyMatcher.
|
||||
Add(matcher Matcher)
|
||||
|
||||
// MatchAny returns true as soon as one matching matcher is found.
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
|
||||
@@ -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
common/platform/filesystem/file_test.go
Normal file
32
common/platform/filesystem/file_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
common/task/parallel.go
Normal file
43
common/task/parallel.go
Normal 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
common/task/parallel_test.go
Normal file
50
common/task/parallel_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/task"
|
||||
)
|
||||
|
||||
func TestParallelForN_Empty(t *testing.T) {
|
||||
called := false
|
||||
err := ParallelForN(0, func(i int) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
common.Must(err)
|
||||
if called {
|
||||
t.Fatal("fn should not be called when n=0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelForN_AllIndicesCovered(t *testing.T) {
|
||||
const N = 10000
|
||||
var seen [N]int32
|
||||
err := ParallelForN(N, func(i int) error {
|
||||
atomic.AddInt32(&seen[i], 1)
|
||||
return nil
|
||||
})
|
||||
common.Must(err)
|
||||
for i := 0; i < N; i++ {
|
||||
if seen[i] != 1 {
|
||||
t.Fatalf("index %d called %d times, expected 1", i, seen[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelForN_Error(t *testing.T) {
|
||||
boom := errors.New("boom")
|
||||
err := ParallelForN(1000, func(i int) error {
|
||||
if i == 42 {
|
||||
return boom
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != boom {
|
||||
t.Fatalf("expected %v, got %v", boom, err)
|
||||
}
|
||||
}
|
||||
73
common/utils/probe_routes.go
Normal file
73
common/utils/probe_routes.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func probeRoutes() (ipv4 bool, ipv6 bool) {
|
||||
if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil {
|
||||
ipv4 = true
|
||||
conn.Close()
|
||||
}
|
||||
if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil {
|
||||
ipv6 = true
|
||||
conn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var routeCache struct {
|
||||
sync.Once
|
||||
sync.RWMutex
|
||||
expire time.Time
|
||||
ipv4, ipv6 bool
|
||||
}
|
||||
|
||||
func CheckRoutes() (bool, bool) {
|
||||
if !isGUIPlatform {
|
||||
routeCache.Once.Do(func() {
|
||||
routeCache.ipv4, routeCache.ipv6 = probeRoutes()
|
||||
})
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
|
||||
routeCache.RWMutex.RLock()
|
||||
now := time.Now()
|
||||
if routeCache.expire.After(now) {
|
||||
routeCache.RWMutex.RUnlock()
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
routeCache.RWMutex.RUnlock()
|
||||
|
||||
routeCache.RWMutex.Lock()
|
||||
defer routeCache.RWMutex.Unlock()
|
||||
|
||||
now = time.Now()
|
||||
if routeCache.expire.After(now) { // double-check
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms
|
||||
routeCache.expire = now.Add(100 * time.Millisecond) // ttl
|
||||
return routeCache.ipv4, routeCache.ipv6
|
||||
}
|
||||
|
||||
var isGUIPlatform = detectGUIPlatform()
|
||||
|
||||
func detectGUIPlatform() bool {
|
||||
switch runtime.GOOS {
|
||||
case "android", "ios", "windows", "darwin":
|
||||
return true
|
||||
case "linux", "freebsd", "openbsd":
|
||||
if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" {
|
||||
return true
|
||||
}
|
||||
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 4
|
||||
Version_z byte = 15
|
||||
Version_y byte = 5
|
||||
Version_z byte = 3
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -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}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -54,9 +54,9 @@ func TestXrayClose(t *testing.T) {
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(0),
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
|
||||
RewritePort: uint32(0),
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -66,7 +66,7 @@ func TestXrayClose(t *testing.T) {
|
||||
Receiver: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(0),
|
||||
User: &protocol.User{
|
||||
User: &protocol.User{
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: userID.String(),
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
11
go.mod
11
go.mod
@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
|
||||
github.com/cloudflare/circl v1.6.3
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
@@ -12,8 +12,9 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pires/go-proxyproto v0.11.0
|
||||
github.com/pires/go-proxyproto v0.12.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -27,8 +28,8 @@ 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
|
||||
google.golang.org/grpc v1.80.0
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.81.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
@@ -49,7 +50,7 @@ require (
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
42
go.sum
42
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
@@ -45,14 +45,16 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/robfig/cron/v3 v3.0.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=
|
||||
@@ -70,16 +72,16 @@ github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt7
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
@@ -131,14 +133,14 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,38 +1,167 @@
|
||||
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"`
|
||||
RewriteNetwork Network `json:"rewriteNetwork"`
|
||||
RewriteAddress *Address `json:"rewriteAddress"`
|
||||
RewritePort uint16 `json:"rewritePort"`
|
||||
Network Network `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
Rules []*DNSOutboundRuleConfig `json:"rules"`
|
||||
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
|
||||
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
|
||||
}
|
||||
|
||||
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||
if len(c.Network) > 0 {
|
||||
c.RewriteNetwork = c.Network
|
||||
}
|
||||
if c.Address != nil {
|
||||
c.RewriteAddress = c.Address
|
||||
}
|
||||
if c.Port != 0 {
|
||||
c.RewritePort = c.Port
|
||||
}
|
||||
config := &dns.Config{
|
||||
Server: &net.Endpoint{
|
||||
Network: c.Network.Build(),
|
||||
Port: uint32(c.Port),
|
||||
RewriteServer: &net.Endpoint{
|
||||
Network: c.RewriteNetwork.Build(),
|
||||
Port: uint32(c.RewritePort),
|
||||
},
|
||||
UserLevel: c.UserLevel,
|
||||
}
|
||||
if c.Address != nil {
|
||||
config.Server.Address = c.Address.Build()
|
||||
if c.RewriteAddress != nil {
|
||||
config.RewriteServer.Address = c.RewriteAddress.Build()
|
||||
}
|
||||
switch c.NonIPQuery {
|
||||
case "", "reject", "drop", "skip":
|
||||
default:
|
||||
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
|
||||
|
||||
// todo: remove legacy
|
||||
if c.NonIPQuery != nil || c.BlockTypes != nil {
|
||||
if c.Rules != nil {
|
||||
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
|
||||
}
|
||||
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes"`, `"rules"`)
|
||||
rules, err := c.buildLegacyDNSPolicy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = rules
|
||||
return config, nil
|
||||
}
|
||||
config.Non_IPQuery = c.NonIPQuery
|
||||
config.BlockTypes = c.BlockTypes
|
||||
|
||||
for _, r := range c.Rules {
|
||||
rule, err := r.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = append(config.Rule, rule)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) {
|
||||
rules := make([]*dns.DNSRuleConfig, 0, 3)
|
||||
|
||||
mode := "reject"
|
||||
if c.NonIPQuery != nil && *c.NonIPQuery != "" {
|
||||
mode = *c.NonIPQuery
|
||||
}
|
||||
switch mode {
|
||||
case "", "reject", "drop", "skip":
|
||||
default:
|
||||
return nil, errors.New("unknown nonIPQuery: ", mode)
|
||||
}
|
||||
|
||||
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
|
||||
if mode == "reject" {
|
||||
rule.Action = dns.RuleAction_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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
@@ -22,12 +24,215 @@ func TestDnsProxyConfig(t *testing.T) {
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{
|
||||
RewriteServer: &net.Endpoint{
|
||||
Network: net.Network_TCP,
|
||||
Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})),
|
||||
Port: 53,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "direct",
|
||||
"qtype": "1,3,23-24"
|
||||
}, {
|
||||
"action": "drop",
|
||||
"qtype": 28,
|
||||
"domain": ["domain:example.com", "full:example.com"]
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Direct,
|
||||
Qtype: []int32{1, 3, 23, 24},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{28},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Domain,
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Full,
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "reject",
|
||||
"domain": "keyword:example"
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Substr,
|
||||
Value: "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "drop",
|
||||
"qtype": 257
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{257},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"blockTypes": []
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"blockTypes": [1, 65]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
Qtype: []int32{1, 65},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"nonIPQuery": "drop",
|
||||
"blockTypes": [1]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{1},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"nonIPQuery": "skip",
|
||||
"blockTypes": [65, 28]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
RewriteServer: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{65, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Direct,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
_, err := loadJSON(creator)(`{
|
||||
"rules": [{
|
||||
"action": "direct",
|
||||
"qtype": 65
|
||||
}],
|
||||
"blockTypes": [65]
|
||||
}`)
|
||||
if err == nil || !strings.Contains(err.Error(), `legacy nonIPQuery and blockTypes cannot be mixed with rules`) {
|
||||
t.Fatal("expected mixed legacy/new config error, but got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,39 @@ import (
|
||||
)
|
||||
|
||||
type DokodemoConfig struct {
|
||||
AllowedNetwork *NetworkList `json:"allowedNetwork"`
|
||||
RewriteAddress *Address `json:"rewriteAddress"`
|
||||
RewritePort uint16 `json:"rewritePort"`
|
||||
Network *NetworkList `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
PortMap map[string]string `json:"portMap"`
|
||||
Network *NetworkList `json:"network"`
|
||||
FollowRedirect bool `json:"followRedirect"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
}
|
||||
|
||||
func (v *DokodemoConfig) Build() (proto.Message, error) {
|
||||
config := new(dokodemo.Config)
|
||||
if v.Address != nil {
|
||||
config.Address = v.Address.Build()
|
||||
if v.Network != nil {
|
||||
v.AllowedNetwork = v.Network
|
||||
}
|
||||
config.Port = uint32(v.Port)
|
||||
if v.Address != nil {
|
||||
v.RewriteAddress = v.Address
|
||||
}
|
||||
if v.Port != 0 {
|
||||
v.RewritePort = v.Port
|
||||
}
|
||||
config := new(dokodemo.Config)
|
||||
config.AllowedNetworks = v.AllowedNetwork.Build()
|
||||
if v.RewriteAddress != nil {
|
||||
config.RewriteAddress = v.RewriteAddress.Build()
|
||||
}
|
||||
config.RewritePort = uint32(v.RewritePort)
|
||||
config.PortMap = v.PortMap
|
||||
for _, v := range config.PortMap {
|
||||
if _, _, err := net.SplitHostPort(v); err != nil {
|
||||
return nil, errors.New("invalid portMap: ", v).Base(err)
|
||||
}
|
||||
}
|
||||
config.Networks = v.Network.Build()
|
||||
config.FollowRedirect = v.FollowRedirect
|
||||
config.UserLevel = v.UserLevel
|
||||
return config, nil
|
||||
|
||||
@@ -24,15 +24,15 @@ func TestDokodemoConfig(t *testing.T) {
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dokodemo.Config{
|
||||
Address: &net.IPOrDomain{
|
||||
RewriteAddress: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
},
|
||||
},
|
||||
Port: 53,
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
FollowRedirect: true,
|
||||
UserLevel: 1,
|
||||
RewritePort: 53,
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
FollowRedirect: true,
|
||||
UserLevel: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
infra/conf/geodata.go
Normal file
71
infra/conf/geodata.go
Normal 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
infra/conf/geodata_test.go
Normal file
75
infra/conf/geodata_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ func (v *HTTPAccount) Build() *http.Account {
|
||||
}
|
||||
|
||||
type HTTPServerConfig struct {
|
||||
Users []*HTTPAccount `json:"users"`
|
||||
Accounts []*HTTPAccount `json:"accounts"`
|
||||
Transparent bool `json:"allowTransparent"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
@@ -34,9 +35,13 @@ func (c *HTTPServerConfig) Build() (proto.Message, error) {
|
||||
UserLevel: c.UserLevel,
|
||||
}
|
||||
|
||||
if len(c.Accounts) > 0 {
|
||||
if c.Accounts != nil {
|
||||
c.Users = c.Accounts
|
||||
}
|
||||
// TODO: PB
|
||||
if len(c.Users) > 0 {
|
||||
config.Accounts = make(map[string]string)
|
||||
for _, account := range c.Accounts {
|
||||
for _, account := range c.Users {
|
||||
config.Accounts[account.Username] = account.Password
|
||||
}
|
||||
}
|
||||
@@ -51,8 +56,8 @@ type HTTPRemoteConfig struct {
|
||||
}
|
||||
|
||||
type HTTPClientConfig struct {
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Level uint32 `json:"level"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"user"`
|
||||
|
||||
@@ -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"
|
||||
@@ -38,22 +39,32 @@ type HysteriaUserConfig struct {
|
||||
|
||||
type HysteriaServerConfig struct {
|
||||
Version int32 `json:"version"`
|
||||
Users []*HysteriaUserConfig `json:"clients"`
|
||||
Users []*HysteriaUserConfig `json:"users"`
|
||||
Clients []*HysteriaUserConfig `json:"clients"`
|
||||
}
|
||||
|
||||
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
|
||||
config := new(hysteria.ServerConfig)
|
||||
|
||||
if c.Users != nil {
|
||||
for _, user := range c.Users {
|
||||
account := &account.Account{
|
||||
if c.Clients != nil {
|
||||
c.Users = c.Clients
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,17 +135,15 @@ func TestRouterConfig(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||
Prefix: 128,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -216,17 +214,15 @@ func TestRouterConfig(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||
Prefix: 128,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -44,13 +45,18 @@ type ShadowsocksServerConfig struct {
|
||||
Password string `json:"password"`
|
||||
Level byte `json:"level"`
|
||||
Email string `json:"email"`
|
||||
Users []*ShadowsocksUserConfig `json:"clients"`
|
||||
Users []*ShadowsocksUserConfig `json:"users"`
|
||||
Clients []*ShadowsocksUserConfig `json:"clients"`
|
||||
NetworkList *NetworkList `json:"network"`
|
||||
}
|
||||
|
||||
func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
||||
errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption")
|
||||
|
||||
if v.Clients != nil {
|
||||
v.Users = v.Clients
|
||||
}
|
||||
|
||||
if C.Contains(shadowaead_2022.List, v.Cipher) {
|
||||
return buildShadowsocks2022(v)
|
||||
}
|
||||
@@ -59,23 +65,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 +135,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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
|
||||
type SocksServerConfig struct {
|
||||
AuthMethod string `json:"auth"`
|
||||
Users []*SocksAccount `json:"users"`
|
||||
Accounts []*SocksAccount `json:"accounts"`
|
||||
UDP bool `json:"udp"`
|
||||
Host *Address `json:"ip"`
|
||||
@@ -47,9 +48,13 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
|
||||
config.AuthType = socks.AuthType_NO_AUTH
|
||||
}
|
||||
|
||||
if len(v.Accounts) > 0 {
|
||||
config.Accounts = make(map[string]string, len(v.Accounts))
|
||||
for _, account := range v.Accounts {
|
||||
if v.Accounts != nil {
|
||||
v.Users = v.Accounts
|
||||
}
|
||||
// TODO: PB
|
||||
if len(v.Users) > 0 {
|
||||
config.Accounts = make(map[string]string, len(v.Users))
|
||||
for _, account := range v.Users {
|
||||
config.Accounts[account.Username] = account.Password
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -111,6 +112,7 @@ type TrojanUserConfig struct {
|
||||
|
||||
// TrojanServerConfig is Inbound configuration
|
||||
type TrojanServerConfig struct {
|
||||
Users []*TrojanUserConfig `json:"users"`
|
||||
Clients []*TrojanUserConfig `json:"clients"`
|
||||
Fallbacks []*TrojanInboundFallback `json:"fallbacks"`
|
||||
}
|
||||
@@ -119,13 +121,18 @@ type TrojanServerConfig struct {
|
||||
func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
||||
errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed")
|
||||
|
||||
config := &trojan.ServerConfig{
|
||||
Users: make([]*protocol.User, len(c.Clients)),
|
||||
if c.Clients != nil {
|
||||
c.Users = c.Clients
|
||||
}
|
||||
|
||||
for idx, rawUser := range c.Clients {
|
||||
config := &trojan.ServerConfig{
|
||||
Users: make([]*protocol.User, len(c.Users)),
|
||||
}
|
||||
|
||||
processClient := func(idx int) error {
|
||||
rawUser := c.Users[idx]
|
||||
if rawUser.Flow != "" {
|
||||
return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
|
||||
return errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
|
||||
}
|
||||
|
||||
config.Users[idx] = &protocol.User{
|
||||
@@ -135,6 +142,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
||||
Password: rawUser.Password,
|
||||
}),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := task.ParallelForN(len(c.Users), processClient); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, fb := range c.Fallbacks {
|
||||
|
||||
@@ -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"
|
||||
@@ -30,6 +31,7 @@ type VLessInboundFallback struct {
|
||||
}
|
||||
|
||||
type VLessInboundConfig struct {
|
||||
Users []json.RawMessage `json:"users"`
|
||||
Clients []json.RawMessage `json:"clients"`
|
||||
Decryption string `json:"decryption"`
|
||||
Fallbacks []*VLessInboundFallback `json:"fallbacks"`
|
||||
@@ -40,25 +42,30 @@ type VLessInboundConfig struct {
|
||||
// Build implements Buildable
|
||||
func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
||||
config := new(inbound.Config)
|
||||
config.Clients = make([]*protocol.User, len(c.Clients))
|
||||
|
||||
if c.Clients != nil {
|
||||
c.Users = c.Clients
|
||||
}
|
||||
config.Users = make([]*protocol.User, len(c.Users))
|
||||
switch c.Flow {
|
||||
case vless.XRV, "":
|
||||
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.Users[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 users: 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 users: invalid user`).Base(err)
|
||||
}
|
||||
|
||||
u, err := uuid.ParseString(account.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
account.Id = u.String()
|
||||
|
||||
@@ -67,7 +74,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 users: "flow" doesn't support "` + account.Flow + `" in this version`)
|
||||
}
|
||||
|
||||
if len(account.Testseed) < 4 {
|
||||
@@ -75,20 +82,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 users: "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 users: "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 users: inbound's "reverse" can't have "sniffing"`)
|
||||
}
|
||||
}
|
||||
|
||||
user.Account = serial.ToTypedMessage(account)
|
||||
config.Clients[idx] = user
|
||||
config.Users[idx] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := task.ParallelForN(len(c.Users), processClient); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Decryption = c.Decryption
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestVLessInbound(t *testing.T) {
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &inbound.Config{
|
||||
Clients: []*protocol.User{
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Account: serial.ToTypedMessage(&vless.Account{
|
||||
Id: "27848739-7e62-4138-9fd3-098a63964b6b",
|
||||
|
||||
@@ -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"
|
||||
@@ -58,7 +59,8 @@ func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig {
|
||||
}
|
||||
|
||||
type VMessInboundConfig struct {
|
||||
Users []json.RawMessage `json:"clients"`
|
||||
Users []json.RawMessage `json:"users"`
|
||||
Clients []json.RawMessage `json:"clients"`
|
||||
Defaults *VMessDefaultConfig `json:"default"`
|
||||
}
|
||||
|
||||
@@ -72,25 +74,33 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) {
|
||||
config.Default = c.Defaults.Build()
|
||||
}
|
||||
|
||||
if c.Clients != nil {
|
||||
c.Users = c.Clients
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -99,9 +99,8 @@ func TestXrayConfig(t *testing.T) {
|
||||
Ip: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{10, 0, 0, 0},
|
||||
Prefix: 8,
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -216,8 +215,12 @@ func TestSniffingConfig_Build(t *testing.T) {
|
||||
if rule == nil {
|
||||
t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i)
|
||||
}
|
||||
if !reflect.DeepEqual(rule.Ip, tc.ip) || rule.Prefix != tc.prefix {
|
||||
t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, rule.Ip, rule.Prefix, tc.ip, tc.prefix)
|
||||
cidr := rule.GetCidr()
|
||||
if cidr == nil {
|
||||
t.Fatalf("SniffingConfig.Build() produced a custom ip rule without cidr at index %d", i)
|
||||
}
|
||||
if !reflect.DeepEqual(cidr.Ip, tc.ip) || cidr.Prefix != tc.prefix {
|
||||
t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, cidr.Ip, cidr.Prefix, tc.ip, tc.prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func extractInboundUsers(inb *core.InboundHandlerConfig) []*protocol.User {
|
||||
case *vmessin.Config:
|
||||
return ty.User
|
||||
case *vlessin.Config:
|
||||
return ty.Clients
|
||||
return ty.Users
|
||||
case *trojan.ServerConfig:
|
||||
return ty.Users
|
||||
case *shadowsocks.ServerConfig:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
RewriteServer *net.Endpoint `protobuf:"bytes,3,opt,name=rewrite_server,json=rewriteServer,proto3" json:"rewrite_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) GetRewriteServer() *net.Endpoint {
|
||||
if x != nil {
|
||||
return x.BlockTypes
|
||||
return x.RewriteServer
|
||||
}
|
||||
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\"\x9c\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\x12@\n" +
|
||||
"\x0erewrite_server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\rrewriteServer*:\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.rewrite_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
|
||||
|
||||
@@ -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 rewrite_server = 3;
|
||||
}
|
||||
|
||||
166
proxy/dns/dns.go
166
proxy/dns/dns.go
@@ -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
|
||||
}
|
||||
@@ -48,10 +74,9 @@ type Handler struct {
|
||||
client dns.Client
|
||||
fdns dns.FakeDNSEngine
|
||||
ownLinkVerifier ownLinkVerifier
|
||||
server net.Destination
|
||||
rewriteServer net.Destination
|
||||
timeout time.Duration
|
||||
nonIPQuery string
|
||||
blockTypes []int32
|
||||
rules []*DNSRule
|
||||
}
|
||||
|
||||
func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error {
|
||||
@@ -62,14 +87,29 @@ func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager polic
|
||||
h.ownLinkVerifier = v
|
||||
}
|
||||
|
||||
if config.Server != nil {
|
||||
h.server = config.Server.AsDestination()
|
||||
if config.RewriteServer != nil {
|
||||
h.rewriteServer = config.RewriteServer.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)
|
||||
@@ -113,14 +161,14 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
|
||||
srcNetwork := ob.Target.Network
|
||||
|
||||
dest := ob.Target
|
||||
if h.server.Network != net.Network_Unknown {
|
||||
dest.Network = h.server.Network
|
||||
if h.rewriteServer.Network != net.Network_Unknown {
|
||||
dest.Network = h.rewriteServer.Network
|
||||
}
|
||||
if h.server.Address != nil {
|
||||
dest.Address = h.server.Address
|
||||
if h.rewriteServer.Address != nil {
|
||||
dest.Address = h.rewriteServer.Address
|
||||
}
|
||||
if h.server.Port != 0 {
|
||||
dest.Port = h.server.Port
|
||||
if h.rewriteServer.Port != 0 {
|
||||
dest.Port = h.rewriteServer.Port
|
||||
}
|
||||
|
||||
errors.LogInfo(ctx, "handling DNS traffic to ", dest)
|
||||
@@ -183,51 +231,51 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
|
||||
if err == io.EOF {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -113,9 +114,9 @@ func TestUDPDNSTunnel(t *testing.T) {
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(port),
|
||||
Networks: []net.Network{net.Network_UDP},
|
||||
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
|
||||
RewritePort: uint32(port),
|
||||
AllowedNetworks: []net.Network{net.Network_UDP},
|
||||
}),
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
@@ -232,9 +233,9 @@ func TestTCPDNSTunnel(t *testing.T) {
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(port),
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
|
||||
RewritePort: uint32(port),
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
@@ -318,9 +319,9 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(port),
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
|
||||
RewritePort: uint32(port),
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
@@ -331,7 +332,7 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
|
||||
Server: &net.Endpoint{
|
||||
RewriteServer: &net.Endpoint{
|
||||
Network: net.Network_TCP,
|
||||
},
|
||||
}),
|
||||
@@ -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{
|
||||
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP),
|
||||
RewritePort: uint32(port),
|
||||
AllowedNetworks: []net.Network{net.Network_UDP},
|
||||
}),
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
|
||||
Rule: []*dns_proxy.DNSRuleConfig{
|
||||
{
|
||||
Qtype: []int32{int32(dns.TypeA)},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Domain,
|
||||
Value: "facebook.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: dns_proxy.RuleAction_Direct,
|
||||
},
|
||||
{
|
||||
Qtype: []int32{int32(dns.TypeA)},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Full,
|
||||
Value: "google.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: dns_proxy.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v, err := core.New(config)
|
||||
common.Must(err)
|
||||
common.Must(v.Start())
|
||||
defer v.Close()
|
||||
|
||||
{
|
||||
m1 := new(dns.Msg)
|
||||
m1.Id = dns.Id()
|
||||
m1.RecursionDesired = true
|
||||
m1.Question = []dns.Question{{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
|
||||
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
common.Must(err)
|
||||
|
||||
if in.Rcode != dns.RcodeRefused {
|
||||
t.Fatal("expected Refused, but got ", in.Rcode)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
m1 := new(dns.Msg)
|
||||
m1.Id = dns.Id()
|
||||
m1.RecursionDesired = true
|
||||
m1.Question = []dns.Question{{Name: "facebook.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
|
||||
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
common.Must(err)
|
||||
|
||||
if in.Rcode != dns.RcodeSuccess {
|
||||
t.Fatal("expected Success, but got ", in.Rcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// GetPredefinedAddress returns the defined address from proto config. Null if address is not valid.
|
||||
func (v *Config) GetPredefinedAddress() net.Address {
|
||||
addr := v.Address.AsAddress()
|
||||
addr := v.RewriteAddress.AsAddress()
|
||||
if addr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,16 +23,16 @@ const (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
|
||||
Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
|
||||
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
|
||||
RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
|
||||
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
// List of networks that the Dokodemo accepts.
|
||||
Networks []net.Network `protobuf:"varint,7,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"`
|
||||
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
|
||||
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
|
||||
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
|
||||
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
@@ -65,16 +65,16 @@ func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetAddress() *net.IPOrDomain {
|
||||
func (x *Config) GetRewriteAddress() *net.IPOrDomain {
|
||||
if x != nil {
|
||||
return x.Address
|
||||
return x.RewriteAddress
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetPort() uint32 {
|
||||
func (x *Config) GetRewritePort() uint32 {
|
||||
if x != nil {
|
||||
return x.Port
|
||||
return x.RewritePort
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -86,9 +86,9 @@ func (x *Config) GetPortMap() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetNetworks() []net.Network {
|
||||
func (x *Config) GetAllowedNetworks() []net.Network {
|
||||
if x != nil {
|
||||
return x.Networks
|
||||
return x.AllowedNetworks
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -111,12 +111,12 @@ var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_dokodemo_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xd2\x02\n" +
|
||||
"\x06Config\x125\n" +
|
||||
"\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" +
|
||||
"\x04port\x18\x02 \x01(\rR\x04port\x12C\n" +
|
||||
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x124\n" +
|
||||
"\bnetworks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x12'\n" +
|
||||
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" +
|
||||
"\x06Config\x12D\n" +
|
||||
"\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" +
|
||||
"\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" +
|
||||
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\n" +
|
||||
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12'\n" +
|
||||
"\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" +
|
||||
"\n" +
|
||||
"user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" +
|
||||
@@ -145,9 +145,9 @@ var file_proxy_dokodemo_config_proto_goTypes = []any{
|
||||
(net.Network)(0), // 3: xray.common.net.Network
|
||||
}
|
||||
var file_proxy_dokodemo_config_proto_depIdxs = []int32{
|
||||
2, // 0: xray.proxy.dokodemo.Config.address:type_name -> xray.common.net.IPOrDomain
|
||||
2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
|
||||
1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
|
||||
3, // 2: xray.proxy.dokodemo.Config.networks:type_name -> xray.common.net.Network
|
||||
3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
|
||||
@@ -10,14 +10,11 @@ import "common/net/address.proto";
|
||||
import "common/net/network.proto";
|
||||
|
||||
message Config {
|
||||
xray.common.net.IPOrDomain address = 1;
|
||||
uint32 port = 2;
|
||||
|
||||
map<string, string> port_map = 3;
|
||||
|
||||
// List of networks that the Dokodemo accepts.
|
||||
repeated xray.common.net.Network networks = 7;
|
||||
|
||||
repeated xray.common.net.Network allowed_networks = 7;
|
||||
xray.common.net.IPOrDomain rewrite_address = 1;
|
||||
uint32 rewrite_port = 2;
|
||||
map<string, string> port_map = 3;
|
||||
bool follow_redirect = 5;
|
||||
uint32 user_level = 6;
|
||||
}
|
||||
|
||||
@@ -32,22 +32,22 @@ func init() {
|
||||
}
|
||||
|
||||
type DokodemoDoor struct {
|
||||
policyManager policy.Manager
|
||||
config *Config
|
||||
address net.Address
|
||||
port net.Port
|
||||
portMap map[string]string
|
||||
sockopt *session.Sockopt
|
||||
policyManager policy.Manager
|
||||
config *Config
|
||||
rewriteAddress net.Address
|
||||
rewritePort net.Port
|
||||
portMap map[string]string
|
||||
sockopt *session.Sockopt
|
||||
}
|
||||
|
||||
// Init initializes the DokodemoDoor instance with necessary parameters.
|
||||
func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.Sockopt) error {
|
||||
if len(config.Networks) == 0 {
|
||||
if len(config.AllowedNetworks) == 0 {
|
||||
return errors.New("no network specified")
|
||||
}
|
||||
d.config = config
|
||||
d.address = config.GetPredefinedAddress()
|
||||
d.port = net.Port(config.Port)
|
||||
d.rewriteAddress = config.GetPredefinedAddress()
|
||||
d.rewritePort = net.Port(config.RewritePort)
|
||||
d.portMap = config.PortMap
|
||||
d.policyManager = pm
|
||||
d.sockopt = sockopt
|
||||
@@ -57,10 +57,10 @@ func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.
|
||||
|
||||
// Network implements proxy.Inbound.
|
||||
func (d *DokodemoDoor) Network() []net.Network {
|
||||
if slices.Contains(d.config.Networks, net.Network_TCP) {
|
||||
return append(d.config.Networks, net.Network_UNIX)
|
||||
if slices.Contains(d.config.AllowedNetworks, net.Network_TCP) {
|
||||
return append(d.config.AllowedNetworks, net.Network_UNIX)
|
||||
}
|
||||
return d.config.Networks
|
||||
return d.config.AllowedNetworks
|
||||
}
|
||||
|
||||
func (d *DokodemoDoor) policy() policy.Session {
|
||||
@@ -78,8 +78,8 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st
|
||||
}
|
||||
dest := net.Destination{
|
||||
Network: network,
|
||||
Address: d.address,
|
||||
Port: d.port,
|
||||
Address: d.rewriteAddress,
|
||||
Port: d.rewritePort,
|
||||
}
|
||||
|
||||
if !d.config.FollowRedirect {
|
||||
@@ -95,7 +95,7 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st
|
||||
}
|
||||
}
|
||||
}
|
||||
if dest.Port == 0 {
|
||||
if dest.Port == 0 && port != "" {
|
||||
dest.Port = net.Port(common.Must2(strconv.Atoi(port)))
|
||||
}
|
||||
if d.portMap != nil && d.portMap[port] != "" {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package freedom
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
span := max - min
|
||||
if max < min {
|
||||
span = min - max
|
||||
}
|
||||
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
|
||||
}
|
||||
|
||||
func isValidAddress(addr *net.IPOrDomain) bool {
|
||||
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,9 @@ 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
|
||||
firstResolve := true
|
||||
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||
dialDest := destination
|
||||
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
|
||||
@@ -183,7 +304,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
||||
if h.config.DomainStrategy.ForceIP() {
|
||||
if h.config.DomainStrategy.ForceIP() || h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -194,6 +315,36 @@ 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
|
||||
domain := dialDest.Address.Domain()
|
||||
var ips []net.IP
|
||||
if firstResolve {
|
||||
firstResolve = false
|
||||
supportIPv4, supportIPv6 := utils.CheckRoutes()
|
||||
if supportIPv4 {
|
||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip4", domain)
|
||||
}
|
||||
if len(ips) == 0 && supportIPv6 {
|
||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip6", domain)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return errors.New("failed to get IP address for domain ", domain)
|
||||
}
|
||||
} else {
|
||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip", domain)
|
||||
}
|
||||
if len(ips) == 0 { // SRV/TXT, lookup failed
|
||||
return errors.New("failed to get IP address for domain ", domain)
|
||||
}
|
||||
if addr := net.IPAddress(ips[dice.Roll(len(ips))]); addr != nil {
|
||||
dialDest.Address = addr
|
||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
||||
}
|
||||
}
|
||||
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &dialDest
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
|
||||
rawConn, err := dialer.Dial(ctx, dialDest)
|
||||
@@ -207,9 +358,20 @@ 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
|
||||
}
|
||||
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
|
||||
version := byte(h.config.ProxyProtocol)
|
||||
@@ -255,7 +417,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 +452,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
if destination.Network == net.Network_TCP {
|
||||
reader = buf.NewReader(conn)
|
||||
} else {
|
||||
reader = NewPacketReader(conn, UDPOverride, destination)
|
||||
reader = NewPacketReader(conn, 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 +471,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {
|
||||
func NewPacketReader(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {
|
||||
iConn := conn
|
||||
statConn, ok := iConn.(*stat.CounterConnection)
|
||||
if ok {
|
||||
@@ -328,6 +490,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
|
||||
return &PacketReader{
|
||||
PacketConnWrapper: c,
|
||||
Counter: counter,
|
||||
Handler: h,
|
||||
DefaultRule: defaultRule,
|
||||
IsOverridden: isOverridden,
|
||||
InitUnchangedAddr: DialDest.Address,
|
||||
InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address,
|
||||
@@ -339,6 +503,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
|
||||
type PacketReader struct {
|
||||
*internet.PacketConnWrapper
|
||||
stats.Counter
|
||||
Handler *Handler
|
||||
DefaultRule *FinalRule
|
||||
IsOverridden bool
|
||||
InitUnchangedAddr net.Address
|
||||
InitChangedAddr net.Address
|
||||
@@ -347,33 +513,40 @@ type PacketReader struct {
|
||||
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
b := buf.New()
|
||||
b.Resize(0, buf.Size)
|
||||
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
|
||||
if err != nil {
|
||||
b.Release()
|
||||
return nil, err
|
||||
}
|
||||
b.Resize(0, int32(n))
|
||||
// if udp dest addr is changed, we are unable to get the correct src addr
|
||||
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
|
||||
if !r.IsOverridden {
|
||||
address := net.IPAddress(d.(*net.UDPAddr).IP)
|
||||
if r.InitChangedAddr == address {
|
||||
address = r.InitUnchangedAddr
|
||||
for {
|
||||
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
|
||||
if err != nil {
|
||||
b.Release()
|
||||
return nil, err
|
||||
}
|
||||
b.UDP = &net.Destination{
|
||||
Address: address,
|
||||
Port: net.Port(d.(*net.UDPAddr).Port),
|
||||
Network: net.Network_UDP,
|
||||
udpAddr := d.(*net.UDPAddr)
|
||||
sourceAddr := net.IPAddress(udpAddr.IP)
|
||||
if 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
|
||||
if !r.IsOverridden {
|
||||
if r.InitChangedAddr == sourceAddr {
|
||||
sourceAddr = r.InitUnchangedAddr
|
||||
}
|
||||
b.UDP = &net.Destination{
|
||||
Address: sourceAddr,
|
||||
Port: net.Port(udpAddr.Port),
|
||||
Network: net.Network_UDP,
|
||||
}
|
||||
}
|
||||
if r.Counter != nil {
|
||||
r.Counter.Add(int64(n))
|
||||
}
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
if r.Counter != nil {
|
||||
r.Counter.Add(int64(n))
|
||||
}
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
// DialDest means the dial target used in the dialer when creating conn
|
||||
func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, DialDest net.Destination, 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 {
|
||||
@@ -394,7 +567,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,
|
||||
@@ -408,8 +581,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
|
||||
@@ -467,11 +640,9 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if isBlockedAddress(w.BlockedIPMatcher, b.UDP.Address) {
|
||||
blockedAddr := b.UDP.Address
|
||||
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block {
|
||||
b.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return errors.New("blocked target IP: ", blockedAddr).AtDebug()
|
||||
continue
|
||||
}
|
||||
destAddr := b.UDP.RawNetAddr()
|
||||
if destAddr == nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -4,13 +4,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/net/cnc"
|
||||
"github.com/xtls/xray-core/common/retry"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
@@ -32,81 +27,24 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
|
||||
destination := ob.Target
|
||||
|
||||
errors.LogInfo(ctx, "opening connection to ", destination)
|
||||
content := new(session.Content)
|
||||
content.SkipDNSResolve = true
|
||||
|
||||
input := link.Reader
|
||||
output := link.Writer
|
||||
ctx = session.ContextWithContent(ctx, content)
|
||||
inbound := &session.Inbound{}
|
||||
originInbound := session.InboundFromContext(ctx)
|
||||
if originInbound != nil {
|
||||
// get a shallow copy to avoid modifying the inbound tag in upstream context
|
||||
*inbound = *originInbound
|
||||
}
|
||||
inbound.Tag = l.config.InboundTag
|
||||
ctx = session.ContextWithInbound(ctx, inbound)
|
||||
|
||||
var conn net.Conn
|
||||
err := retry.ExponentialBackoff(2, 100).On(func() error {
|
||||
dialDest := destination
|
||||
|
||||
content := new(session.Content)
|
||||
content.SkipDNSResolve = true
|
||||
|
||||
ctx = session.ContextWithContent(ctx, content)
|
||||
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
if inbound == nil {
|
||||
inbound = &session.Inbound{}
|
||||
}
|
||||
|
||||
inbound.Tag = l.config.InboundTag
|
||||
|
||||
ctx = session.ContextWithInbound(ctx, inbound)
|
||||
|
||||
rawConn, err := l.dispatcherInstance.Dispatch(ctx, dialDest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var readerOpt cnc.ConnectionOption
|
||||
if dialDest.Network == net.Network_TCP {
|
||||
readerOpt = cnc.ConnectionOutputMulti(rawConn.Reader)
|
||||
} else {
|
||||
readerOpt = cnc.ConnectionOutputMultiUDP(rawConn.Reader)
|
||||
}
|
||||
|
||||
conn = cnc.NewConnection(cnc.ConnectionInputMulti(rawConn.Writer), readerOpt)
|
||||
return nil
|
||||
})
|
||||
err := l.dispatcherInstance.DispatchLink(ctx, destination, link)
|
||||
if err != nil {
|
||||
return errors.New("failed to open connection to ", destination).Base(err)
|
||||
errors.New(ctx, "failed to process loopback connection").Base(err)
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
requestDone := func() error {
|
||||
var writer buf.Writer
|
||||
if destination.Network == net.Network_TCP {
|
||||
writer = buf.NewWriter(conn)
|
||||
} else {
|
||||
writer = &buf.SequentialWriter{Writer: conn}
|
||||
}
|
||||
|
||||
if err := buf.Copy(input, writer); err != nil {
|
||||
return errors.New("failed to process request").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
var reader buf.Reader
|
||||
if destination.Network == net.Network_TCP {
|
||||
reader = buf.NewReader(conn)
|
||||
} else {
|
||||
reader = buf.NewPacketReader(conn)
|
||||
}
|
||||
if err := buf.Copy(reader, output); err != nil {
|
||||
return errors.New("failed to process response").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := task.Run(ctx, requestDone, task.OnSuccess(responseDone, task.Close(output))); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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
proxy/tun/icmp/packet.go
Normal file
106
proxy/tun/icmp/packet.go
Normal 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user