Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3774d05ad | |||
| 528b50dbb8 | |||
| 940de4a789 | |||
| 505fa325e4 | |||
| 6dbcc3d1e2 | |||
| 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 | |||
| c5edc122b7 | |||
| 9dd17d55fb | |||
| 310b764811 | |||
| 3691741440 | |||
| 05e259c8e4 | |||
| 175502d807 | |||
| 6780045550 | |||
| ff6126463b | |||
| 5c3d639c09 | |||
| 7c56b7beea | |||
| 5b91b152bb | |||
| ef77a42063 | |||
| dab99614dc |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -111,6 +111,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)
|
||||
@@ -119,6 +121,7 @@
|
||||
- [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 +146,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
|
||||
}
|
||||
|
||||
|
||||
+38
-27
@@ -158,9 +158,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var domainMatcher geodata.DomainMatcher
|
||||
if len(effectiveRules) > 0 {
|
||||
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no DNS client in config, add a `localhost` DNS client
|
||||
@@ -271,24 +274,27 @@ func (s *DNS) sortClients(domain string) []*Client {
|
||||
|
||||
// Priority domain matching
|
||||
hasMatch := false
|
||||
MatchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||
sort.Slice(MatchSlice, func(i, j int) bool {
|
||||
return MatchSlice[i] < MatchSlice[j]
|
||||
})
|
||||
for _, match := range MatchSlice {
|
||||
info := s.matcherInfos[match]
|
||||
client := s.clients[info.clientIdx]
|
||||
domainRule := info.domainRule
|
||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||
if clientUsed[info.clientIdx] {
|
||||
continue
|
||||
}
|
||||
clientUsed[info.clientIdx] = true
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
hasMatch = true
|
||||
if client.finalQuery {
|
||||
return clients
|
||||
if s.domainMatcher != nil {
|
||||
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||
sort.Slice(matchSlice, func(i, j int) bool {
|
||||
return matchSlice[i] < matchSlice[j]
|
||||
})
|
||||
for _, match := range matchSlice {
|
||||
info := s.matcherInfos[match]
|
||||
client := s.clients[info.clientIdx]
|
||||
domainRule := info.domainRule
|
||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||
if clientUsed[info.clientIdx] {
|
||||
continue
|
||||
}
|
||||
clientUsed[info.clientIdx] = true
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
hasMatch = true
|
||||
if client.finalQuery {
|
||||
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||
return clients
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,17 +308,13 @@ func (s *DNS) sortClients(domain string) []*Client {
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
if client.finalQuery {
|
||||
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||
return clients
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(domainRules) > 0 {
|
||||
errors.LogDebug(s.ctx, "domain ", domain, " matches following rules: ", domainRules)
|
||||
}
|
||||
if len(clientNames) > 0 {
|
||||
errors.LogDebug(s.ctx, "domain ", domain, " will use DNS in order: ", clientNames)
|
||||
}
|
||||
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||
|
||||
if len(clients) == 0 {
|
||||
if len(s.clients) > 0 {
|
||||
@@ -327,6 +329,15 @@ func (s *DNS) sortClients(domain string) []*Client {
|
||||
return clients
|
||||
}
|
||||
|
||||
func logDecision(ctx context.Context, domain string, domainRules []string, clientNames []string) {
|
||||
if len(domainRules) > 0 {
|
||||
errors.LogDebug(ctx, "domain ", domain, " matches following rules: ", domainRules)
|
||||
}
|
||||
if len(clientNames) > 0 {
|
||||
errors.LogDebug(ctx, "domain ", domain, " will use DNS in order: ", clientNames)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeQueryErrors(domain string, errs []error) error {
|
||||
if len(errs) == 0 {
|
||||
return dns.ErrEmptyResponse
|
||||
|
||||
+36
-76
@@ -147,7 +147,9 @@ func TestUDPServerSubnet(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -207,7 +209,9 @@ func TestUDPServer(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -345,7 +349,9 @@ func TestPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -414,7 +420,9 @@ func TestUDPServerIPv6(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -481,7 +489,9 @@ func TestStaticHostDomain(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -538,15 +548,8 @@ func TestIPMatch(t *testing.T) {
|
||||
Port: uint32(port),
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
// inner ip, will not match
|
||||
Ip: []byte{192, 168, 11, 1},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
// inner ip, will not match
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
// second dns, match ip
|
||||
@@ -561,22 +564,8 @@ func TestIPMatch(t *testing.T) {
|
||||
Port: uint32(port),
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 4},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -587,7 +576,9 @@ func TestIPMatch(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -664,9 +655,9 @@ func TestLocalDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will match localhost, localhost-a and localhost-b,
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -704,7 +695,9 @@ func TestLocalDomain(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -887,22 +880,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will only match 8.8.8.8 and 8.8.4.4
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 8},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 4, 4},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -922,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will match 8.8.8.8 and 8.8.8.7, etc
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 8, 7},
|
||||
Prefix: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -949,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will only match 8.8.7.7 (api.google.com)
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 7, 7},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -976,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
ExpectedIp: []*geodata.IPRule{
|
||||
// Will only match 8.8.7.8 (v2.api.google.com)
|
||||
{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDR{
|
||||
Ip: []byte{8, 8, 7, 8},
|
||||
Prefix: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -994,7 +952,9 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+8
-12
@@ -17,7 +17,7 @@ import (
|
||||
type Holder struct {
|
||||
domainToIP cache.Lru
|
||||
ipRange *net.IPNet
|
||||
mu *sync.Mutex
|
||||
mu sync.Mutex
|
||||
|
||||
config *FakeDnsPool
|
||||
}
|
||||
@@ -49,9 +49,7 @@ func (fkdns *Holder) Start() error {
|
||||
}
|
||||
|
||||
func (fkdns *Holder) Close() error {
|
||||
fkdns.domainToIP = nil
|
||||
fkdns.ipRange = nil
|
||||
fkdns.mu = nil
|
||||
// nothing to do for now, just wait GC
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -70,7 +68,7 @@ func NewFakeDNSHolder() (*Holder, error) {
|
||||
}
|
||||
|
||||
func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) {
|
||||
return &Holder{nil, nil, nil, conf}, nil
|
||||
return &Holder{config: conf}, nil
|
||||
}
|
||||
|
||||
func (fkdns *Holder) initializeFromConfig() error {
|
||||
@@ -92,7 +90,6 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
|
||||
}
|
||||
fkdns.domainToIP = cache.NewLru(lruSize)
|
||||
fkdns.ipRange = ipRange
|
||||
fkdns.mu = new(sync.Mutex)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -103,7 +100,7 @@ func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
|
||||
if v, ok := fkdns.domainToIP.Get(domain); ok {
|
||||
return []net.Address{v.(net.Address)}
|
||||
}
|
||||
currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
|
||||
currentTimeMillis := uint64(time.Now().UnixMilli())
|
||||
ones, bits := fkdns.ipRange.Mask.Size()
|
||||
rooms := bits - ones
|
||||
if rooms < 64 {
|
||||
@@ -202,12 +199,11 @@ func (h *HolderMulti) Start() error {
|
||||
}
|
||||
|
||||
func (h *HolderMulti) Close() error {
|
||||
var errs []error
|
||||
for _, v := range h.holders {
|
||||
if err := v.Close(); err != nil {
|
||||
return errors.New("Cannot close all fake dns pools").Base(err)
|
||||
}
|
||||
errs = append(errs, v.Close())
|
||||
}
|
||||
return nil
|
||||
return errors.Combine(errs...)
|
||||
}
|
||||
|
||||
func (h *HolderMulti) createHolderGroups() error {
|
||||
@@ -222,7 +218,7 @@ func (h *HolderMulti) createHolderGroups() error {
|
||||
}
|
||||
|
||||
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
|
||||
holderMulti := &HolderMulti{nil, conf}
|
||||
holderMulti := &HolderMulti{config: conf}
|
||||
if err := holderMulti.createHolderGroups(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+14
-11
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -267,6 +267,20 @@ func (h *Handler) DestIpAddress() net.IP {
|
||||
return internet.DestIpAddress()
|
||||
}
|
||||
|
||||
func (h *Handler) SocketSettings() *internet.SocketConfig {
|
||||
if h.streamSettings == nil {
|
||||
return nil
|
||||
}
|
||||
return h.streamSettings.SocketSettings
|
||||
}
|
||||
|
||||
func (h *Handler) UsesProxySettings() bool {
|
||||
if h.senderSettings != nil && h.senderSettings.ProxySettings.HasTag() {
|
||||
return true
|
||||
}
|
||||
return h.streamSettings != nil && h.streamSettings.SocketSettings != nil && len(h.streamSettings.SocketSettings.DialerProxy) > 0
|
||||
}
|
||||
|
||||
// Dial implements internet.Dialer.
|
||||
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
||||
if h.senderSettings != nil {
|
||||
|
||||
@@ -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,60 +136,58 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
|
||||
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
|
||||
continue
|
||||
}
|
||||
g.Add(m, 0)
|
||||
s.Add(m)
|
||||
}
|
||||
f.shared[key] = g
|
||||
return g, err
|
||||
f.shared[key] = s
|
||||
return s, err
|
||||
}
|
||||
|
||||
// BuildMatcher implements DomainMatcherFactory.
|
||||
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||
if len(rules) == 0 {
|
||||
return nil, errors.New("empty domain rule list")
|
||||
}
|
||||
compact := &CompactDomainMatcher{
|
||||
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
|
||||
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
|
||||
values: make([]uint32, 0, len(rules)),
|
||||
}
|
||||
custom := strmatcher.NewLinearValueMatcher()
|
||||
var idx uint32
|
||||
for _, r := range rules {
|
||||
for i, r := range rules {
|
||||
switch v := r.Value.(type) {
|
||||
case *DomainRule_Custom:
|
||||
m, err := parseDomain(v.Custom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
custom.Add(m, 0)
|
||||
if compact.custom == nil {
|
||||
compact.custom = strmatcher.NewLinearValueMatcher()
|
||||
}
|
||||
compact.custom.Add(m, uint32(i))
|
||||
case *DomainRule_Geosite:
|
||||
m, err := f.getOrCreateFrom(v.Geosite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compact.matchers = append(compact.matchers, m)
|
||||
compact.values = append(compact.values, idx)
|
||||
idx++
|
||||
compact.values = append(compact.values, uint32(i))
|
||||
default:
|
||||
panic("unknown domain rule type")
|
||||
}
|
||||
}
|
||||
if len(compact.matchers) != len(rules) {
|
||||
compact.matchers = append(compact.matchers, custom)
|
||||
compact.values = append(compact.values, idx+1)
|
||||
}
|
||||
return compact, nil
|
||||
}
|
||||
|
||||
type CompactDomainMatcher struct {
|
||||
matchers []strmatcher.MatcherGroup
|
||||
custom strmatcher.ValueMatcher
|
||||
matchers []strmatcher.MatcherSet
|
||||
values []uint32
|
||||
}
|
||||
|
||||
func (c *CompactDomainMatcher) Add(matcher strmatcher.MatcherGroup, value uint32) {
|
||||
c.matchers = append(c.matchers, matcher)
|
||||
c.values = append(c.values, value)
|
||||
}
|
||||
|
||||
// Match implements DomainMatcher.
|
||||
func (c *CompactDomainMatcher) Match(input string) []uint32 {
|
||||
result := make([]uint32, 0)
|
||||
var result []uint32
|
||||
if c.custom != nil {
|
||||
result = append(result, c.custom.Match(input)...)
|
||||
}
|
||||
for i, m := range c.matchers {
|
||||
if m.MatchAny(input) {
|
||||
result = append(result, c.values[i])
|
||||
@@ -147,6 +198,9 @@ func (c *CompactDomainMatcher) Match(input string) []uint32 {
|
||||
|
||||
// MatchAny implements DomainMatcher.
|
||||
func (c *CompactDomainMatcher) MatchAny(input string) bool {
|
||||
if c.custom != nil && c.custom.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
for _, m := range c.matchers {
|
||||
if m.MatchAny(input) {
|
||||
return true
|
||||
@@ -175,9 +229,9 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
||||
|
||||
func newDomainMatcherFactory() DomainMatcherFactory {
|
||||
switch runtime.GOOS {
|
||||
case "ios":
|
||||
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
||||
case "ios", "android":
|
||||
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||
default:
|
||||
return &MphDomainMatcherFactory{}
|
||||
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
||||
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildMatcher() failed: %v", err)
|
||||
}
|
||||
|
||||
got := matcher.Match("example.com")
|
||||
slices.Sort(got)
|
||||
|
||||
want := []uint32{0, 1}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Match() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildMatcher() failed: %v", err)
|
||||
}
|
||||
|
||||
got := matcher.Match("163.com")
|
||||
slices.Sort(got)
|
||||
|
||||
want := []uint32{0, 1}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Match() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
||||
matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildMatcher() failed: %v", err)
|
||||
}
|
||||
|
||||
got := matcher.Match("example.com")
|
||||
if !reflect.DeepEqual(got, []uint32{0, 1}) {
|
||||
t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1})
|
||||
}
|
||||
|
||||
got[0] = 1
|
||||
|
||||
gotAgain := matcher.Match("example.com")
|
||||
if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) {
|
||||
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,59 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
type DomainRegistry struct {
|
||||
factory DomainMatcherFactory
|
||||
mu sync.Mutex
|
||||
factory DomainMatcherFactory
|
||||
matchers []*DynamicDomainMatcher
|
||||
}
|
||||
|
||||
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||
return r.factory.BuildMatcher(rules)
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
m, err := r.factory.BuildMatcher(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := NewDynamicDomainMatcher(rules, m)
|
||||
r.matchers = append(r.matchers, d)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (r *DomainRegistry) Reload() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
|
||||
|
||||
factory := newDomainMatcherFactory()
|
||||
type reloadEntry struct {
|
||||
dynamic *DynamicDomainMatcher
|
||||
matcher DomainMatcher
|
||||
}
|
||||
reloaded := make([]reloadEntry, len(r.matchers))
|
||||
for i, d := range r.matchers {
|
||||
m, err := factory.BuildMatcher(d.rules)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
|
||||
return err
|
||||
}
|
||||
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
|
||||
}
|
||||
for _, entry := range reloaded {
|
||||
entry.dynamic.Reload(entry.matcher)
|
||||
}
|
||||
r.factory = factory
|
||||
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func newDomainRegistry() *DomainRegistry {
|
||||
@@ -15,3 +63,32 @@ func newDomainRegistry() *DomainRegistry {
|
||||
}
|
||||
|
||||
var DomainReg = newDomainRegistry()
|
||||
|
||||
type domainMatcherState struct {
|
||||
matcher DomainMatcher
|
||||
}
|
||||
|
||||
type DynamicDomainMatcher struct {
|
||||
rules []*DomainRule
|
||||
state atomic.Pointer[domainMatcherState]
|
||||
}
|
||||
|
||||
// Match implements DomainMatcher.
|
||||
func (d *DynamicDomainMatcher) Match(input string) []uint32 {
|
||||
return d.state.Load().matcher.Match(input)
|
||||
}
|
||||
|
||||
// MatchAny implements DomainMatcher.
|
||||
func (d *DynamicDomainMatcher) MatchAny(input string) bool {
|
||||
return d.state.Load().matcher.MatchAny(input)
|
||||
}
|
||||
|
||||
func (d *DynamicDomainMatcher) Reload(newMatcher DomainMatcher) {
|
||||
d.state.Store(&domainMatcherState{matcher: newMatcher})
|
||||
}
|
||||
|
||||
func NewDynamicDomainMatcher(rules []*DomainRule, matcher DomainMatcher) *DynamicDomainMatcher {
|
||||
d := &DynamicDomainMatcher{rules: rules}
|
||||
d.Reload(matcher)
|
||||
return d
|
||||
}
|
||||
|
||||
+96
-39
@@ -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"))
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package strmatcher
|
||||
|
||||
type trieNode2 struct {
|
||||
matched bool
|
||||
children map[string]*trieNode2
|
||||
}
|
||||
|
||||
// DomainMatcherSet is an implementation of MatcherSet.
|
||||
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
|
||||
type DomainMatcherSet struct {
|
||||
root *trieNode2
|
||||
}
|
||||
|
||||
func NewDomainMatcherSet() *DomainMatcherSet {
|
||||
return &DomainMatcherSet{
|
||||
root: new(trieNode2),
|
||||
}
|
||||
}
|
||||
|
||||
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
|
||||
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
|
||||
node := s.root
|
||||
pattern := matcher.Pattern()
|
||||
for i := len(pattern); i > 0; {
|
||||
var part string
|
||||
for j := i - 1; ; j-- {
|
||||
if pattern[j] == '.' {
|
||||
part = pattern[j+1 : i]
|
||||
i = j
|
||||
break
|
||||
}
|
||||
if j == 0 {
|
||||
part = pattern[j:i]
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if node.children == nil {
|
||||
node.children = make(map[string]*trieNode2)
|
||||
}
|
||||
next := node.children[part]
|
||||
if next == nil {
|
||||
next = new(trieNode2)
|
||||
node.children[part] = next
|
||||
}
|
||||
node = next
|
||||
}
|
||||
|
||||
node.matched = true
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherSet.MatchAny.
|
||||
func (s *DomainMatcherSet) MatchAny(input string) bool {
|
||||
node := s.root
|
||||
for i := len(input); i > 0; {
|
||||
for j := i - 1; ; j-- {
|
||||
if input[j] == '.' {
|
||||
node = node.children[input[j+1:i]]
|
||||
i = j
|
||||
break
|
||||
}
|
||||
if j == 0 {
|
||||
node = node.children[input[j:i]]
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
if node.matched {
|
||||
return true
|
||||
}
|
||||
if node.children == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestDomainMatcherSet(t *testing.T) {
|
||||
patterns := []struct {
|
||||
Pattern string
|
||||
}{
|
||||
{
|
||||
Pattern: "example.com",
|
||||
},
|
||||
{
|
||||
Pattern: "google.com",
|
||||
},
|
||||
{
|
||||
Pattern: "x.a.com",
|
||||
},
|
||||
{
|
||||
Pattern: "a.b.com",
|
||||
},
|
||||
{
|
||||
Pattern: "c.a.b.com",
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
Domain string
|
||||
Result bool
|
||||
}{
|
||||
{
|
||||
Domain: "x.example.com",
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
Domain: "y.com",
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Domain: "a.b.com",
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
Domain: "c.a.b.com",
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
Domain: "c.a..b.com",
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Domain: ".com",
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Domain: "com",
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Domain: "",
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Domain: "x.y.com",
|
||||
Result: true,
|
||||
},
|
||||
}
|
||||
s := NewDomainMatcherSet()
|
||||
for _, pattern := range patterns {
|
||||
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
r := s.MatchAny(testCase.Domain)
|
||||
if !reflect.DeepEqual(r, testCase.Result) {
|
||||
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyDomainMatcherSet(t *testing.T) {
|
||||
s := NewDomainMatcherSet()
|
||||
r := s.MatchAny("example.com")
|
||||
if r {
|
||||
t.Error("Expect false, but ", r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package strmatcher
|
||||
|
||||
// FullMatcherSet is an implementation of MatcherSet.
|
||||
// It uses a hash table to facilitate exact match lookup.
|
||||
type FullMatcherSet struct {
|
||||
matchers map[string]struct{}
|
||||
}
|
||||
|
||||
func NewFullMatcherSet() *FullMatcherSet {
|
||||
return &FullMatcherSet{
|
||||
matchers: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
|
||||
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
|
||||
s.matchers[matcher.Pattern()] = struct{}{}
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherSet.Any.
|
||||
func (s *FullMatcherSet) MatchAny(input string) bool {
|
||||
_, found := s.matchers[input]
|
||||
return found
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestFullMatcherSet(t *testing.T) {
|
||||
patterns := []struct {
|
||||
Pattern string
|
||||
}{
|
||||
{
|
||||
Pattern: "example.com",
|
||||
},
|
||||
{
|
||||
Pattern: "google.com",
|
||||
},
|
||||
{
|
||||
Pattern: "x.a.com",
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
Domain string
|
||||
Result bool
|
||||
}{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
Domain: "y.com",
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Domain: "x.y.com",
|
||||
Result: true,
|
||||
},
|
||||
}
|
||||
s := NewFullMatcherSet()
|
||||
for _, pattern := range patterns {
|
||||
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
r := s.MatchAny(testCase.Domain)
|
||||
if !reflect.DeepEqual(r, testCase.Result) {
|
||||
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFullMatcherSet(t *testing.T) {
|
||||
s := NewFullMatcherSet()
|
||||
r := s.MatchAny("example.com")
|
||||
if r {
|
||||
t.Error("Expect false, but ", r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package strmatcher
|
||||
|
||||
// SimpleMatcherSet is an implementation of MatcherSet.
|
||||
// It simply stores all matchers in an array and sequentially matches them.
|
||||
type SimpleMatcherSet struct {
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
// AddMatcher implements MatcherSetForAll.AddMatcher.
|
||||
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
|
||||
s.matchers = append(s.matchers, matcher)
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherSet.MatchAny.
|
||||
func (s *SimpleMatcherSet) MatchAny(input string) bool {
|
||||
for _, m := range s.matchers {
|
||||
if m.Match(input) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestSimpleMatcherSet(t *testing.T) {
|
||||
patterns := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Regex,
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
input string
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
input: "www.example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "www.e3ample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: "xample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: "xexample.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "examplexcom",
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
matcherSet := &SimpleMatcherSet{}
|
||||
for _, entry := range patterns {
|
||||
matcher, err := entry.mType.New(entry.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToSet(matcherSet, matcher))
|
||||
}
|
||||
for _, test := range cases {
|
||||
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
|
||||
t.Error("unexpected output: ", r, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package strmatcher
|
||||
|
||||
import "strings"
|
||||
|
||||
// SubstrMatcherSet is implementation of MatcherSet,
|
||||
// It is simply implmeneted to comply with the priority specification of Substr matchers.
|
||||
type SubstrMatcherSet struct {
|
||||
patterns []string
|
||||
}
|
||||
|
||||
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
|
||||
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
|
||||
s.patterns = append(s.patterns, matcher.Pattern())
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherSet.MatchAny.
|
||||
func (s *SubstrMatcherSet) MatchAny(input string) bool {
|
||||
for _, pattern := range s.patterns {
|
||||
if strings.Contains(input, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestSubstrMatcherSet(t *testing.T) {
|
||||
patterns := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "apis",
|
||||
mType: Substr,
|
||||
},
|
||||
{
|
||||
pattern: "google",
|
||||
mType: Substr,
|
||||
},
|
||||
{
|
||||
pattern: "apis",
|
||||
mType: Substr,
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
input string
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
input: "google.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "apis.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "googleapis.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "fonts.googleapis.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "apis.googleapis.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: "baidu.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: "goog",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: "api",
|
||||
output: false,
|
||||
},
|
||||
}
|
||||
matcherSet := &SubstrMatcherSet{}
|
||||
for _, entry := range patterns {
|
||||
matcher, err := entry.mType.New(entry.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToSet(matcherSet, matcher))
|
||||
}
|
||||
for _, test := range cases {
|
||||
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
|
||||
t.Error("unexpected output: ", r, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
)
|
||||
|
||||
func TestGetInstance(t *testing.T) {
|
||||
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
|
||||
t.Error("expect nil, but actually not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) {
|
||||
msg := &custom.UDPConfig{
|
||||
Mode: "standalone",
|
||||
Client: []*custom.UDPItem{
|
||||
{Rand: 12, Save: "txid"},
|
||||
},
|
||||
}
|
||||
|
||||
tm := ToTypedMessage(msg)
|
||||
if tm == nil {
|
||||
t.Fatal("expected typed message")
|
||||
}
|
||||
|
||||
roundTrip, err := tm.GetInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("GetInstance() failed: %v", err)
|
||||
}
|
||||
|
||||
udp, ok := roundTrip.(*custom.UDPConfig)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected round-trip type: %T", roundTrip)
|
||||
}
|
||||
|
||||
if udp.GetMode() != "standalone" {
|
||||
t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -53,7 +53,7 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
return
|
||||
}
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
|
||||
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun" || inbound.Name == "wireguard") {
|
||||
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
|
||||
h := blake3.New(8, BaseKey)
|
||||
h.Write([]byte(inbound.Source.String()))
|
||||
copy(globalID[:], h.Sum(nil))
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 4
|
||||
Version_z byte = 13
|
||||
Version_z byte = 25
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -52,7 +52,9 @@ func TestXrayDial(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -102,7 +104,9 @@ func TestXrayDialUDPConn(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -169,7 +173,9 @@ func TestXrayDialUDP(t *testing.T) {
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -80,13 +80,17 @@ func New() *Client {
|
||||
d := &net.Dialer{
|
||||
Timeout: time.Second * 16,
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
var errs []error
|
||||
for _, ctl := range internet.Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
err := errors.Combine(errs...)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,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,7 +28,7 @@ require (
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
|
||||
@@ -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=
|
||||
@@ -131,8 +133,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||
|
||||
+129
-12
@@ -1,19 +1,70 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type DNSOutboundRuleConfig struct {
|
||||
Action string `json:"action"`
|
||||
QType *PortList `json:"qtype"`
|
||||
Domain *StringList `json:"domain"`
|
||||
}
|
||||
|
||||
func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
|
||||
rule := &dns.DNSRuleConfig{}
|
||||
|
||||
switch strings.ToLower(c.Action) {
|
||||
case "direct":
|
||||
rule.Action = dns.RuleAction_Direct
|
||||
case "drop":
|
||||
rule.Action = dns.RuleAction_Drop
|
||||
case "reject":
|
||||
rule.Action = dns.RuleAction_Reject
|
||||
case "hijack":
|
||||
rule.Action = dns.RuleAction_Hijack
|
||||
default:
|
||||
return nil, errors.New("unknown action: ", c.Action)
|
||||
}
|
||||
|
||||
if c.QType != nil {
|
||||
for _, r := range c.QType.Range {
|
||||
if r.From > r.To {
|
||||
return nil, errors.New("invalid qtype range: ", r.String())
|
||||
}
|
||||
if r.To > 65535 {
|
||||
return nil, errors.New("dns rule qtype out of range: ", r.String())
|
||||
}
|
||||
for qtype := r.From; qtype <= r.To; qtype++ {
|
||||
rule.Qtype = append(rule.Qtype, int32(qtype))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Domain != nil {
|
||||
rules, err := geodata.ParseDomainRules(*c.Domain, geodata.Domain_Substr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule.Domain = rules
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
type DNSOutboundConfig struct {
|
||||
Network Network `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
NonIPQuery string `json:"nonIPQuery"`
|
||||
BlockTypes []int32 `json:"blockTypes"`
|
||||
Network Network `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
Rules []*DNSOutboundRuleConfig `json:"rules"`
|
||||
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
|
||||
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
|
||||
}
|
||||
|
||||
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||
@@ -27,12 +78,78 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||
if c.Address != nil {
|
||||
config.Server.Address = c.Address.Build()
|
||||
}
|
||||
switch c.NonIPQuery {
|
||||
case "", "reject", "drop", "skip":
|
||||
default:
|
||||
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
|
||||
|
||||
// todo: remove legacy
|
||||
if c.NonIPQuery != nil || c.BlockTypes != nil {
|
||||
if c.Rules != nil {
|
||||
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
|
||||
}
|
||||
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes"`, `"rules"`)
|
||||
rules, err := c.buildLegacyDNSPolicy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = rules
|
||||
return config, nil
|
||||
}
|
||||
config.Non_IPQuery = c.NonIPQuery
|
||||
config.BlockTypes = c.BlockTypes
|
||||
|
||||
for _, r := range c.Rules {
|
||||
rule, err := r.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = append(config.Rule, rule)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) {
|
||||
rules := make([]*dns.DNSRuleConfig, 0, 3)
|
||||
|
||||
mode := "reject"
|
||||
if c.NonIPQuery != nil && *c.NonIPQuery != "" {
|
||||
mode = *c.NonIPQuery
|
||||
}
|
||||
switch mode {
|
||||
case "", "reject", "drop", "skip":
|
||||
default:
|
||||
return nil, errors.New("unknown nonIPQuery: ", mode)
|
||||
}
|
||||
|
||||
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
|
||||
if mode == "reject" {
|
||||
rule.Action = dns.RuleAction_Reject
|
||||
}
|
||||
for _, qtype := range *c.BlockTypes {
|
||||
if qtype < 0 || qtype > 65535 {
|
||||
return nil, errors.New("legacy blockTypes qtype out of range: ", qtype)
|
||||
}
|
||||
rule.Qtype = append(rule.Qtype, qtype)
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
{
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack}
|
||||
rule.Qtype = append(rule.Qtype, 1)
|
||||
rule.Qtype = append(rule.Qtype, 28)
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
{
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject}
|
||||
if mode == "reject" {
|
||||
rule.Action = dns.RuleAction_Reject
|
||||
} else if mode == "drop" {
|
||||
rule.Action = dns.RuleAction_Drop
|
||||
} else if mode == "skip" {
|
||||
rule.Action = dns.RuleAction_Direct
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
@@ -29,5 +31,208 @@ func TestDnsProxyConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "direct",
|
||||
"qtype": "1,3,23-24"
|
||||
}, {
|
||||
"action": "drop",
|
||||
"qtype": 28,
|
||||
"domain": ["domain:example.com", "full:example.com"]
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Direct,
|
||||
Qtype: []int32{1, 3, 23, 24},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{28},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Domain,
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Full,
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "reject",
|
||||
"domain": "keyword:example"
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Substr,
|
||||
Value: "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "drop",
|
||||
"qtype": 257
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{257},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"blockTypes": []
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"blockTypes": [1, 65]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
Qtype: []int32{1, 65},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"nonIPQuery": "drop",
|
||||
"blockTypes": [1]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{1},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"nonIPQuery": "skip",
|
||||
"blockTypes": [65, 28]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{65, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Direct,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
_, err := loadJSON(creator)(`{
|
||||
"rules": [{
|
||||
"action": "direct",
|
||||
"qtype": 65
|
||||
}],
|
||||
"blockTypes": [65]
|
||||
}`)
|
||||
if err == nil || !strings.Contains(err.Error(), `legacy nonIPQuery and blockTypes cannot be mixed with rules`) {
|
||||
t.Fatal("expected mixed legacy/new config error, but got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
+77
-11
@@ -1,13 +1,15 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
v2net "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
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"
|
||||
@@ -15,14 +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"`
|
||||
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 {
|
||||
@@ -39,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 == "" {
|
||||
@@ -140,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)
|
||||
}
|
||||
@@ -156,12 +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
|
||||
}
|
||||
|
||||
for _, r := range c.FinalRules {
|
||||
rule, err := r.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.FinalRules = append(config.FinalRules, rule)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -220,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},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/app/geodata"
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
)
|
||||
|
||||
func TestGeodataConfig(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
creator := func() Buildable {
|
||||
return new(GeodataConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"cron": "0 4 * * *",
|
||||
"outbound": "proxy",
|
||||
"assets": [
|
||||
{"url": "https://example.com/geoip.dat", "file": "geoip.dat"},
|
||||
{"url": "https://example.com/geosite.dat", "file": "geosite.dat"}
|
||||
]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &geodata.Config{
|
||||
Cron: "0 4 * * *",
|
||||
Outbound: "proxy",
|
||||
Assets: []*geodata.Asset{
|
||||
{Url: "https://example.com/geoip.dat", File: "geoip.dat"},
|
||||
{Url: "https://example.com/geosite.dat", File: "geosite.dat"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGeodataAssetConfig(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
if _, err := (&GeodataAssetConfig{
|
||||
URL: "https://example.com/geoip.dat",
|
||||
File: "geoip.dat",
|
||||
}).Build(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := (&GeodataAssetConfig{
|
||||
URL: "https://example.com/geoip.dat",
|
||||
File: "missing.dat",
|
||||
}).Build(); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeodataAssetConfigInvalidURL(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
for _, rawURL := range []string{
|
||||
"",
|
||||
"http://example.com/geoip.dat",
|
||||
"ftp://example.com/geoip.dat",
|
||||
"https:///geoip.dat",
|
||||
} {
|
||||
if _, err := (&GeodataAssetConfig{
|
||||
URL: rawURL,
|
||||
File: "geoip.dat",
|
||||
}).Build(); err == nil {
|
||||
t.Fatalf("expected error for %q", rawURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-6
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/proxy/hysteria"
|
||||
"github.com/xtls/xray-core/proxy/hysteria/account"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -44,16 +45,22 @@ type HysteriaServerConfig struct {
|
||||
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
|
||||
config := new(hysteria.ServerConfig)
|
||||
|
||||
if c.Users != nil {
|
||||
for _, user := range c.Users {
|
||||
account := &account.Account{
|
||||
if len(c.Users) > 0 {
|
||||
config.Users = make([]*protocol.User, len(c.Users))
|
||||
processUser := func(idx int) error {
|
||||
user := c.Users[idx]
|
||||
acc := &account.Account{
|
||||
Auth: user.Auth,
|
||||
}
|
||||
config.Users = append(config.Users, &protocol.User{
|
||||
config.Users[idx] = &protocol.User{
|
||||
Email: user.Email,
|
||||
Level: user.Level,
|
||||
Account: serial.ToTypedMessage(account),
|
||||
})
|
||||
Account: serial.ToTypedMessage(acc),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+34
-19
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -59,23 +60,31 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
||||
config.Network = v.NetworkList.Build()
|
||||
|
||||
if v.Users != nil {
|
||||
for _, user := range v.Users {
|
||||
account := &shadowsocks.Account{
|
||||
Password: user.Password,
|
||||
CipherType: cipherFromString(user.Cipher),
|
||||
if len(v.Users) > 0 {
|
||||
config.Users = make([]*protocol.User, len(v.Users))
|
||||
processUser := func(idx int) error {
|
||||
user := v.Users[idx]
|
||||
account := &shadowsocks.Account{
|
||||
Password: user.Password,
|
||||
CipherType: cipherFromString(user.Cipher),
|
||||
}
|
||||
if account.Password == "" {
|
||||
return errors.New("Shadowsocks password is not specified.")
|
||||
}
|
||||
if account.CipherType < shadowsocks.CipherType_AES_128_GCM ||
|
||||
account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 {
|
||||
return errors.New("unsupported cipher method: ", user.Cipher)
|
||||
}
|
||||
config.Users[idx] = &protocol.User{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
Account: serial.ToTypedMessage(account),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if account.Password == "" {
|
||||
return nil, errors.New("Shadowsocks password is not specified.")
|
||||
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if account.CipherType < shadowsocks.CipherType_AES_128_GCM ||
|
||||
account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 {
|
||||
return nil, errors.New("unsupported cipher method: ", user.Cipher)
|
||||
}
|
||||
config.Users = append(config.Users, &protocol.User{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
Account: serial.ToTypedMessage(account),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
account := &shadowsocks.Account{
|
||||
@@ -121,18 +130,24 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
||||
config.Key = v.Password
|
||||
config.Network = v.NetworkList.Build()
|
||||
|
||||
for _, user := range v.Users {
|
||||
config.Users = make([]*protocol.User, len(v.Users))
|
||||
processUser := func(idx int) error {
|
||||
user := v.Users[idx]
|
||||
if user.Cipher != "" {
|
||||
return nil, errors.New("shadowsocks 2022 (multi-user): users must have empty method")
|
||||
return errors.New("shadowsocks 2022 (multi-user): users must have empty method")
|
||||
}
|
||||
account := &shadowsocks_2022.Account{
|
||||
Key: user.Password,
|
||||
}
|
||||
config.Users = append(config.Users, &protocol.User{
|
||||
config.Users[idx] = &protocol.User{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
Account: serial.ToTypedMessage(account),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -1231,6 +1232,8 @@ func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
|
||||
}
|
||||
|
||||
var (
|
||||
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||
"fragment": func() interface{} { return new(FragmentMask) },
|
||||
@@ -1256,11 +1259,14 @@ var (
|
||||
)
|
||||
|
||||
type TCPItem struct {
|
||||
Delay Int32Range `json:"delay"`
|
||||
Rand int32 `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Type string `json:"type"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
Delay Int32Range `json:"delay"`
|
||||
Rand int32 `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Capture string `json:"capture"`
|
||||
Type string `json:"type"`
|
||||
Reuse string `json:"reuse"`
|
||||
Transform *CustomTransform `json:"transform"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
}
|
||||
|
||||
type HeaderCustomTCP struct {
|
||||
@@ -1272,22 +1278,22 @@ type HeaderCustomTCP struct {
|
||||
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
for _, value := range c.Clients {
|
||||
for _, item := range value {
|
||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, value := range c.Servers {
|
||||
for _, item := range value {
|
||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, value := range c.Errors {
|
||||
for _, item := range value {
|
||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1308,6 +1314,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
@@ -1315,6 +1325,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1333,6 +1346,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
@@ -1340,6 +1357,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1358,6 +1378,10 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
@@ -1365,6 +1389,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1471,26 +1498,185 @@ func (c *NoiseMask) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
type UDPItem struct {
|
||||
Rand int32 `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Type string `json:"type"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
Rand int32 `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Capture string `json:"capture"`
|
||||
Type string `json:"type"`
|
||||
Reuse string `json:"reuse"`
|
||||
Transform *CustomTransform `json:"transform"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
}
|
||||
|
||||
type CustomTransform struct {
|
||||
Op string `json:"op"`
|
||||
Args []CustomTransformArg `json:"args"`
|
||||
}
|
||||
|
||||
type CustomTransformArg struct {
|
||||
Type string `json:"type"`
|
||||
Bytes json.RawMessage `json:"bytes"`
|
||||
U64 *uint64 `json:"u64"`
|
||||
Reuse string `json:"reuse"`
|
||||
Metadata string `json:"metadata"`
|
||||
Transform *CustomTransform `json:"transform"`
|
||||
}
|
||||
|
||||
func validateCustomVarName(name string) error {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if !customVarNamePattern.MatchString(name) {
|
||||
return errors.New("invalid variable name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error {
|
||||
if err := validateCustomVarName(capture); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateCustomVarName(reuse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kindCount := 0
|
||||
if len(packet) > 0 {
|
||||
kindCount++
|
||||
}
|
||||
if rand > 0 {
|
||||
kindCount++
|
||||
}
|
||||
if reuse != "" {
|
||||
kindCount++
|
||||
}
|
||||
if transform != nil {
|
||||
kindCount++
|
||||
}
|
||||
if kindCount > 1 {
|
||||
return errors.New("exactly one item kind must be set")
|
||||
}
|
||||
if kindCount == 0 && capture != "" {
|
||||
return errors.New("exactly one item kind must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) {
|
||||
if transform == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if transform.Op == "" {
|
||||
return nil, errors.New("transform op is required")
|
||||
}
|
||||
if len(transform.Args) == 0 {
|
||||
return nil, errors.New("transform args are required")
|
||||
}
|
||||
|
||||
args := make([]*custom.ExprArg, 0, len(transform.Args))
|
||||
for _, arg := range transform.Args {
|
||||
parsedArg, err := buildCustomTransformArg(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, parsedArg)
|
||||
}
|
||||
|
||||
return &custom.Expr{
|
||||
Op: transform.Op,
|
||||
Args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) {
|
||||
kindCount := 0
|
||||
if len(arg.Bytes) > 0 {
|
||||
kindCount++
|
||||
}
|
||||
if arg.U64 != nil {
|
||||
kindCount++
|
||||
}
|
||||
if arg.Reuse != "" {
|
||||
kindCount++
|
||||
}
|
||||
if arg.Metadata != "" {
|
||||
kindCount++
|
||||
}
|
||||
if arg.Transform != nil {
|
||||
kindCount++
|
||||
}
|
||||
if kindCount != 1 {
|
||||
return nil, errors.New("transform arg must set exactly one value")
|
||||
}
|
||||
|
||||
if len(arg.Bytes) > 0 {
|
||||
value, err := PraseByteSlice(arg.Bytes, arg.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Bytes{
|
||||
Bytes: value,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if arg.U64 != nil {
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_U64{
|
||||
U64: *arg.U64,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if arg.Reuse != "" {
|
||||
if err := validateCustomVarName(arg.Reuse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Var{
|
||||
Var: arg.Reuse,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if arg.Metadata != "" {
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Metadata{
|
||||
Metadata: arg.Metadata,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
parsedExpr, err := buildCustomTransform(arg.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Expr{
|
||||
Expr: parsedExpr,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type HeaderCustomUDP struct {
|
||||
Mode string `json:"mode"`
|
||||
Client []UDPItem `json:"client"`
|
||||
Server []UDPItem `json:"server"`
|
||||
}
|
||||
|
||||
func (c *HeaderCustomUDP) Build() (proto.Message, error) {
|
||||
switch c.Mode {
|
||||
case "", "prefix", "standalone":
|
||||
default:
|
||||
return nil, errors.New("unknown udp mode")
|
||||
}
|
||||
|
||||
for _, item := range c.Client {
|
||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, item := range c.Server {
|
||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1506,11 +1692,18 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) {
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client = append(client, &custom.UDPItem{
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1526,17 +1719,25 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) {
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server = append(server, &custom.UDPItem{
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
|
||||
return &custom.UDPConfig{
|
||||
Client: client,
|
||||
Server: server,
|
||||
Mode: c.Mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -156,3 +158,137 @@ func TestSocketConfig(t *testing.T) {
|
||||
t.Fatalf("unexpected parsed TFO value, which should be -1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderCustomUDPBuild(t *testing.T) {
|
||||
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"client": [
|
||||
{
|
||||
"type": "hex",
|
||||
"packet": "aabb"
|
||||
},
|
||||
{
|
||||
"rand": 2,
|
||||
"capture": "seed",
|
||||
"randRange": "16-32"
|
||||
}
|
||||
],
|
||||
"server": [
|
||||
{
|
||||
"capture": "txid",
|
||||
"transform": {
|
||||
"op": "concat",
|
||||
"args": [
|
||||
{"reuse": "seed"},
|
||||
{"u64": 258},
|
||||
{"type": "hex", "bytes": "c0de"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"reuse": "txid"
|
||||
}
|
||||
],
|
||||
"mode": "standalone"
|
||||
}`,
|
||||
Parser: parser,
|
||||
Output: &finalmaskcustom.UDPConfig{
|
||||
Mode: "standalone",
|
||||
Client: []*finalmaskcustom.UDPItem{
|
||||
{
|
||||
RandMax: 255,
|
||||
Packet: []byte{0xAA, 0xBB},
|
||||
},
|
||||
{
|
||||
Rand: 2,
|
||||
RandMin: 16,
|
||||
RandMax: 32,
|
||||
Save: "seed",
|
||||
},
|
||||
},
|
||||
Server: []*finalmaskcustom.UDPItem{
|
||||
{
|
||||
RandMax: 255,
|
||||
Save: "txid",
|
||||
Expr: &finalmaskcustom.Expr{
|
||||
Op: "concat",
|
||||
Args: []*finalmaskcustom.ExprArg{
|
||||
{
|
||||
Value: &finalmaskcustom.ExprArg_Var{
|
||||
Var: "seed",
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &finalmaskcustom.ExprArg_U64{
|
||||
U64: 258,
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &finalmaskcustom.ExprArg_Bytes{
|
||||
Bytes: []byte{0xC0, 0xDE},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
RandMax: 255,
|
||||
Var: "txid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestHeaderCustomTCPBuildRejectsMixedItemKinds(t *testing.T) {
|
||||
parser := loadJSON(func() Buildable { return new(HeaderCustomTCP) })
|
||||
|
||||
_, err := parser(`{
|
||||
"clients": [[
|
||||
{
|
||||
"packet": [1, 2],
|
||||
"reuse": "txid"
|
||||
}
|
||||
]]
|
||||
}`)
|
||||
if err == nil || !strings.Contains(err.Error(), "exactly one item kind") {
|
||||
t.Fatalf("expected mixed item kind rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderCustomUDPBuildRejectsInvalidVariableNames(t *testing.T) {
|
||||
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
|
||||
|
||||
_, err := parser(`{
|
||||
"client": [
|
||||
{
|
||||
"capture": "bad-name",
|
||||
"rand": 4
|
||||
}
|
||||
]
|
||||
}`)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid variable name") {
|
||||
t.Fatalf("expected invalid variable name rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) {
|
||||
parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) })
|
||||
|
||||
_, err := parser(`{
|
||||
"client": [
|
||||
{
|
||||
"transform": {
|
||||
"op": "concat"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
if err == nil || !strings.Contains(err.Error(), "transform args") {
|
||||
t.Fatalf("expected transform arg rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/proxy/trojan"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -123,9 +124,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
||||
Users: make([]*protocol.User, len(c.Clients)),
|
||||
}
|
||||
|
||||
for idx, rawUser := range c.Clients {
|
||||
processClient := func(idx int) error {
|
||||
rawUser := c.Clients[idx]
|
||||
if rawUser.Flow != "" {
|
||||
return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
|
||||
return errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
|
||||
}
|
||||
|
||||
config.Users[idx] = &protocol.User{
|
||||
@@ -135,6 +137,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
||||
Password: rawUser.Password,
|
||||
}),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := task.ParallelForN(len(c.Clients), processClient); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, fb := range c.Fallbacks {
|
||||
|
||||
+3
-6
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
type TunConfig struct {
|
||||
Name string `json:"name"`
|
||||
MTU []uint32 `json:"mtu"`
|
||||
MTU uint32 `json:"mtu"`
|
||||
Gateway []string `json:"gateway"`
|
||||
DNS []string `json:"dns"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
@@ -34,11 +34,8 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
if config.Name == "" {
|
||||
config.Name = "xray0"
|
||||
}
|
||||
if len(config.MTU) == 0 {
|
||||
config.MTU = []uint32{1500, 1280}
|
||||
}
|
||||
if len(config.MTU) == 1 {
|
||||
config.MTU = append(config.MTU, config.MTU[0])
|
||||
if config.MTU == 0 {
|
||||
config.MTU = 1500
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
+15
-8
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
"github.com/xtls/xray-core/proxy/vless/inbound"
|
||||
@@ -46,19 +47,20 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
||||
default:
|
||||
return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`)
|
||||
}
|
||||
for idx, rawUser := range c.Clients {
|
||||
processClient := func(idx int) error {
|
||||
rawUser := c.Clients[idx]
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||
return nil, errors.New(`VLESS clients: invalid user`).Base(err)
|
||||
return errors.New(`VLESS clients: invalid user`).Base(err)
|
||||
}
|
||||
account := new(vless.Account)
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, errors.New(`VLESS clients: invalid user`).Base(err)
|
||||
return errors.New(`VLESS clients: invalid user`).Base(err)
|
||||
}
|
||||
|
||||
u, err := uuid.ParseString(account.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
account.Id = u.String()
|
||||
|
||||
@@ -67,7 +69,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
||||
account.Flow = c.Flow
|
||||
case vless.XRV:
|
||||
default:
|
||||
return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`)
|
||||
return errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`)
|
||||
}
|
||||
|
||||
if len(account.Testseed) < 4 {
|
||||
@@ -75,20 +77,25 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
if account.Encryption != "" {
|
||||
return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
|
||||
return errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
|
||||
}
|
||||
|
||||
if account.Reverse != nil {
|
||||
if account.Reverse.Tag == "" {
|
||||
return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`)
|
||||
return errors.New(`VLESS clients: "tag" can't be empty for "reverse"`)
|
||||
}
|
||||
if account.Reverse.Sniffing != nil { // may not be reached: error json unmarshal
|
||||
return nil, errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`)
|
||||
return errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`)
|
||||
}
|
||||
}
|
||||
|
||||
user.Account = serial.ToTypedMessage(account)
|
||||
config.Clients[idx] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := task.ParallelForN(len(c.Clients), processClient); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Decryption = c.Decryption
|
||||
|
||||
+10
-4
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/proxy/vmess"
|
||||
"github.com/xtls/xray-core/proxy/vmess/inbound"
|
||||
@@ -73,24 +74,29 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
config.User = make([]*protocol.User, len(c.Users))
|
||||
for idx, rawData := range c.Users {
|
||||
processUser := func(idx int) error {
|
||||
rawData := c.Users[idx]
|
||||
user := new(protocol.User)
|
||||
if err := json.Unmarshal(rawData, user); err != nil {
|
||||
return nil, errors.New("invalid VMess user").Base(err)
|
||||
return errors.New("invalid VMess user").Base(err)
|
||||
}
|
||||
account := new(VMessAccount)
|
||||
if err := json.Unmarshal(rawData, account); err != nil {
|
||||
return nil, errors.New("invalid VMess user").Base(err)
|
||||
return errors.New("invalid VMess user").Base(err)
|
||||
}
|
||||
|
||||
u, err := uuid.ParseString(account.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
account.ID = u.String()
|
||||
|
||||
user.Account = serial.ToTypedMessage(account.Build())
|
||||
config.User[idx] = user
|
||||
return nil
|
||||
}
|
||||
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+162
-41
@@ -7,6 +7,7 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
geodata "github.com/xtls/xray-core/common/geodata"
|
||||
net "github.com/xtls/xray-core/common/net"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
@@ -22,21 +23,130 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type RuleAction int32
|
||||
|
||||
const (
|
||||
RuleAction_Direct RuleAction = 0
|
||||
RuleAction_Drop RuleAction = 1
|
||||
RuleAction_Reject RuleAction = 2
|
||||
RuleAction_Hijack RuleAction = 3
|
||||
)
|
||||
|
||||
// Enum value maps for RuleAction.
|
||||
var (
|
||||
RuleAction_name = map[int32]string{
|
||||
0: "Direct",
|
||||
1: "Drop",
|
||||
2: "Reject",
|
||||
3: "Hijack",
|
||||
}
|
||||
RuleAction_value = map[string]int32{
|
||||
"Direct": 0,
|
||||
"Drop": 1,
|
||||
"Reject": 2,
|
||||
"Hijack": 3,
|
||||
}
|
||||
)
|
||||
|
||||
func (x RuleAction) Enum() *RuleAction {
|
||||
p := new(RuleAction)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x RuleAction) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (RuleAction) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_proxy_dns_config_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (RuleAction) Type() protoreflect.EnumType {
|
||||
return &file_proxy_dns_config_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x RuleAction) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RuleAction.Descriptor instead.
|
||||
func (RuleAction) EnumDescriptor() ([]byte, []int) {
|
||||
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type DNSRuleConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.dns.RuleAction" json:"action,omitempty"`
|
||||
Qtype []int32 `protobuf:"varint,2,rep,packed,name=qtype,proto3" json:"qtype,omitempty"`
|
||||
Domain []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DNSRuleConfig) Reset() {
|
||||
*x = DNSRuleConfig{}
|
||||
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DNSRuleConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DNSRuleConfig) ProtoMessage() {}
|
||||
|
||||
func (x *DNSRuleConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DNSRuleConfig.ProtoReflect.Descriptor instead.
|
||||
func (*DNSRuleConfig) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *DNSRuleConfig) GetAction() RuleAction {
|
||||
if x != nil {
|
||||
return x.Action
|
||||
}
|
||||
return RuleAction_Direct
|
||||
}
|
||||
|
||||
func (x *DNSRuleConfig) GetQtype() []int32 {
|
||||
if x != nil {
|
||||
return x.Qtype
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *DNSRuleConfig) GetDomain() []*geodata.DomainRule {
|
||||
if x != nil {
|
||||
return x.Domain
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Server is the DNS server address. If specified, this address overrides the
|
||||
// original one.
|
||||
Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
||||
UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
Non_IPQuery string `protobuf:"bytes,3,opt,name=non_IP_query,json=nonIPQuery,proto3" json:"non_IP_query,omitempty"`
|
||||
BlockTypes []int32 `protobuf:"varint,4,rep,packed,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
Rule []*DNSRuleConfig `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
|
||||
Server *net.Endpoint `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
||||
mi := &file_proxy_dns_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -48,7 +158,7 @@ func (x *Config) String() string {
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
||||
mi := &file_proxy_dns_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -61,14 +171,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetServer() *net.Endpoint {
|
||||
if x != nil {
|
||||
return x.Server
|
||||
}
|
||||
return nil
|
||||
return file_proxy_dns_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetUserLevel() uint32 {
|
||||
@@ -78,16 +181,16 @@ func (x *Config) GetUserLevel() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetNon_IPQuery() string {
|
||||
func (x *Config) GetRule() []*DNSRuleConfig {
|
||||
if x != nil {
|
||||
return x.Non_IPQuery
|
||||
return x.Rule
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetBlockTypes() []int32 {
|
||||
func (x *Config) GetServer() *net.Endpoint {
|
||||
if x != nil {
|
||||
return x.BlockTypes
|
||||
return x.Server
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -96,15 +199,25 @@ var File_proxy_dns_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_dns_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" +
|
||||
"\x06Config\x121\n" +
|
||||
"\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\n" +
|
||||
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" +
|
||||
"\rDNSRuleConfig\x122\n" +
|
||||
"\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" +
|
||||
"\x05qtype\x18\x02 \x03(\x05R\x05qtype\x127\n" +
|
||||
"\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\"\x8d\x01\n" +
|
||||
"\x06Config\x12\x1d\n" +
|
||||
"\n" +
|
||||
"user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" +
|
||||
"\fnon_IP_query\x18\x03 \x01(\tR\n" +
|
||||
"nonIPQuery\x12\x1f\n" +
|
||||
"\vblock_types\x18\x04 \x03(\x05R\n" +
|
||||
"blockTypesBL\n" +
|
||||
"user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" +
|
||||
"\x04rule\x18\x02 \x03(\v2\x1d.xray.proxy.dns.DNSRuleConfigR\x04rule\x121\n" +
|
||||
"\x06server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\x06server*:\n" +
|
||||
"\n" +
|
||||
"RuleAction\x12\n" +
|
||||
"\n" +
|
||||
"\x06Direct\x10\x00\x12\b\n" +
|
||||
"\x04Drop\x10\x01\x12\n" +
|
||||
"\n" +
|
||||
"\x06Reject\x10\x02\x12\n" +
|
||||
"\n" +
|
||||
"\x06Hijack\x10\x03BL\n" +
|
||||
"\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -119,18 +232,25 @@ func file_proxy_dns_config_proto_rawDescGZIP() []byte {
|
||||
return file_proxy_dns_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_proxy_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_proxy_dns_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.proxy.dns.Config
|
||||
(*net.Endpoint)(nil), // 1: xray.common.net.Endpoint
|
||||
(RuleAction)(0), // 0: xray.proxy.dns.RuleAction
|
||||
(*DNSRuleConfig)(nil), // 1: xray.proxy.dns.DNSRuleConfig
|
||||
(*Config)(nil), // 2: xray.proxy.dns.Config
|
||||
(*geodata.DomainRule)(nil), // 3: xray.common.geodata.DomainRule
|
||||
(*net.Endpoint)(nil), // 4: xray.common.net.Endpoint
|
||||
}
|
||||
var file_proxy_dns_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
0, // 0: xray.proxy.dns.DNSRuleConfig.action:type_name -> xray.proxy.dns.RuleAction
|
||||
3, // 1: xray.proxy.dns.DNSRuleConfig.domain:type_name -> xray.common.geodata.DomainRule
|
||||
1, // 2: xray.proxy.dns.Config.rule:type_name -> xray.proxy.dns.DNSRuleConfig
|
||||
4, // 3: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_dns_config_proto_init() }
|
||||
@@ -143,13 +263,14 @@ func file_proxy_dns_config_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumEnums: 1,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_proxy_dns_config_proto_goTypes,
|
||||
DependencyIndexes: file_proxy_dns_config_proto_depIdxs,
|
||||
EnumInfos: file_proxy_dns_config_proto_enumTypes,
|
||||
MessageInfos: file_proxy_dns_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proxy_dns_config_proto = out.File
|
||||
|
||||
+17
-6
@@ -7,12 +7,23 @@ option java_package = "com.xray.proxy.dns";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/net/destination.proto";
|
||||
import "common/geodata/geodat.proto";
|
||||
|
||||
enum RuleAction {
|
||||
Direct = 0;
|
||||
Drop = 1;
|
||||
Reject = 2;
|
||||
Hijack = 3;
|
||||
}
|
||||
|
||||
message DNSRuleConfig {
|
||||
RuleAction action = 1;
|
||||
repeated int32 qtype = 2;
|
||||
repeated xray.common.geodata.DomainRule domain = 3;
|
||||
}
|
||||
|
||||
message Config {
|
||||
// Server is the DNS server address. If specified, this address overrides the
|
||||
// original one.
|
||||
xray.common.net.Endpoint server = 1;
|
||||
uint32 user_level = 2;
|
||||
string non_IP_query = 3;
|
||||
repeated int32 block_types = 4;
|
||||
uint32 user_level = 1;
|
||||
repeated DNSRuleConfig rule = 2;
|
||||
xray.common.net.Endpoint server = 3;
|
||||
}
|
||||
|
||||
+98
-50
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
dns_proto "github.com/xtls/xray-core/common/protocol/dns"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
@@ -40,6 +41,31 @@ func init() {
|
||||
}))
|
||||
}
|
||||
|
||||
type DNSRule struct {
|
||||
action RuleAction
|
||||
qTypes []uint16
|
||||
domains geodata.DomainMatcher
|
||||
}
|
||||
|
||||
func (r *DNSRule) matchQType(qType uint16) bool {
|
||||
if len(r.qTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, t := range r.qTypes {
|
||||
if t == qType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *DNSRule) Apply(qType uint16, domain string) bool {
|
||||
if !r.matchQType(qType) {
|
||||
return false
|
||||
}
|
||||
return r.domains == nil || r.domains.MatchAny(strings.TrimSuffix(strings.ToLower(domain), "."))
|
||||
}
|
||||
|
||||
type ownLinkVerifier interface {
|
||||
IsOwnLink(ctx context.Context) bool
|
||||
}
|
||||
@@ -50,8 +76,7 @@ type Handler struct {
|
||||
ownLinkVerifier ownLinkVerifier
|
||||
server net.Destination
|
||||
timeout time.Duration
|
||||
nonIPQuery string
|
||||
blockTypes []int32
|
||||
rules []*DNSRule
|
||||
}
|
||||
|
||||
func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error {
|
||||
@@ -65,11 +90,26 @@ func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager polic
|
||||
if config.Server != nil {
|
||||
h.server = config.Server.AsDestination()
|
||||
}
|
||||
h.nonIPQuery = config.Non_IPQuery
|
||||
if h.nonIPQuery == "" {
|
||||
h.nonIPQuery = "reject"
|
||||
|
||||
h.rules = make([]*DNSRule, 0, len(config.Rule))
|
||||
for _, r := range config.Rule {
|
||||
rule := &DNSRule{
|
||||
action: r.Action,
|
||||
qTypes: make([]uint16, 0, len(r.Qtype)),
|
||||
}
|
||||
for _, t := range r.Qtype {
|
||||
rule.qTypes = append(rule.qTypes, uint16(t))
|
||||
}
|
||||
if len(r.Domain) > 0 {
|
||||
m, err := geodata.DomainReg.BuildDomainMatcher(r.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rule.domains = m
|
||||
}
|
||||
h.rules = append(h.rules, rule)
|
||||
}
|
||||
h.blockTypes = config.BlockTypes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -77,30 +117,38 @@ func (h *Handler) isOwnLink(ctx context.Context) bool {
|
||||
return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx)
|
||||
}
|
||||
|
||||
func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) {
|
||||
func parseQuery(b []byte) (id uint16, qType dnsmessage.Type, domain string, ok bool) {
|
||||
var parser dnsmessage.Parser
|
||||
header, err := parser.Start(b)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "parser start")
|
||||
return
|
||||
}
|
||||
|
||||
id = header.ID
|
||||
q, err := parser.Question()
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "question")
|
||||
return
|
||||
}
|
||||
domain = q.Name.String()
|
||||
qType = q.Type
|
||||
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
|
||||
return
|
||||
}
|
||||
|
||||
r = true
|
||||
domain = q.Name.String()
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction {
|
||||
qCode := uint16(qType)
|
||||
for _, r := range h.rules {
|
||||
if r.Apply(qCode, domain) {
|
||||
return r.action
|
||||
}
|
||||
}
|
||||
if qType == dnsmessage.TypeA || qType == dnsmessage.TypeAAAA {
|
||||
return RuleAction_Hijack
|
||||
}
|
||||
return RuleAction_Reject
|
||||
}
|
||||
|
||||
// Process implements proxy.Outbound.
|
||||
func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error {
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
@@ -183,51 +231,51 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timer.Update()
|
||||
|
||||
if !h.isOwnLink(ctx) {
|
||||
isIPQuery, domain, id, qType := parseIPQuery(b.Bytes())
|
||||
if len(h.blockTypes) > 0 {
|
||||
for _, blocktype := range h.blockTypes {
|
||||
if blocktype == int32(qType) {
|
||||
b.Release()
|
||||
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
|
||||
if h.nonIPQuery == "reject" {
|
||||
err := h.rejectNonIPQuery(id, qType, domain, writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if isIPQuery {
|
||||
b.Release()
|
||||
go h.handleIPQuery(id, qType, domain, writer, timer)
|
||||
continue
|
||||
}
|
||||
if h.nonIPQuery == "drop" {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
if h.nonIPQuery == "reject" {
|
||||
b.Release()
|
||||
err := h.rejectNonIPQuery(id, qType, domain, writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
if h.isOwnLink(ctx) {
|
||||
if err := connWriter.WriteMessage(b); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := connWriter.WriteMessage(b); err != nil {
|
||||
return err
|
||||
id, qType, domain, ok := parseQuery(b.Bytes())
|
||||
if !ok {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.applyRules(qType, domain) {
|
||||
case RuleAction_Drop:
|
||||
b.Release()
|
||||
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
|
||||
case RuleAction_Reject:
|
||||
b.Release()
|
||||
errors.LogInfo(ctx, "rejected type ", qType, " query for domain ", domain)
|
||||
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
case RuleAction_Hijack:
|
||||
b.Release()
|
||||
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
|
||||
errors.LogError(ctx, "can only hijack A/AAAA records")
|
||||
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
go h.handleIPQuery(id, qType, domain, writer, timer)
|
||||
}
|
||||
case RuleAction_Direct:
|
||||
if err := connWriter.WriteMessage(b); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
panic("unknown rule action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
_ "github.com/xtls/xray-core/app/proxyman/inbound"
|
||||
_ "github.com/xtls/xray-core/app/proxyman/outbound"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/core"
|
||||
@@ -368,3 +369,126 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSRules(t *testing.T) {
|
||||
port := udp.PickPort()
|
||||
|
||||
dnsServer := dns.Server{
|
||||
Addr: "127.0.0.1:" + port.String(),
|
||||
Net: "udp",
|
||||
Handler: &staticHandler{},
|
||||
}
|
||||
defer dnsServer.Shutdown()
|
||||
|
||||
go dnsServer.ListenAndServe()
|
||||
time.Sleep(time.Second)
|
||||
|
||||
serverPort := udp.PickPort()
|
||||
config := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&dnsapp.Config{
|
||||
NameServer: []*dnsapp.NameServer{
|
||||
{
|
||||
Address: &net.Endpoint{
|
||||
Network: net.Network_UDP,
|
||||
Address: &net.IPOrDomain{
|
||||
Address: &net.IPOrDomain_Ip{
|
||||
Ip: []byte{127, 0, 0, 1},
|
||||
},
|
||||
},
|
||||
Port: uint32(port),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
serial.ToTypedMessage(&dispatcher.Config{}),
|
||||
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
||||
serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
||||
serial.ToTypedMessage(&policy.Config{}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(port),
|
||||
Networks: []net.Network{net.Network_UDP},
|
||||
}),
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
|
||||
Rule: []*dns_proxy.DNSRuleConfig{
|
||||
{
|
||||
Qtype: []int32{int32(dns.TypeA)},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Domain,
|
||||
Value: "facebook.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: dns_proxy.RuleAction_Direct,
|
||||
},
|
||||
{
|
||||
Qtype: []int32{int32(dns.TypeA)},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Full,
|
||||
Value: "google.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: dns_proxy.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
v, err := core.New(config)
|
||||
common.Must(err)
|
||||
common.Must(v.Start())
|
||||
defer v.Close()
|
||||
|
||||
{
|
||||
m1 := new(dns.Msg)
|
||||
m1.Id = dns.Id()
|
||||
m1.RecursionDesired = true
|
||||
m1.Question = []dns.Question{{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
|
||||
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
common.Must(err)
|
||||
|
||||
if in.Rcode != dns.RcodeRefused {
|
||||
t.Fatal("expected Refused, but got ", in.Rcode)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
m1 := new(dns.Msg)
|
||||
m1.Id = dns.Id()
|
||||
m1.RecursionDesired = true
|
||||
m1.Question = []dns.Question{{Name: "facebook.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
|
||||
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
|
||||
common.Must(err)
|
||||
|
||||
if in.Rcode != dns.RcodeSuccess {
|
||||
t.Fatal("expected Success, but got ", in.Rcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+239
-25
@@ -7,6 +7,8 @@
|
||||
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"
|
||||
@@ -23,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"`
|
||||
@@ -251,6 +299,134 @@ func (x *Noise) GetApplyTo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
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 *Range) Reset() {
|
||||
*x = Range{}
|
||||
mi := &file_proxy_freedom_config_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Range) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Range) ProtoMessage() {}
|
||||
|
||||
func (x *Range) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_freedom_config_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Range.ProtoReflect.Descriptor instead.
|
||||
func (*Range) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *Range) GetMin() uint64 {
|
||||
if x != nil {
|
||||
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
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"`
|
||||
@@ -259,13 +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"`
|
||||
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[3]
|
||||
mi := &file_proxy_freedom_config_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -277,7 +454,7 @@ func (x *Config) String() string {
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_freedom_config_proto_msgTypes[3]
|
||||
mi := &file_proxy_freedom_config_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -290,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{3}
|
||||
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *Config) GetDomainStrategy() internet.DomainStrategy {
|
||||
@@ -335,11 +512,18 @@ func (x *Config) GetNoises() []*Noise {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetFinalRules() []*FinalRuleConfig {
|
||||
if x != nil {
|
||||
return x.FinalRules
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proxy_freedom_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_freedom_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\"S\n" +
|
||||
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\x1a\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" +
|
||||
@@ -362,7 +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\"\xe9\x02\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" +
|
||||
@@ -370,7 +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\x06noisesBX\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 (
|
||||
@@ -385,26 +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, 4)
|
||||
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
|
||||
(*Config)(nil), // 3: xray.proxy.freedom.Config
|
||||
(*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint
|
||||
(internet.DomainStrategy)(0), // 5: 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{
|
||||
4, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
|
||||
5, // 1: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||
0, // 2: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
|
||||
1, // 3: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
|
||||
2, // 4: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
|
||||
5, // [5:5] is the sub-list for method output_type
|
||||
5, // [5:5] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
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() }
|
||||
@@ -417,13 +630,14 @@ func file_proxy_freedom_config_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 4,
|
||||
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,9 @@ 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 {
|
||||
xray.common.protocol.ServerEndpoint server = 1;
|
||||
@@ -23,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;
|
||||
@@ -32,6 +36,24 @@ message Noise {
|
||||
string apply_to = 6;
|
||||
}
|
||||
|
||||
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 {
|
||||
xray.transport.internet.DomainStrategy domain_strategy = 1;
|
||||
DestinationOverride destination_override = 3;
|
||||
@@ -39,4 +61,5 @@ message Config {
|
||||
Fragment fragment = 5;
|
||||
uint32 proxy_protocol = 6;
|
||||
repeated Noise noises = 7;
|
||||
repeated FinalRuleConfig final_rules = 8;
|
||||
}
|
||||
|
||||
+341
-62
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/crypto"
|
||||
"github.com/xtls/xray-core/common/dice"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
"github.com/xtls/xray-core/common/retry"
|
||||
@@ -29,10 +31,21 @@ import (
|
||||
)
|
||||
|
||||
var useSplice bool
|
||||
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) {
|
||||
h := new(Handler)
|
||||
if handler, ok := session.FullHandlerFromContext(ctx).(handlerWithSocketSettings); ok {
|
||||
if sockopt := handler.SocketSettings(); sockopt != nil {
|
||||
h.socketStrategy = sockopt.DomainStrategy
|
||||
}
|
||||
}
|
||||
if handler, ok := session.FullHandlerFromContext(ctx).(handlerWithProxySettings); ok {
|
||||
h.usesProxySettings = handler.UsesProxySettings()
|
||||
}
|
||||
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
|
||||
return h.Init(config.(*Config), pm)
|
||||
}); err != nil {
|
||||
@@ -40,24 +53,172 @@ 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 handlerWithSocketSettings interface {
|
||||
SocketSettings() *internet.SocketConfig
|
||||
}
|
||||
|
||||
type handlerWithProxySettings interface {
|
||||
UsesProxySettings() bool
|
||||
}
|
||||
|
||||
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
|
||||
policyManager policy.Manager
|
||||
config *Config
|
||||
finalRules []*FinalRule
|
||||
socketStrategy internet.DomainStrategy
|
||||
usesProxySettings bool
|
||||
}
|
||||
|
||||
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) 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
|
||||
}
|
||||
|
||||
// Init initializes the Handler with necessary parameters.
|
||||
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
||||
h.config = config
|
||||
h.policyManager = pm
|
||||
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 final rule").Base(err)
|
||||
}
|
||||
h.finalRules = append(h.finalRules, rule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -66,6 +227,41 @@ 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 (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
|
||||
delay := h.blockDelay(rule)
|
||||
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
|
||||
timer := time.AfterFunc(delay, func() {
|
||||
common.Interrupt(input)
|
||||
common.Interrupt(output)
|
||||
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
|
||||
})
|
||||
defer timer.Stop()
|
||||
defer common.Close(output)
|
||||
_ = buf.Copy(input, buf.Discard)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) udpDomainStrategy() internet.DomainStrategy {
|
||||
if h.config.DomainStrategy.HasStrategy() {
|
||||
return h.config.DomainStrategy
|
||||
}
|
||||
return h.socketStrategy
|
||||
}
|
||||
|
||||
func isValidAddress(addr *net.IPOrDomain) bool {
|
||||
if addr == nil {
|
||||
return false
|
||||
@@ -85,6 +281,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ob.Name = "freedom"
|
||||
ob.CanSpliceCopy = 1
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
defaultRule := getDefaultFinalRule(inbound)
|
||||
|
||||
destination := ob.Target
|
||||
origTargetAddr := ob.OriginalTarget.Address
|
||||
@@ -110,26 +307,76 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
output := link.Writer
|
||||
|
||||
var conn stat.Connection
|
||||
var blockedDest *net.Destination
|
||||
var blockedRule *FinalRule
|
||||
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||
dialDest := destination
|
||||
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
|
||||
strategy := h.config.DomainStrategy
|
||||
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
|
||||
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
|
||||
|
||||
if dialDest.Address.Family().IsDomain() {
|
||||
if strategy := h.config.DomainStrategy; strategy.HasStrategy() {
|
||||
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
|
||||
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
|
||||
}
|
||||
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
|
||||
if err != nil { // SRV/TXT
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
||||
if h.config.DomainStrategy.ForceIP() || defaultRule != nil || len(h.finalRules) > 0 {
|
||||
return err // retry
|
||||
}
|
||||
} else { // to ip
|
||||
dialDest = net.Destination{
|
||||
Network: dialDest.Network,
|
||||
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
|
||||
Port: dialDest.Port,
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
} else if defaultRule != nil || len(h.finalRules) > 0 { // freedom asis + hasrules
|
||||
if strategy := h.socketStrategy; strategy.HasStrategy() {
|
||||
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
|
||||
if err != nil { // SRV/TXT
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
||||
if strategy.ForceIP() {
|
||||
return err // retry
|
||||
}
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if addr := net.IPAddress(ip); addr != nil {
|
||||
if rule := h.matchFinalRule(dialDest.Network, addr, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &dialDest
|
||||
blockedDest.Address = addr
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // sockopt asis
|
||||
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, dialDest.Address.Domain())
|
||||
if err != nil { // SRV/TXT
|
||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipAddr := net.IPAddress(addr.IP); ipAddr != nil {
|
||||
if rule := h.matchFinalRule(dialDest.Network, ipAddr, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &dialDest
|
||||
blockedDest.Address = ipAddr
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dialDest = net.Destination{
|
||||
Network: dialDest.Network,
|
||||
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
|
||||
Port: dialDest.Port,
|
||||
}
|
||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
||||
} else {
|
||||
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
blockedDest = &dialDest
|
||||
blockedRule = rule
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,23 +385,38 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return err
|
||||
}
|
||||
|
||||
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
|
||||
version := byte(h.config.ProxyProtocol)
|
||||
srcAddr := inbound.Source.RawNetAddr()
|
||||
dstAddr := rawConn.RemoteAddr()
|
||||
header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
|
||||
if _, err = header.WriteTo(rawConn); err != nil {
|
||||
rawConn.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
conn = rawConn
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("failed to open connection to ", destination).Base(err)
|
||||
}
|
||||
if blockedDest != nil {
|
||||
return h.blackhole(ctx, input, output, blockedRule, blockedDest)
|
||||
}
|
||||
if defaultRule != nil || len(h.finalRules) > 0 {
|
||||
if h.usesProxySettings {
|
||||
errors.LogInfo(ctx, "skipping final rule check for proxied remote endpoint, original target: ", destination)
|
||||
} else {
|
||||
// SRV/TXT, lookup failed
|
||||
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
conn.Close()
|
||||
return h.blackhole(ctx, input, output, rule, &remoteDest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
|
||||
version := byte(h.config.ProxyProtocol)
|
||||
srcAddr := inbound.Source.RawNetAddr()
|
||||
dstAddr := conn.RemoteAddr()
|
||||
header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
|
||||
if _, err = header.WriteTo(conn); err != nil {
|
||||
conn.Close()
|
||||
return errors.New("failed to set PROXY protocol v", version).Base(err)
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr())
|
||||
|
||||
@@ -189,7 +451,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
writer = buf.NewWriter(conn)
|
||||
}
|
||||
} else {
|
||||
writer = NewPacketWriter(conn, h, UDPOverride, destination)
|
||||
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
|
||||
if h.config.Noises != nil {
|
||||
errors.LogDebug(ctx, "NOISE", h.config.Noises)
|
||||
writer = &NoisePacketWriter{
|
||||
@@ -224,7 +486,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)
|
||||
@@ -243,7 +505,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 {
|
||||
@@ -262,6 +524,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,
|
||||
@@ -273,6 +537,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
|
||||
@@ -281,33 +547,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 rule := r.Handler.matchFinalRule(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule); rule != nil && rule.action == 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) buf.Writer {
|
||||
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination, outGateway net.Address) buf.Writer {
|
||||
iConn := conn
|
||||
statConn, ok := iConn.(*stat.CounterConnection)
|
||||
if ok {
|
||||
@@ -328,9 +601,10 @@ func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, Dia
|
||||
PacketConnWrapper: c,
|
||||
Counter: counter,
|
||||
Handler: h,
|
||||
DefaultRule: defaultRule,
|
||||
UDPOverride: UDPOverride,
|
||||
ResolvedUDPAddr: resolvedUDPAddr,
|
||||
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
|
||||
OutGateway: outGateway,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -341,6 +615,7 @@ type PacketWriter struct {
|
||||
*internet.PacketConnWrapper
|
||||
stats.Counter
|
||||
*Handler
|
||||
DefaultRule *FinalRule
|
||||
UDPOverride net.Destination
|
||||
|
||||
// Dest of udp packets might be a domain, we will resolve them to IP
|
||||
@@ -348,7 +623,7 @@ type PacketWriter struct {
|
||||
// Resulting in these packets being sent to many different IPs randomly
|
||||
// So, cache and keep the resolve result
|
||||
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
|
||||
LocalAddr net.Address
|
||||
OutGateway net.Address
|
||||
}
|
||||
|
||||
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
@@ -371,21 +646,21 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
|
||||
b.UDP.Address = ip
|
||||
} else {
|
||||
ShouldUseSystemResolver := true
|
||||
if w.Handler.config.DomainStrategy.HasStrategy() {
|
||||
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr)
|
||||
shouldUseSystemResolver := true
|
||||
if resolveStrategy := w.Handler.udpDomainStrategy(); resolveStrategy.HasStrategy() {
|
||||
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), resolveStrategy, w.OutGateway)
|
||||
if err != nil {
|
||||
// drop packet if resolve failed when forceIP
|
||||
if w.Handler.config.DomainStrategy.ForceIP() {
|
||||
if resolveStrategy.ForceIP() {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
ip = net.IPAddress(ips[dice.Roll(len(ips))])
|
||||
ShouldUseSystemResolver = false
|
||||
shouldUseSystemResolver = false
|
||||
}
|
||||
}
|
||||
if ShouldUseSystemResolver {
|
||||
if shouldUseSystemResolver {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
|
||||
if err != nil {
|
||||
b.Release()
|
||||
@@ -399,6 +674,10 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if rule := w.matchFinalRule(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
destAddr := b.UDP.RawNetAddr()
|
||||
if destAddr == nil {
|
||||
b.Release()
|
||||
|
||||
+45
-48
@@ -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,30 +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()
|
||||
buffer.Write(r.firstMsg.Data)
|
||||
buffer.UDP = r.firstDest
|
||||
|
||||
r.firstMsg = 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
|
||||
}
|
||||
@@ -270,14 +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()
|
||||
buffer.Write(dfMsg.Data)
|
||||
buffer.UDP = &dest
|
||||
if len(p) < len(dfMsg.Data) {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+22
-58
@@ -2,7 +2,6 @@ package hysteria
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -82,79 +81,44 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.Name = "hysteria"
|
||||
inbound.CanSpliceCopy = 3
|
||||
inbound.User = &protocol.MemoryUser{}
|
||||
|
||||
iConn := stat.TryUnwrapStatsConn(conn)
|
||||
|
||||
var useremail string
|
||||
var userlevel uint32
|
||||
type User interface{ User() *protocol.MemoryUser }
|
||||
if v, ok := iConn.(User); ok {
|
||||
if v, ok := iConn.(User); ok && v.User() != nil {
|
||||
inbound.User = v.User()
|
||||
if inbound.User != nil {
|
||||
useremail = inbound.User.Email
|
||||
userlevel = inbound.User.Level
|
||||
}
|
||||
} else {
|
||||
// get a dummy user
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: "",
|
||||
Level: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := iConn.(*hysteria.InterUdpConn); ok {
|
||||
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,
|
||||
})
|
||||
} else {
|
||||
sessionPolicy := s.policyManager.ForLevel(userlevel)
|
||||
sessionPolicy := s.policyManager.ForLevel(inbound.User.Level)
|
||||
|
||||
common.Must(conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)))
|
||||
addr, err := ReadTCPRequest(conn)
|
||||
@@ -178,7 +142,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Reason: "",
|
||||
Email: useremail,
|
||||
Email: inbound.User.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to ", dest)
|
||||
|
||||
|
||||
+14
-76
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+23
-2
@@ -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
|
||||
@@ -173,6 +175,25 @@ Note on ipv6 support. \
|
||||
Despite Windows also giving the adapter autoconfigured ipv6 address, the ipv6 is not possible until the interface has any _routable_ ipv6 address (given link-local address will not accept traffic from external addresses). \
|
||||
So everything applicable for ipv4 above also works for ipv6, you only need to give the interface some address manually, e.g. anything private like fc00::a:b:c:d/64 will do just fine
|
||||
|
||||
## FreeBSD SUPPORT
|
||||
|
||||
FreeBSD support of the same functionality is implemented through tun(4).
|
||||
|
||||
Interface name in the configuration must comply to the scheme "tunN", where N is some number. \
|
||||
It's necessary to set an IP address to the interface, ex.:
|
||||
```
|
||||
ifconfig tun0 inet 169.254.10.1/30
|
||||
```
|
||||
To attach routing to the interface, route command like following can be executed:
|
||||
```
|
||||
route add -net 1.1.1.0/24 -iface tun10
|
||||
```
|
||||
```
|
||||
route add -inet6 -host 2606:4700:4700::1111 -iface tun10
|
||||
route add -inet6 -host 2606:4700:4700::1001 -iface tun10
|
||||
```
|
||||
Important to remember that everything written above about Linux routing concept, also apply to FreeBSD. If you simply route default route through tun interface, that will result network loop and immediate network failure.
|
||||
|
||||
## MAC OS X SUPPORT
|
||||
|
||||
Darwin (Mac OS X) support of the same functionality is implemented through utun (userspace tunnel).
|
||||
@@ -229,4 +250,4 @@ Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number b
|
||||
Build using gomobile for iOS framework integration:
|
||||
```
|
||||
gomobile bind -target=ios
|
||||
```
|
||||
```
|
||||
|
||||
+60
-13
@@ -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
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
MTU []uint32 `protobuf:"varint,2,rep,packed,name=MTU,proto3" json:"MTU,omitempty"`
|
||||
MTU uint32 `protobuf:"varint,2,opt,name=MTU,proto3" json:"MTU,omitempty"`
|
||||
Gateway []string `protobuf:"bytes,3,rep,name=gateway,proto3" json:"gateway,omitempty"`
|
||||
DNS []string `protobuf:"bytes,4,rep,name=DNS,proto3" json:"DNS,omitempty"`
|
||||
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
@@ -71,11 +71,11 @@ func (x *Config) GetName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetMTU() []uint32 {
|
||||
func (x *Config) GetMTU() uint32 {
|
||||
if x != nil {
|
||||
return x.MTU
|
||||
}
|
||||
return nil
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetGateway() []string {
|
||||
@@ -120,7 +120,7 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x03(\rR\x03MTU\x12\x18\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
"\agateway\x18\x03 \x03(\tR\agateway\x12\x10\n" +
|
||||
"\x03DNS\x18\x04 \x03(\tR\x03DNS\x12\x1d\n" +
|
||||
"\n" +
|
||||
|
||||
@@ -8,7 +8,7 @@ option java_multiple_files = true;
|
||||
|
||||
message Config {
|
||||
string name = 1;
|
||||
repeated uint32 MTU = 2;
|
||||
uint32 MTU = 2;
|
||||
repeated string gateway = 3;
|
||||
repeated string DNS = 4;
|
||||
uint32 user_level = 5;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package icmp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
)
|
||||
|
||||
func TestParseEchoRequest(t *testing.T) {
|
||||
t.Run("ipv4 echo", func(t *testing.T) {
|
||||
var zero tcpip.Address
|
||||
packet := []byte{
|
||||
byte(header.ICMPv4Echo), 0,
|
||||
0, 0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb,
|
||||
}
|
||||
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ident, sequence, ok := ParseEchoRequest(header.IPv4ProtocolNumber, packet)
|
||||
if !ok {
|
||||
t.Fatal("expected ipv4 echo request to parse")
|
||||
}
|
||||
if ident != 0x1234 || sequence != 0x5678 {
|
||||
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ipv6 echo", func(t *testing.T) {
|
||||
packet := []byte{
|
||||
byte(header.ICMPv6EchoRequest), 0,
|
||||
0, 0,
|
||||
0xab, 0xcd,
|
||||
0xef, 0x01,
|
||||
0xaa, 0xbb,
|
||||
}
|
||||
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
|
||||
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ident, sequence, ok := ParseEchoRequest(header.IPv6ProtocolNumber, packet)
|
||||
if !ok {
|
||||
t.Fatal("expected ipv6 echo request to parse")
|
||||
}
|
||||
if ident != 0xabcd || sequence != 0xef01 {
|
||||
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRewriteChecksum(t *testing.T) {
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
var zero tcpip.Address
|
||||
packet := []byte{
|
||||
byte(header.ICMPv4Echo), 0,
|
||||
0xff, 0xff,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
icmpHdr := header.ICMPv4(packet)
|
||||
if got, want := icmpHdr.Checksum(), header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksumPayloadV4(icmpHdr.Payload())); got != want {
|
||||
t.Fatalf("unexpected ipv4 checksum: got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ipv6", func(t *testing.T) {
|
||||
packet := []byte{
|
||||
byte(header.ICMPv6EchoReply), 0,
|
||||
0xff, 0xff,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
|
||||
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
icmpHdr := header.ICMPv6(packet)
|
||||
want := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
|
||||
Header: icmpHdr[:header.ICMPv6MinimumSize],
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
PayloadLen: len(icmpHdr.Payload()),
|
||||
PayloadCsum: checksumPayloadV6(icmpHdr.Payload()),
|
||||
})
|
||||
if got := icmpHdr.Checksum(); got != want {
|
||||
t.Fatalf("unexpected ipv6 checksum: got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildLocalEchoReply(t *testing.T) {
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
request := []byte{
|
||||
byte(header.ICMPv4Echo), 0,
|
||||
0, 0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
src := tcpip.Address{}
|
||||
dst := tcpip.Address{}
|
||||
if err := RewriteChecksum(header.IPv4ProtocolNumber, request, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reply, err := BuildLocalEchoReply(header.IPv4ProtocolNumber, request, dst, src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if request[0] != byte(header.ICMPv4Echo) {
|
||||
t.Fatal("request mutated")
|
||||
}
|
||||
icmpHdr := header.ICMPv4(reply)
|
||||
if icmpHdr.Type() != header.ICMPv4EchoReply || icmpHdr.Code() != header.ICMPv4UnusedCode {
|
||||
t.Fatalf("unexpected ipv4 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
|
||||
}
|
||||
if icmpHdr.Ident() != 0x1234 || icmpHdr.Sequence() != 0x5678 {
|
||||
t.Fatalf("unexpected ipv4 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ipv6", func(t *testing.T) {
|
||||
request := []byte{
|
||||
byte(header.ICMPv6EchoRequest), 0,
|
||||
0, 0,
|
||||
0xab, 0xcd,
|
||||
0xef, 0x01,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
|
||||
if err := RewriteChecksum(header.IPv6ProtocolNumber, request, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reply, err := BuildLocalEchoReply(header.IPv6ProtocolNumber, request, dst, src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if request[0] != byte(header.ICMPv6EchoRequest) {
|
||||
t.Fatal("request mutated")
|
||||
}
|
||||
icmpHdr := header.ICMPv6(reply)
|
||||
if icmpHdr.Type() != header.ICMPv6EchoReply || icmpHdr.Code() != header.ICMPv6UnusedCode {
|
||||
t.Fatalf("unexpected ipv6 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
|
||||
}
|
||||
if icmpHdr.Ident() != 0xabcd || icmpHdr.Sequence() != 0xef01 {
|
||||
t.Fatalf("unexpected ipv6 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func checksumPayloadV4(payload []byte) uint16 {
|
||||
return checksum.Checksum(payload, 0)
|
||||
}
|
||||
|
||||
func checksumPayloadV6(payload []byte) uint16 {
|
||||
return checksum.Checksum(payload, 0)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
@@ -110,14 +111,15 @@ func (t *stackGVisor) Start() error {
|
||||
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
|
||||
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
|
||||
if srcIP == nil || dstIP == nil {
|
||||
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice())
|
||||
return true
|
||||
panic(id)
|
||||
}
|
||||
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
|
||||
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
|
||||
udpForwarder.HandlePacket(src, dst, data)
|
||||
return true
|
||||
})
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, t.handleICMPv4Packet)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, t.handleICMPv6Packet)
|
||||
|
||||
t.stack = ipStack
|
||||
t.endpoint = linkEndpoint
|
||||
@@ -206,7 +208,7 @@ func (t *stackGVisor) Close() error {
|
||||
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
|
||||
opts := stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
|
||||
HandleLocal: false,
|
||||
}
|
||||
gStack := stack.New(opts)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
tunicmp "github.com/xtls/xray-core/proxy/tun/icmp"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
)
|
||||
|
||||
func (t *stackGVisor) handleICMPv4Packet(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
|
||||
return t.handleICMPEchoPacket(header.IPv4ProtocolNumber, id, pkt)
|
||||
}
|
||||
|
||||
func (t *stackGVisor) handleICMPv6Packet(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
|
||||
return t.handleICMPEchoPacket(header.IPv6ProtocolNumber, id, pkt)
|
||||
}
|
||||
|
||||
func (t *stackGVisor) handleICMPEchoPacket(netProto tcpip.NetworkProtocolNumber, id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
|
||||
srcIP := id.RemoteAddress
|
||||
dstIP := id.LocalAddress
|
||||
if srcIP.Len() == 0 || dstIP.Len() == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
message := transportPacketBytes(pkt)
|
||||
ident, sequence, ok := tunicmp.ParseEchoRequest(netProto, message)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
reply, err := tunicmp.BuildLocalEchoReply(netProto, message, dstIP, srcIP)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(t.ctx, err, "[tun] failed to build local icmp echo reply")
|
||||
return true
|
||||
}
|
||||
|
||||
errors.LogDebug(t.ctx, "[tun][icmp] ", tunicmp.ProtocolLabel(netProto), " local echo reply ", dstIP, " -> ", srcIP, " id=", ident, " seq=", sequence)
|
||||
if err := t.writeRawICMPPacket(netProto, reply, dstIP, srcIP); err != nil {
|
||||
errors.LogInfoInner(t.ctx, err, "[tun] failed to write local icmp echo reply")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *stackGVisor) writeRawICMPPacket(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
|
||||
ipHeaderSize := header.IPv6MinimumSize
|
||||
ipProtocol := header.IPv6ProtocolNumber
|
||||
transportProtocol := header.ICMPv6ProtocolNumber
|
||||
if netProto == header.IPv4ProtocolNumber {
|
||||
ipHeaderSize = header.IPv4MinimumSize
|
||||
ipProtocol = header.IPv4ProtocolNumber
|
||||
transportProtocol = header.ICMPv4ProtocolNumber
|
||||
}
|
||||
|
||||
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
ReserveHeaderBytes: ipHeaderSize,
|
||||
Payload: buffer.MakeWithData(message),
|
||||
})
|
||||
defer pkt.DecRef()
|
||||
|
||||
if netProto == header.IPv4ProtocolNumber {
|
||||
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
|
||||
ipHdr.Encode(&header.IPv4Fields{
|
||||
TotalLength: uint16(header.IPv4MinimumSize + len(message)),
|
||||
TTL: 64,
|
||||
Protocol: uint8(transportProtocol),
|
||||
SrcAddr: srcIP,
|
||||
DstAddr: dstIP,
|
||||
})
|
||||
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
|
||||
} else {
|
||||
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
|
||||
ipHdr.Encode(&header.IPv6Fields{
|
||||
PayloadLength: uint16(len(message)),
|
||||
TransportProtocol: transportProtocol,
|
||||
HopLimit: 64,
|
||||
SrcAddr: srcIP,
|
||||
DstAddr: dstIP,
|
||||
})
|
||||
}
|
||||
|
||||
if err := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView())); err != nil {
|
||||
return errors.New("failed to write raw icmp packet back to stack", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func transportPacketBytes(pkt *stack.PacketBuffer) []byte {
|
||||
headerBytes := pkt.TransportHeader().Slice()
|
||||
payloadBytes := pkt.Data().AsRange().ToSlice()
|
||||
message := make([]byte, len(headerBytes)+len(payloadBytes))
|
||||
copy(message, headerBytes)
|
||||
copy(message[len(headerBytes):], payloadBytes)
|
||||
return message
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (t *AndroidTun) Index() (int, error) {
|
||||
func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return fdbased.New(&fdbased.Options{
|
||||
FDs: []int{t.tunFd},
|
||||
MTU: t.options.MTU[0],
|
||||
MTU: t.options.MTU,
|
||||
RXChecksumOffload: true,
|
||||
})
|
||||
}
|
||||
|
||||
+16
-12
@@ -3,7 +3,7 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
go_errors "errors"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
@@ -76,7 +75,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = setup(options.Name, options.MTU[0])
|
||||
err = setup(options.Name, options.MTU)
|
||||
if err != nil {
|
||||
_ = tunFile.Close()
|
||||
return nil, err
|
||||
@@ -121,7 +120,7 @@ func (t *DarwinTun) Index() (int, error) {
|
||||
// WritePacket implements GVisorDevice method to write one packet to the tun device
|
||||
func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
|
||||
defer b.Release()
|
||||
|
||||
// prepare Darwin specific packet header
|
||||
@@ -143,7 +142,7 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
b.SetByte(3, family)
|
||||
|
||||
if _, err := t.tunFile.Write(b.Bytes()); err != nil {
|
||||
if go_errors.Is(err, unix.EAGAIN) {
|
||||
if errors.Is(err, unix.EAGAIN) {
|
||||
return &tcpip.ErrWouldBlock{}
|
||||
}
|
||||
return &tcpip.ErrAborted{}
|
||||
@@ -156,11 +155,11 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// which will make the stack call Wait which should implement desired push-back
|
||||
func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
|
||||
|
||||
// read the bytes to the interface file
|
||||
n, err := b.ReadFrom(t.tunFile)
|
||||
if go_errors.Is(err, unix.EAGAIN) || go_errors.Is(err, unix.EINTR) {
|
||||
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
|
||||
b.Release()
|
||||
return 0, nil, ErrQueueEmpty
|
||||
}
|
||||
@@ -193,7 +192,7 @@ func (t *DarwinTun) Wait() {
|
||||
}
|
||||
|
||||
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
||||
}
|
||||
|
||||
// open the interface, by creating new utunN if in the system and returning its file descriptor
|
||||
@@ -373,12 +372,17 @@ func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
var err1, err2 error
|
||||
|
||||
switch network {
|
||||
case "tcp4", "udp4", "ip4":
|
||||
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
|
||||
case "tcp6", "udp6", "ip6":
|
||||
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
|
||||
err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
|
||||
fallthrough
|
||||
case "tcp4", "udp4", "ip4":
|
||||
err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
|
||||
default:
|
||||
return errors.New("unknown network ", network)
|
||||
panic(network + " " + address)
|
||||
}
|
||||
|
||||
return errors.Join(err1, err2)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user