Compare commits
55 Commits
v26.4.13
...
finalrules
| 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
|
FROM gcr.io/distroless/static:nonroot
|
||||||
|
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
|
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
|
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
|
||||||
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
|
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
|
||||||
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
|
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
|
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
|
|||||||
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
|
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
|
||||||
|
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
|
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
|
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
|
||||||
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
|
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
|
||||||
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
|
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
|
||||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
|
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
|
||||||
|
|||||||
@@ -111,6 +111,8 @@
|
|||||||
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
|
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
|
||||||
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
|
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
|
||||||
- [GenyConnect](https://github.com/genyleap/GenyConnect)
|
- [GenyConnect](https://github.com/genyleap/GenyConnect)
|
||||||
|
- [OneXray](https://github.com/OneXray/OneXray)
|
||||||
|
- [XrayUI-dev](https://github.com/PhoenixNil/XrayUI-dev)
|
||||||
- Android
|
- Android
|
||||||
- [v2rayNG](https://github.com/2dust/v2rayNG)
|
- [v2rayNG](https://github.com/2dust/v2rayNG)
|
||||||
- [X-flutter](https://github.com/XTLS/X-flutter)
|
- [X-flutter](https://github.com/XTLS/X-flutter)
|
||||||
@@ -119,6 +121,7 @@
|
|||||||
- [XrayFA](https://github.com/Q7DF1/XrayFA)
|
- [XrayFA](https://github.com/Q7DF1/XrayFA)
|
||||||
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
|
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
|
||||||
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
|
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
|
||||||
|
- [OneXray](https://github.com/OneXray/OneXray)
|
||||||
- iOS & macOS arm64 & tvOS
|
- 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)
|
- [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)
|
- [Streisand](https://apps.apple.com/app/streisand/id6450534064)
|
||||||
@@ -143,10 +146,12 @@
|
|||||||
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
|
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
|
||||||
- [v2rayN](https://github.com/2dust/v2rayN)
|
- [v2rayN](https://github.com/2dust/v2rayN)
|
||||||
- [GenyConnect](https://github.com/genyleap/GenyConnect)
|
- [GenyConnect](https://github.com/genyleap/GenyConnect)
|
||||||
|
- [OneXray](https://github.com/OneXray/OneXray)
|
||||||
|
|
||||||
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
|
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
|
||||||
|
|
||||||
- iOS & macOS arm64 & tvOS
|
- iOS & macOS arm64 & tvOS
|
||||||
|
- [Anywhere](https://github.com/NodePassProject/Anywhere)
|
||||||
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
|
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
|
||||||
- [Loon](https://apps.apple.com/us/app/loon/id1373567447)
|
- [Loon](https://apps.apple.com/us/app/loon/id1373567447)
|
||||||
- [Egern](https://apps.apple.com/us/app/egern/id1616105820)
|
- [Egern](https://apps.apple.com/us/app/egern/id1616105820)
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/signal/done"
|
"github.com/xtls/xray-core/common/signal/done"
|
||||||
core "github.com/xtls/xray-core/core"
|
core "github.com/xtls/xray-core/core"
|
||||||
"github.com/xtls/xray-core/features/outbound"
|
"github.com/xtls/xray-core/features/outbound"
|
||||||
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,14 +75,27 @@ func (c *Commander) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if len(c.listen) > 0 {
|
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)
|
errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen)
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-27
@@ -158,9 +158,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
|||||||
clients = append(clients, client)
|
clients = append(clients, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
var domainMatcher geodata.DomainMatcher
|
||||||
if err != nil {
|
if len(effectiveRules) > 0 {
|
||||||
return nil, err
|
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no DNS client in config, add a `localhost` DNS client
|
// If there is no DNS client in config, add a `localhost` DNS client
|
||||||
@@ -271,24 +274,27 @@ func (s *DNS) sortClients(domain string) []*Client {
|
|||||||
|
|
||||||
// Priority domain matching
|
// Priority domain matching
|
||||||
hasMatch := false
|
hasMatch := false
|
||||||
MatchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
if s.domainMatcher != nil {
|
||||||
sort.Slice(MatchSlice, func(i, j int) bool {
|
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||||
return MatchSlice[i] < MatchSlice[j]
|
sort.Slice(matchSlice, func(i, j int) bool {
|
||||||
})
|
return matchSlice[i] < matchSlice[j]
|
||||||
for _, match := range MatchSlice {
|
})
|
||||||
info := s.matcherInfos[match]
|
for _, match := range matchSlice {
|
||||||
client := s.clients[info.clientIdx]
|
info := s.matcherInfos[match]
|
||||||
domainRule := info.domainRule
|
client := s.clients[info.clientIdx]
|
||||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
domainRule := info.domainRule
|
||||||
if clientUsed[info.clientIdx] {
|
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||||
continue
|
if clientUsed[info.clientIdx] {
|
||||||
}
|
continue
|
||||||
clientUsed[info.clientIdx] = true
|
}
|
||||||
clients = append(clients, client)
|
clientUsed[info.clientIdx] = true
|
||||||
clientNames = append(clientNames, client.Name())
|
clients = append(clients, client)
|
||||||
hasMatch = true
|
clientNames = append(clientNames, client.Name())
|
||||||
if client.finalQuery {
|
hasMatch = true
|
||||||
return clients
|
if client.finalQuery {
|
||||||
|
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||||
|
return clients
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,17 +308,13 @@ func (s *DNS) sortClients(domain string) []*Client {
|
|||||||
clients = append(clients, client)
|
clients = append(clients, client)
|
||||||
clientNames = append(clientNames, client.Name())
|
clientNames = append(clientNames, client.Name())
|
||||||
if client.finalQuery {
|
if client.finalQuery {
|
||||||
|
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||||
return clients
|
return clients
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(domainRules) > 0 {
|
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||||
errors.LogDebug(s.ctx, "domain ", domain, " matches following rules: ", domainRules)
|
|
||||||
}
|
|
||||||
if len(clientNames) > 0 {
|
|
||||||
errors.LogDebug(s.ctx, "domain ", domain, " will use DNS in order: ", clientNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(clients) == 0 {
|
if len(clients) == 0 {
|
||||||
if len(s.clients) > 0 {
|
if len(s.clients) > 0 {
|
||||||
@@ -327,6 +329,15 @@ func (s *DNS) sortClients(domain string) []*Client {
|
|||||||
return clients
|
return clients
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logDecision(ctx context.Context, domain string, domainRules []string, clientNames []string) {
|
||||||
|
if len(domainRules) > 0 {
|
||||||
|
errors.LogDebug(ctx, "domain ", domain, " matches following rules: ", domainRules)
|
||||||
|
}
|
||||||
|
if len(clientNames) > 0 {
|
||||||
|
errors.LogDebug(ctx, "domain ", domain, " will use DNS in order: ", clientNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mergeQueryErrors(domain string, errs []error) error {
|
func mergeQueryErrors(domain string, errs []error) error {
|
||||||
if len(errs) == 0 {
|
if len(errs) == 0 {
|
||||||
return dns.ErrEmptyResponse
|
return dns.ErrEmptyResponse
|
||||||
|
|||||||
+36
-76
@@ -147,7 +147,9 @@ func TestUDPServerSubnet(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
Outbound: []*core.OutboundHandlerConfig{
|
||||||
{
|
{
|
||||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||||
|
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -207,7 +209,9 @@ func TestUDPServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
Outbound: []*core.OutboundHandlerConfig{
|
||||||
{
|
{
|
||||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
|
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||||
|
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -345,7 +349,9 @@ func TestPrioritizedDomain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
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{
|
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{
|
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),
|
Port: uint32(port),
|
||||||
},
|
},
|
||||||
ExpectedIp: []*geodata.IPRule{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
{
|
// inner ip, will not match
|
||||||
Value: &geodata.IPRule_Custom{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
|
||||||
Custom: &geodata.CIDR{
|
|
||||||
// inner ip, will not match
|
|
||||||
Ip: []byte{192, 168, 11, 1},
|
|
||||||
Prefix: 32,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// second dns, match ip
|
// second dns, match ip
|
||||||
@@ -561,22 +564,8 @@ func TestIPMatch(t *testing.T) {
|
|||||||
Port: uint32(port),
|
Port: uint32(port),
|
||||||
},
|
},
|
||||||
ExpectedIp: []*geodata.IPRule{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
|
||||||
Value: &geodata.IPRule_Custom{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -587,7 +576,9 @@ func TestIPMatch(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
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{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
// Will match localhost, localhost-a and localhost-b,
|
// 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.CIDRRule{Cidr: &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.CIDRRule{Cidr: &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, 4}, Prefix: 32}}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -704,7 +695,9 @@ func TestLocalDomain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
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{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
// Will only match 8.8.8.8 and 8.8.4.4
|
// Will only match 8.8.8.8 and 8.8.4.4
|
||||||
{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
|
||||||
Value: &geodata.IPRule_Custom{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}},
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -922,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ExpectedIp: []*geodata.IPRule{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
// Will match 8.8.8.8 and 8.8.8.7, etc
|
// Will match 8.8.8.8 and 8.8.8.7, etc
|
||||||
{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
|
||||||
Value: &geodata.IPRule_Custom{
|
|
||||||
Custom: &geodata.CIDR{
|
|
||||||
Ip: []byte{8, 8, 8, 7},
|
|
||||||
Prefix: 24,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -949,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ExpectedIp: []*geodata.IPRule{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
// Will only match 8.8.7.7 (api.google.com)
|
// Will only match 8.8.7.7 (api.google.com)
|
||||||
{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
|
||||||
Value: &geodata.IPRule_Custom{
|
|
||||||
Custom: &geodata.CIDR{
|
|
||||||
Ip: []byte{8, 8, 7, 7},
|
|
||||||
Prefix: 32,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -976,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ExpectedIp: []*geodata.IPRule{
|
ExpectedIp: []*geodata.IPRule{
|
||||||
// Will only match 8.8.7.8 (v2.api.google.com)
|
// Will only match 8.8.7.8 (v2.api.google.com)
|
||||||
{
|
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
|
||||||
Value: &geodata.IPRule_Custom{
|
|
||||||
Custom: &geodata.CIDR{
|
|
||||||
Ip: []byte{8, 8, 7, 8},
|
|
||||||
Prefix: 32,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -994,7 +952,9 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
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 {
|
type Holder struct {
|
||||||
domainToIP cache.Lru
|
domainToIP cache.Lru
|
||||||
ipRange *net.IPNet
|
ipRange *net.IPNet
|
||||||
mu *sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
config *FakeDnsPool
|
config *FakeDnsPool
|
||||||
}
|
}
|
||||||
@@ -49,9 +49,7 @@ func (fkdns *Holder) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fkdns *Holder) Close() error {
|
func (fkdns *Holder) Close() error {
|
||||||
fkdns.domainToIP = nil
|
// nothing to do for now, just wait GC
|
||||||
fkdns.ipRange = nil
|
|
||||||
fkdns.mu = nil
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ func NewFakeDNSHolder() (*Holder, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*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 {
|
func (fkdns *Holder) initializeFromConfig() error {
|
||||||
@@ -92,7 +90,6 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
|
|||||||
}
|
}
|
||||||
fkdns.domainToIP = cache.NewLru(lruSize)
|
fkdns.domainToIP = cache.NewLru(lruSize)
|
||||||
fkdns.ipRange = ipRange
|
fkdns.ipRange = ipRange
|
||||||
fkdns.mu = new(sync.Mutex)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +100,7 @@ func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
|
|||||||
if v, ok := fkdns.domainToIP.Get(domain); ok {
|
if v, ok := fkdns.domainToIP.Get(domain); ok {
|
||||||
return []net.Address{v.(net.Address)}
|
return []net.Address{v.(net.Address)}
|
||||||
}
|
}
|
||||||
currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
|
currentTimeMillis := uint64(time.Now().UnixMilli())
|
||||||
ones, bits := fkdns.ipRange.Mask.Size()
|
ones, bits := fkdns.ipRange.Mask.Size()
|
||||||
rooms := bits - ones
|
rooms := bits - ones
|
||||||
if rooms < 64 {
|
if rooms < 64 {
|
||||||
@@ -202,12 +199,11 @@ func (h *HolderMulti) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HolderMulti) Close() error {
|
func (h *HolderMulti) Close() error {
|
||||||
|
var errs []error
|
||||||
for _, v := range h.holders {
|
for _, v := range h.holders {
|
||||||
if err := v.Close(); err != nil {
|
errs = append(errs, v.Close())
|
||||||
return errors.New("Cannot close all fake dns pools").Base(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return errors.Combine(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HolderMulti) createHolderGroups() error {
|
func (h *HolderMulti) createHolderGroups() error {
|
||||||
@@ -222,7 +218,7 @@ func (h *HolderMulti) createHolderGroups() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
|
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
|
||||||
holderMulti := &HolderMulti{nil, conf}
|
holderMulti := &HolderMulti{config: conf}
|
||||||
if err := holderMulti.createHolderGroups(); err != nil {
|
if err := holderMulti.createHolderGroups(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-11
@@ -13,8 +13,8 @@ import (
|
|||||||
|
|
||||||
// StaticHosts represents static domain-ip mapping in DNS server.
|
// StaticHosts represents static domain-ip mapping in DNS server.
|
||||||
type StaticHosts struct {
|
type StaticHosts struct {
|
||||||
reps [][]net.Address
|
responses [][]net.Address
|
||||||
matcher geodata.DomainMatcher
|
matcher geodata.DomainMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStaticHosts creates a new StaticHosts instance.
|
// NewStaticHosts creates a new StaticHosts instance.
|
||||||
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
|
|||||||
rep = append(rep, addr)
|
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)
|
reps = append(reps, rep)
|
||||||
rules = append(rules, mapping.Domain)
|
rules = append(rules, mapping.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return &StaticHosts{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
|
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &StaticHosts{
|
return &StaticHosts{
|
||||||
reps: reps,
|
responses: reps,
|
||||||
matcher: matcher,
|
matcher: matcher,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
|
|||||||
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
|
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
|
||||||
ips := make([]net.Address, 0)
|
ips := make([]net.Address, 0)
|
||||||
found := false
|
found := false
|
||||||
for _, ruleIdx := range h.matcher.Match(domain) {
|
for _, idx := range h.matcher.Match(domain) {
|
||||||
for _, rep := range h.reps[ruleIdx] {
|
for _, rep := range h.responses[idx] {
|
||||||
if err, ok := rep.(dns.RCodeError); ok {
|
if err, ok := rep.(dns.RCodeError); ok {
|
||||||
if uint16(err) == 0 {
|
if uint16(err) == 0 {
|
||||||
return nil, dns.ErrEmptyResponse
|
return nil, dns.ErrEmptyResponse
|
||||||
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ips = append(ips, h.reps[ruleIdx]...)
|
ips = append(ips, h.responses[idx]...)
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
if !found {
|
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.
|
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
|
||||||
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
|
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
|
||||||
|
if h.matcher == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return h.lookup(domain, option, 5)
|
return h.lookup(domain, option, 5)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/routing"
|
||||||
"github.com/xtls/xray-core/features/stats"
|
"github.com/xtls/xray-core/features/stats"
|
||||||
"github.com/xtls/xray-core/proxy"
|
"github.com/xtls/xray-core/proxy"
|
||||||
"github.com/xtls/xray-core/proxy/hysteria/account"
|
hysteria_proxy "github.com/xtls/xray-core/proxy/hysteria"
|
||||||
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
|
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"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/stat"
|
||||||
"github.com/xtls/xray-core/transport/internet/tcp"
|
"github.com/xtls/xray-core/transport/internet/tcp"
|
||||||
"github.com/xtls/xray-core/transport/internet/udp"
|
"github.com/xtls/xray-core/transport/internet/udp"
|
||||||
@@ -134,10 +134,8 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
|
|||||||
func (w *tcpWorker) Start() error {
|
func (w *tcpWorker) Start() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator }
|
if v, ok := w.proxy.(*hysteria_proxy.Server); ok {
|
||||||
if v, ok := w.proxy.(HysteriaInboundValidator); ok {
|
ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator())
|
||||||
ctx = hyCtx.ContextWithRequireDatagram(ctx, true)
|
|
||||||
ctx = hyCtx.ContextWithValidator(ctx, v.HysteriaInboundValidator())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
|
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()
|
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.
|
// Dial implements internet.Dialer.
|
||||||
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
||||||
if h.senderSettings != nil {
|
if h.senderSettings != nil {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
|
|||||||
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
|
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
|
||||||
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
|
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
|
||||||
Tag: "tag",
|
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))
|
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
|
||||||
_, ok := conn.(*stat.CounterConnection)
|
_, ok := conn.(*stat.CounterConnection)
|
||||||
@@ -78,7 +78,7 @@ func TestOutboundWithStatCounter(t *testing.T) {
|
|||||||
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
|
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
|
||||||
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
|
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
|
||||||
Tag: "tag",
|
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))
|
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
|
||||||
_, ok := conn.(*stat.CounterConnection)
|
_, ok := conn.(*stat.CounterConnection)
|
||||||
@@ -118,7 +118,7 @@ func TestTagsCache(t *testing.T) {
|
|||||||
tag := fmt.Sprintf("%s%d", tags_prefix, idx)
|
tag := fmt.Sprintf("%s%d", tags_prefix, idx)
|
||||||
cfg := &core.OutboundHandlerConfig{
|
cfg := &core.OutboundHandlerConfig{
|
||||||
Tag: tag,
|
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 h, err := NewHandler(ctx, cfg); err == nil {
|
||||||
if err := ohm.AddHandler(ctx, h); 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"},
|
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"},
|
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/geodata"
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/features/routing"
|
"github.com/xtls/xray-core/features/routing"
|
||||||
|
"github.com/xtls/xray-core/features/routing/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Condition interface {
|
type Condition interface {
|
||||||
@@ -356,7 +357,13 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
|
|||||||
|
|
||||||
var dstIP string
|
var dstIP string
|
||||||
var dstPort uint16 = 0
|
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()
|
dstIP = ctx.GetTargetIPs()[0].String()
|
||||||
dstPort = uint16(ctx.GetTargetPort())
|
dstPort = uint16(ctx.GetTargetPort())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,25 +92,22 @@ func TestRoutingRule(t *testing.T) {
|
|||||||
Ip: []*geodata.IPRule{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{8, 8, 8, 8},
|
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
|
||||||
Prefix: 32,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{8, 8, 8, 8},
|
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
|
||||||
Prefix: 32,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
|
Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
|
||||||
Prefix: 128,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -140,9 +137,8 @@ func TestRoutingRule(t *testing.T) {
|
|||||||
SourceIp: []*geodata.IPRule{
|
SourceIp: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{192, 168, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
|
||||||
Prefix: 16,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -159,9 +159,8 @@ func TestIPOnDemand(t *testing.T) {
|
|||||||
Ip: []*geodata.IPRule{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{192, 168, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
|
||||||
Prefix: 16,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -204,9 +203,8 @@ func TestIPIfNonMatchDomain(t *testing.T) {
|
|||||||
Ip: []*geodata.IPRule{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{192, 168, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
|
||||||
Prefix: 16,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -249,9 +247,8 @@ func TestIPIfNonMatchIP(t *testing.T) {
|
|||||||
Ip: []*geodata.IPRule{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{127, 0, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
|
||||||
Prefix: 8,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DomainMatcher interface {
|
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
|
Match(input string) []uint32
|
||||||
|
|
||||||
MatchAny(input string) bool
|
MatchAny(input string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,10 +23,54 @@ type DomainMatcherFactory interface {
|
|||||||
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
|
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.
|
// BuildMatcher implements DomainMatcherFactory.
|
||||||
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
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()
|
g := strmatcher.NewMphValueMatcher()
|
||||||
for i, r := range rules {
|
for i, r := range rules {
|
||||||
switch v := r.Value.(type) {
|
switch v := r.Value.(type) {
|
||||||
@@ -53,25 +101,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
|
|||||||
if err := g.Build(); err != nil {
|
if err := g.Build(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if key != "" {
|
||||||
|
f.shared[key] = g
|
||||||
|
}
|
||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactDomainMatcherFactory struct {
|
type CompactDomainMatcherFactory struct {
|
||||||
sync.Mutex
|
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
|
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
|
||||||
|
|
||||||
f.Lock()
|
f.Lock()
|
||||||
defer f.Unlock()
|
defer f.Unlock()
|
||||||
|
|
||||||
if m := f.shared[key]; m != nil {
|
if s := f.shared[key]; s != nil {
|
||||||
return m, 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)
|
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
g.Add(m, 0)
|
s.Add(m)
|
||||||
}
|
}
|
||||||
f.shared[key] = g
|
f.shared[key] = s
|
||||||
return g, err
|
return s, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildMatcher implements DomainMatcherFactory.
|
// BuildMatcher implements DomainMatcherFactory.
|
||||||
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil, errors.New("empty domain rule list")
|
||||||
|
}
|
||||||
compact := &CompactDomainMatcher{
|
compact := &CompactDomainMatcher{
|
||||||
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
|
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
|
||||||
values: make([]uint32, 0, len(rules)),
|
values: make([]uint32, 0, len(rules)),
|
||||||
}
|
}
|
||||||
custom := strmatcher.NewLinearValueMatcher()
|
for i, r := range rules {
|
||||||
var idx uint32
|
|
||||||
for _, r := range rules {
|
|
||||||
switch v := r.Value.(type) {
|
switch v := r.Value.(type) {
|
||||||
case *DomainRule_Custom:
|
case *DomainRule_Custom:
|
||||||
m, err := parseDomain(v.Custom)
|
m, err := parseDomain(v.Custom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
custom.Add(m, 0)
|
if compact.custom == nil {
|
||||||
|
compact.custom = strmatcher.NewLinearValueMatcher()
|
||||||
|
}
|
||||||
|
compact.custom.Add(m, uint32(i))
|
||||||
case *DomainRule_Geosite:
|
case *DomainRule_Geosite:
|
||||||
m, err := f.getOrCreateFrom(v.Geosite)
|
m, err := f.getOrCreateFrom(v.Geosite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
compact.matchers = append(compact.matchers, m)
|
compact.matchers = append(compact.matchers, m)
|
||||||
compact.values = append(compact.values, idx)
|
compact.values = append(compact.values, uint32(i))
|
||||||
idx++
|
|
||||||
default:
|
default:
|
||||||
panic("unknown domain rule type")
|
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
|
return compact, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactDomainMatcher struct {
|
type CompactDomainMatcher struct {
|
||||||
matchers []strmatcher.MatcherGroup
|
custom strmatcher.ValueMatcher
|
||||||
|
matchers []strmatcher.MatcherSet
|
||||||
values []uint32
|
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.
|
// Match implements DomainMatcher.
|
||||||
func (c *CompactDomainMatcher) Match(input string) []uint32 {
|
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 {
|
for i, m := range c.matchers {
|
||||||
if m.MatchAny(input) {
|
if m.MatchAny(input) {
|
||||||
result = append(result, c.values[i])
|
result = append(result, c.values[i])
|
||||||
@@ -147,6 +198,9 @@ func (c *CompactDomainMatcher) Match(input string) []uint32 {
|
|||||||
|
|
||||||
// MatchAny implements DomainMatcher.
|
// MatchAny implements DomainMatcher.
|
||||||
func (c *CompactDomainMatcher) MatchAny(input string) bool {
|
func (c *CompactDomainMatcher) MatchAny(input string) bool {
|
||||||
|
if c.custom != nil && c.custom.MatchAny(input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
for _, m := range c.matchers {
|
for _, m := range c.matchers {
|
||||||
if m.MatchAny(input) {
|
if m.MatchAny(input) {
|
||||||
return true
|
return true
|
||||||
@@ -175,9 +229,9 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
|||||||
|
|
||||||
func newDomainMatcherFactory() DomainMatcherFactory {
|
func newDomainMatcherFactory() DomainMatcherFactory {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "ios":
|
case "ios", "android":
|
||||||
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||||
default:
|
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
|
package geodata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
)
|
||||||
|
|
||||||
type DomainRegistry struct {
|
type DomainRegistry struct {
|
||||||
factory DomainMatcherFactory
|
mu sync.Mutex
|
||||||
|
factory DomainMatcherFactory
|
||||||
|
matchers []*DynamicDomainMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
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 {
|
func newDomainRegistry() *DomainRegistry {
|
||||||
@@ -15,3 +63,32 @@ func newDomainRegistry() *DomainRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var DomainReg = newDomainRegistry()
|
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
|
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 {
|
type GeoIP struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
|
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||||
@@ -444,7 +496,7 @@ type GeoIP struct {
|
|||||||
|
|
||||||
func (x *GeoIP) Reset() {
|
func (x *GeoIP) Reset() {
|
||||||
*x = GeoIP{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -456,7 +508,7 @@ func (x *GeoIP) String() string {
|
|||||||
func (*GeoIP) ProtoMessage() {}
|
func (*GeoIP) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GeoIP) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -469,7 +521,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
|
||||||
func (*GeoIP) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *GeoIP) GetCode() string {
|
||||||
@@ -502,7 +554,7 @@ type GeoIPList struct {
|
|||||||
|
|
||||||
func (x *GeoIPList) Reset() {
|
func (x *GeoIPList) Reset() {
|
||||||
*x = GeoIPList{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -514,7 +566,7 @@ func (x *GeoIPList) String() string {
|
|||||||
func (*GeoIPList) ProtoMessage() {}
|
func (*GeoIPList) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -527,7 +579,7 @@ func (x *GeoIPList) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
|
||||||
func (*GeoIPList) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *GeoIPList) GetEntry() []*GeoIP {
|
||||||
@@ -548,7 +600,7 @@ type GeoIPRule struct {
|
|||||||
|
|
||||||
func (x *GeoIPRule) Reset() {
|
func (x *GeoIPRule) Reset() {
|
||||||
*x = GeoIPRule{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -560,7 +612,7 @@ func (x *GeoIPRule) String() string {
|
|||||||
func (*GeoIPRule) ProtoMessage() {}
|
func (*GeoIPRule) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -573,7 +625,7 @@ func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
|
||||||
func (*GeoIPRule) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *GeoIPRule) GetFile() string {
|
||||||
@@ -610,7 +662,7 @@ type IPRule struct {
|
|||||||
|
|
||||||
func (x *IPRule) Reset() {
|
func (x *IPRule) Reset() {
|
||||||
*x = IPRule{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -622,7 +674,7 @@ func (x *IPRule) String() string {
|
|||||||
func (*IPRule) ProtoMessage() {}
|
func (*IPRule) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IPRule) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -635,7 +687,7 @@ func (x *IPRule) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
|
||||||
func (*IPRule) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *IPRule) GetValue() isIPRule_Value {
|
||||||
@@ -654,7 +706,7 @@ func (x *IPRule) GetGeoip() *GeoIPRule {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IPRule) GetCustom() *CIDR {
|
func (x *IPRule) GetCustom() *CIDRRule {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
if x, ok := x.Value.(*IPRule_Custom); ok {
|
if x, ok := x.Value.(*IPRule_Custom); ok {
|
||||||
return x.Custom
|
return x.Custom
|
||||||
@@ -672,7 +724,7 @@ type IPRule_Geoip struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IPRule_Custom 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() {}
|
func (*IPRule_Geoip) isIPRule_Value() {}
|
||||||
@@ -693,7 +745,7 @@ type Domain_Attribute struct {
|
|||||||
|
|
||||||
func (x *Domain_Attribute) Reset() {
|
func (x *Domain_Attribute) Reset() {
|
||||||
*x = Domain_Attribute{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -705,7 +757,7 @@ func (x *Domain_Attribute) String() string {
|
|||||||
func (*Domain_Attribute) ProtoMessage() {}
|
func (*Domain_Attribute) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -807,7 +859,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
|
|||||||
"\x05value\".\n" +
|
"\x05value\".\n" +
|
||||||
"\x04CIDR\x12\x0e\n" +
|
"\x04CIDR\x12\x0e\n" +
|
||||||
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\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" +
|
"\x05GeoIP\x12\x12\n" +
|
||||||
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
|
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
|
||||||
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\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" +
|
"\tGeoIPRule\x12\x12\n" +
|
||||||
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
|
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
|
||||||
"\x04code\x18\x02 \x01(\tR\x04code\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" +
|
"\x06IPRule\x126\n" +
|
||||||
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x123\n" +
|
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" +
|
||||||
"\x06custom\x18\x02 \x01(\v2\x19.xray.common.geodata.CIDRH\x00R\x06customB\a\n" +
|
"\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" +
|
||||||
"\x05valueB[\n" +
|
"\x05valueB[\n" +
|
||||||
"\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3"
|
"\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_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{
|
var file_common_geodata_geodat_proto_goTypes = []any{
|
||||||
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
|
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
|
||||||
(*Domain)(nil), // 1: xray.common.geodata.Domain
|
(*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
|
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
|
||||||
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
|
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
|
||||||
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
|
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
|
||||||
(*GeoIP)(nil), // 7: xray.common.geodata.GeoIP
|
(*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule
|
||||||
(*GeoIPList)(nil), // 8: xray.common.geodata.GeoIPList
|
(*GeoIP)(nil), // 8: xray.common.geodata.GeoIP
|
||||||
(*GeoIPRule)(nil), // 9: xray.common.geodata.GeoIPRule
|
(*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList
|
||||||
(*IPRule)(nil), // 10: xray.common.geodata.IPRule
|
(*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule
|
||||||
(*Domain_Attribute)(nil), // 11: xray.common.geodata.Domain.Attribute
|
(*IPRule)(nil), // 11: xray.common.geodata.IPRule
|
||||||
|
(*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute
|
||||||
}
|
}
|
||||||
var file_common_geodata_geodat_proto_depIdxs = []int32{
|
var file_common_geodata_geodat_proto_depIdxs = []int32{
|
||||||
0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type
|
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
|
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
|
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
|
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
|
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
|
6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR
|
||||||
7, // 7: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
|
6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
|
||||||
9, // 8: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
|
8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
|
||||||
6, // 9: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDR
|
10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
|
||||||
10, // [10:10] is the sub-list for method output_type
|
7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule
|
||||||
10, // [10:10] is the sub-list for method input_type
|
11, // [11:11] is the sub-list for method output_type
|
||||||
10, // [10:10] is the sub-list for extension type_name
|
11, // [11:11] is the sub-list for method input_type
|
||||||
10, // [10:10] is the sub-list for extension extendee
|
11, // [11:11] is the sub-list for extension type_name
|
||||||
0, // [0:10] is the sub-list for field type_name
|
11, // [11:11] is the sub-list for extension extendee
|
||||||
|
0, // [0:11] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_common_geodata_geodat_proto_init() }
|
func init() { file_common_geodata_geodat_proto_init() }
|
||||||
@@ -879,11 +936,11 @@ func file_common_geodata_geodat_proto_init() {
|
|||||||
(*DomainRule_Geosite)(nil),
|
(*DomainRule_Geosite)(nil),
|
||||||
(*DomainRule_Custom)(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_Geoip)(nil),
|
||||||
(*IPRule_Custom)(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_BoolValue)(nil),
|
||||||
(*Domain_Attribute_IntValue)(nil),
|
(*Domain_Attribute_IntValue)(nil),
|
||||||
}
|
}
|
||||||
@@ -893,7 +950,7 @@ func file_common_geodata_geodat_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
|
||||||
NumEnums: 1,
|
NumEnums: 1,
|
||||||
NumMessages: 11,
|
NumMessages: 12,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ message CIDR {
|
|||||||
uint32 prefix = 2;
|
uint32 prefix = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CIDRRule {
|
||||||
|
CIDR cidr = 1;
|
||||||
|
bool reverse_match = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message GeoIP {
|
message GeoIP {
|
||||||
string code = 1;
|
string code = 1;
|
||||||
repeated CIDR cidr = 2;
|
repeated CIDR cidr = 2;
|
||||||
@@ -85,6 +90,6 @@ message GeoIPRule {
|
|||||||
message IPRule {
|
message IPRule {
|
||||||
oneof value {
|
oneof value {
|
||||||
GeoIPRule geoip = 1;
|
GeoIPRule geoip = 1;
|
||||||
CIDR custom = 2;
|
CIDRRule custom = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -816,8 +816,10 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
|
|||||||
defer f.Unlock()
|
defer f.Unlock()
|
||||||
|
|
||||||
if ipset := f.shared[key]; ipset != nil {
|
if ipset := f.shared[key]; ipset != nil {
|
||||||
|
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
|
||||||
return ipset, nil
|
return ipset, nil
|
||||||
}
|
}
|
||||||
|
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
|
||||||
|
|
||||||
ipset, err := f.createFrom(func(add func(*CIDR)) error {
|
ipset, err := f.createFrom(func(add func(*CIDR)) error {
|
||||||
for _, r := range rules {
|
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)
|
return nil, errors.New("failed to build IPv6 set").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var has4, has6 bool
|
||||||
var max4, max6 int
|
var max4, max6 int
|
||||||
|
|
||||||
for _, p := range ipv4.Prefixes() {
|
for _, p := range ipv4.Prefixes() {
|
||||||
|
has4 = true
|
||||||
if b := p.Bits(); b > max4 {
|
if b := p.Bits(); b > max4 {
|
||||||
max4 = b
|
max4 = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, p := range ipv6.Prefixes() {
|
for _, p := range ipv6.Prefixes() {
|
||||||
|
has6 = true
|
||||||
if b := p.Bits(); b > max6 {
|
if b := p.Bits(); b > max6 {
|
||||||
max6 = b
|
max6 = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if max4 == 0 {
|
if !has4 {
|
||||||
max4 = 0xff
|
max4 = 0xff
|
||||||
|
} else if max4 == 0 {
|
||||||
|
max4 = 0xfe
|
||||||
}
|
}
|
||||||
if max6 == 0 {
|
if !has6 {
|
||||||
max6 = 0xff
|
max6 = 0xff
|
||||||
|
} else if max6 == 0 {
|
||||||
|
max6 = 0xfe
|
||||||
}
|
}
|
||||||
|
|
||||||
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
|
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) {
|
func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) {
|
||||||
n := len(rules)
|
n := len(rules)
|
||||||
custom := make([]*CIDR, 0, n)
|
posCustom := make([]*CIDR, 0, n)
|
||||||
pos := make([]*GeoIPRule, 0, n)
|
negCustom := make([]*CIDR, 0, n)
|
||||||
neg := make([]*GeoIPRule, 0, n)
|
posGeoip := make([]*GeoIPRule, 0, n)
|
||||||
|
negGeoip := make([]*GeoIPRule, 0, n)
|
||||||
|
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
switch v := r.Value.(type) {
|
switch v := r.Value.(type) {
|
||||||
case *IPRule_Custom:
|
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:
|
case *IPRule_Geoip:
|
||||||
if !v.Geoip.ReverseMatch {
|
if !v.Geoip.ReverseMatch {
|
||||||
pos = append(pos, v.Geoip)
|
posGeoip = append(posGeoip, v.Geoip)
|
||||||
} else {
|
} else {
|
||||||
neg = append(neg, v.Geoip)
|
negGeoip = append(negGeoip, v.Geoip)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
panic("unknown ip rule type")
|
panic("unknown ip rule type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subs := make([]*HeuristicIPMatcher, 0, 3)
|
subs := make([]*HeuristicIPMatcher, 0, 4)
|
||||||
|
|
||||||
if len(custom) > 0 {
|
if len(posCustom) > 0 {
|
||||||
ipset, err := f.CreateFromCIDRs(custom)
|
ipset, err := f.CreateFromCIDRs(posCustom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
|
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pos) > 0 {
|
if len(negCustom) > 0 {
|
||||||
ipset, err := f.GetOrCreateFromGeoIPRules(pos)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
|
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(neg) > 0 {
|
if len(negGeoip) > 0 {
|
||||||
ipset, err := f.GetOrCreateFromGeoIPRules(neg)
|
ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -994,3 +1016,7 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
|
|||||||
return &HeuristicMultiIPMatcher{matchers: subs}, nil
|
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) {
|
func TestIPMatcherRegression(t *testing.T) {
|
||||||
matcher := buildIPMatcher(
|
matcher := buildIPMatcher(
|
||||||
"98.108.20.0/22",
|
"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) {
|
func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
|
||||||
matcher := buildIPMatcher(
|
matcher := buildIPMatcher(
|
||||||
"8.8.8.8/32",
|
"8.8.8.8/32",
|
||||||
|
|||||||
@@ -1,17 +1,135 @@
|
|||||||
package geodata
|
package geodata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/net"
|
||||||
|
)
|
||||||
|
|
||||||
type IPRegistry struct {
|
type IPRegistry struct {
|
||||||
|
mu sync.Mutex
|
||||||
ipsetFactory *IPSetFactory
|
ipsetFactory *IPSetFactory
|
||||||
|
matchers []*DynamicIPMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
|
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 {
|
func newIPRegistry() *IPRegistry {
|
||||||
return &IPRegistry{
|
return &IPRegistry{
|
||||||
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
|
ipsetFactory: newIPSetFactory(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var IPReg = newIPRegistry()
|
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
|
var ipRules []*IPRule
|
||||||
|
|
||||||
for i, r := range rules {
|
for i, r := range rules {
|
||||||
|
r, reverse := cutReversePrefix(r)
|
||||||
|
|
||||||
if strings.HasPrefix(r, "geoip:") {
|
if strings.HasPrefix(r, "geoip:") {
|
||||||
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
|
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
|
||||||
}
|
}
|
||||||
@@ -32,9 +34,9 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
|
|||||||
var rule isIPRule_Value
|
var rule isIPRule_Value
|
||||||
var err error
|
var err error
|
||||||
if prefix > 0 {
|
if prefix > 0 {
|
||||||
rule, err = parseGeoIPRule(r[prefix:])
|
rule, err = parseGeoIPRule(r[prefix:], reverse)
|
||||||
} else {
|
} else {
|
||||||
rule, err = parseCustomIPRule(r)
|
rule, err = parseCustomIPRule(r, reverse)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
|
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
|
||||||
@@ -45,7 +47,16 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
|
|||||||
return ipRules, nil
|
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, ":")
|
file, code, ok := strings.Cut(rule, ":")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("syntax error")
|
return nil, errors.New("syntax error")
|
||||||
@@ -55,11 +66,8 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
|
|||||||
return nil, errors.New("empty file")
|
return nil, errors.New("empty file")
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse := false
|
code, codeReverse := cutReversePrefix(code)
|
||||||
if strings.HasPrefix(code, "!") {
|
reverse = reverse != codeReverse
|
||||||
code = code[1:]
|
|
||||||
reverse = true
|
|
||||||
}
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
return nil, errors.New("empty code")
|
return nil, errors.New("empty code")
|
||||||
}
|
}
|
||||||
@@ -78,13 +86,16 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCustomIPRule(rule string) (*IPRule_Custom, error) {
|
func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
|
||||||
cidr, err := parseCIDR(rule)
|
cidr, err := parseCIDR(rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &IPRule_Custom{
|
return &IPRule_Custom{
|
||||||
Custom: cidr,
|
Custom: &CIDRRule{
|
||||||
|
Cidr: cidr,
|
||||||
|
ReverseMatch: reverse,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,20 @@ func TestParseIPRules(t *testing.T) {
|
|||||||
rules := []string{
|
rules := []string{
|
||||||
"geoip:us",
|
"geoip:us",
|
||||||
"geoip:cn",
|
"geoip:cn",
|
||||||
|
"!geoip:cn",
|
||||||
|
"!!geoip:cn",
|
||||||
"geoip:!cn",
|
"geoip:!cn",
|
||||||
|
"geoip:!!cn",
|
||||||
|
"!geoip:!cn",
|
||||||
"ext:geoip.dat:!cn",
|
"ext:geoip.dat:!cn",
|
||||||
|
"ext:geoip.dat:!!cn",
|
||||||
"ext:geoip.dat:ca",
|
"ext:geoip.dat:ca",
|
||||||
"ext-ip:geoip.dat:!cn",
|
"ext-ip:geoip.dat:!cn",
|
||||||
"ext-ip:geoip.dat:!ca",
|
"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.0/24",
|
||||||
"192.168.0.1",
|
"192.168.0.1",
|
||||||
"fe80::/64",
|
"fe80::/64",
|
||||||
"fe80::",
|
"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) {
|
func TestParseDomainRules(t *testing.T) {
|
||||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -99,10 +100,6 @@ func (t Type) New(pattern string) (Matcher, error) {
|
|||||||
case Substr:
|
case Substr:
|
||||||
return SubstrMatcher(pattern), nil
|
return SubstrMatcher(pattern), nil
|
||||||
case Domain:
|
case Domain:
|
||||||
pattern, err := ToDomain(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return DomainMatcher(pattern), nil
|
return DomainMatcher(pattern), nil
|
||||||
case Regex: // 1. regex matching is case-sensitive
|
case Regex: // 1. regex matching is case-sensitive
|
||||||
regex, err := regexp.Compile(pattern)
|
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.
|
// 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 {
|
func CompositeMatches(matches [][]uint32) []uint32 {
|
||||||
switch len(matches) {
|
switch len(matches) {
|
||||||
case 0:
|
case 0:
|
||||||
return nil
|
return nil
|
||||||
case 1:
|
case 1:
|
||||||
return matches[0]
|
return slices.Clone(matches[0])
|
||||||
default:
|
default:
|
||||||
result := make([]uint32, 0, 5)
|
result := make([]uint32, 0, 5)
|
||||||
for i := 0; i < len(matches); i++ {
|
for i := 0; i < len(matches); i++ {
|
||||||
@@ -288,3 +284,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
|
|||||||
return result
|
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.
|
// 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 Matcher interface {
|
||||||
// Type returns the matcher's type.
|
// Type returns the matcher's type.
|
||||||
Type() Type
|
Type() Type
|
||||||
@@ -62,6 +62,7 @@ type IndexMatcher interface {
|
|||||||
// Match returns the indices of all matchers that matches the input.
|
// Match returns the indices of all matchers that matches the input.
|
||||||
// * Empty array is returned if no such matcher exists.
|
// * Empty array is returned if no such matcher exists.
|
||||||
// * The order of returned matchers should follow priority specification.
|
// * The order of returned matchers should follow priority specification.
|
||||||
|
// * The returned slice is owned by the caller and may be safely modified.
|
||||||
// Priority specification:
|
// Priority specification:
|
||||||
// 1. Priority between matcher types: full > domain > substr > regex.
|
// 1. Priority between matcher types: full > domain > substr > regex.
|
||||||
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
|
// 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.
|
// * Empty array is returned if no such matcher exists.
|
||||||
// * The order of returned values should follow priority specification.
|
// * The order of returned values should follow priority specification.
|
||||||
// * Same value may appear multiple times if multiple matched matchers were added with that value.
|
// * 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:
|
// Priority specification:
|
||||||
// 1. Priority between matcher types: full > domain > substr > regex.
|
// 1. Priority between matcher types: full > domain > substr > regex.
|
||||||
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
|
// 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 returns true as soon as one matching matcher is found.
|
||||||
MatchAny(input string) bool
|
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
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -26,11 +27,48 @@ func ReadFile(path string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReadAsset(file string) ([]byte, error) {
|
func ReadAsset(file string) ([]byte, error) {
|
||||||
return ReadFile(platform.GetAssetLocation(file))
|
path, _, err := getAssetFileLocation(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ReadFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenAsset(file string) (io.ReadCloser, error) {
|
func OpenAsset(file string) (io.ReadCloser, error) {
|
||||||
return NewFileReader(platform.GetAssetLocation(file))
|
path, _, err := getAssetFileLocation(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewFileReader(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StatAsset(file string) (os.FileInfo, error) {
|
||||||
|
_, info, err := getAssetFileLocation(file)
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveAsset(file string) (string, error) {
|
||||||
|
path, _, err := getAssetFileLocation(file)
|
||||||
|
return path, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
|
||||||
|
if !filepath.IsLocal(file) || file == "." {
|
||||||
|
return "", nil, errors.New("asset path must stay in asset directory")
|
||||||
|
}
|
||||||
|
local, err := filepath.Localize(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
path := platform.GetAssetLocation(local)
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return "", nil, errors.New("asset is not a regular file")
|
||||||
|
}
|
||||||
|
return path, info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadCert(file string) ([]byte, error) {
|
func ReadCert(file string) ([]byte, error) {
|
||||||
|
|||||||
@@ -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"
|
UseFreedomSplice = "xray.buf.splice"
|
||||||
UseVmessPadding = "xray.vmess.padding"
|
UseVmessPadding = "xray.vmess.padding"
|
||||||
UseCone = "xray.cone.disabled"
|
UseCone = "xray.cone.disabled"
|
||||||
|
UseStrictJSON = "xray.json.strict"
|
||||||
|
|
||||||
BufferSize = "xray.ray.buffer.size"
|
BufferSize = "xray.ray.buffer.size"
|
||||||
BrowserDialerAddress = "xray.browser.dialer"
|
BrowserDialerAddress = "xray.browser.dialer"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/xtls/xray-core/common/serial"
|
. "github.com/xtls/xray-core/common/serial"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetInstance(t *testing.T) {
|
func TestGetInstance(t *testing.T) {
|
||||||
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
|
|||||||
t.Error("expect nil, but actually not")
|
t.Error("expect nil, but actually not")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) {
|
||||||
|
msg := &custom.UDPConfig{
|
||||||
|
Mode: "standalone",
|
||||||
|
Client: []*custom.UDPItem{
|
||||||
|
{Rand: 12, Save: "txid"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := ToTypedMessage(msg)
|
||||||
|
if tm == nil {
|
||||||
|
t.Fatal("expected typed message")
|
||||||
|
}
|
||||||
|
|
||||||
|
roundTrip, err := tm.GetInstance()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetInstance() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
udp, ok := roundTrip.(*custom.UDPConfig)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected round-trip type: %T", roundTrip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if udp.GetMode() != "standalone" {
|
||||||
|
t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
return
|
||||||
}
|
}
|
||||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
|
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 := blake3.New(8, BaseKey)
|
||||||
h.Write([]byte(inbound.Source.String()))
|
h.Write([]byte(inbound.Source.String()))
|
||||||
copy(globalID[:], h.Sum(nil))
|
copy(globalID[:], h.Sum(nil))
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
Version_x byte = 26
|
Version_x byte = 26
|
||||||
Version_y byte = 4
|
Version_y byte = 4
|
||||||
Version_z byte = 13
|
Version_z byte = 25
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ func TestXrayDial(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Outbound: []*core.OutboundHandlerConfig{
|
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{
|
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{
|
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{
|
d := &net.Dialer{
|
||||||
Timeout: time.Second * 16,
|
Timeout: time.Second * 16,
|
||||||
Control: func(network, address string, c syscall.RawConn) error {
|
Control: func(network, address string, c syscall.RawConn) error {
|
||||||
|
var errs []error
|
||||||
for _, ctl := range internet.Controllers {
|
for _, ctl := range internet.Controllers {
|
||||||
if err := ctl(network, address, c); err != nil {
|
if err := ctl(network, address, c); err != nil {
|
||||||
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
|
errs = append(errs, err)
|
||||||
return 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
|
go 1.26
|
||||||
|
|
||||||
require (
|
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/cloudflare/circl v1.6.3
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||||
github.com/golang/mock v1.7.0-rc.1
|
github.com/golang/mock v1.7.0-rc.1
|
||||||
@@ -12,8 +12,9 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.3.0
|
github.com/klauspost/cpuid/v2 v2.3.0
|
||||||
github.com/miekg/dns v1.1.72
|
github.com/miekg/dns v1.1.72
|
||||||
github.com/pelletier/go-toml v1.9.5
|
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/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 v0.5.1
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
@@ -27,7 +28,7 @@ require (
|
|||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
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/grpc v1.80.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
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 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
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.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
|
||||||
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/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
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/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 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
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.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
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 h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
||||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
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=
|
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/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 h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
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 v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
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 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
|
|||||||
+129
-12
@@ -1,19 +1,70 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/proxy/dns"
|
"github.com/xtls/xray-core/proxy/dns"
|
||||||
"google.golang.org/protobuf/proto"
|
"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 {
|
type DNSOutboundConfig struct {
|
||||||
Network Network `json:"network"`
|
Network Network `json:"network"`
|
||||||
Address *Address `json:"address"`
|
Address *Address `json:"address"`
|
||||||
Port uint16 `json:"port"`
|
Port uint16 `json:"port"`
|
||||||
UserLevel uint32 `json:"userLevel"`
|
UserLevel uint32 `json:"userLevel"`
|
||||||
NonIPQuery string `json:"nonIPQuery"`
|
Rules []*DNSOutboundRuleConfig `json:"rules"`
|
||||||
BlockTypes []int32 `json:"blockTypes"`
|
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
|
||||||
|
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||||
@@ -27,12 +78,78 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
|||||||
if c.Address != nil {
|
if c.Address != nil {
|
||||||
config.Server.Address = c.Address.Build()
|
config.Server.Address = c.Address.Build()
|
||||||
}
|
}
|
||||||
switch c.NonIPQuery {
|
|
||||||
case "", "reject", "drop", "skip":
|
// todo: remove legacy
|
||||||
default:
|
if c.NonIPQuery != nil || c.BlockTypes != nil {
|
||||||
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
|
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
|
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
|
package conf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
. "github.com/xtls/xray-core/infra/conf"
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
"github.com/xtls/xray-core/proxy/dns"
|
"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
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"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/common/protocol"
|
||||||
"github.com/xtls/xray-core/proxy/freedom"
|
"github.com/xtls/xray-core/proxy/freedom"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
@@ -15,14 +17,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FreedomConfig struct {
|
type FreedomConfig struct {
|
||||||
TargetStrategy string `json:"targetStrategy"`
|
TargetStrategy string `json:"targetStrategy"`
|
||||||
DomainStrategy string `json:"domainStrategy"`
|
DomainStrategy string `json:"domainStrategy"`
|
||||||
Redirect string `json:"redirect"`
|
Redirect string `json:"redirect"`
|
||||||
UserLevel uint32 `json:"userLevel"`
|
UserLevel uint32 `json:"userLevel"`
|
||||||
Fragment *Fragment `json:"fragment"`
|
Fragment *Fragment `json:"fragment"`
|
||||||
Noise *Noise `json:"noise"`
|
Noise *Noise `json:"noise"`
|
||||||
Noises []*Noise `json:"noises"`
|
Noises []*Noise `json:"noises"`
|
||||||
ProxyProtocol uint32 `json:"proxyProtocol"`
|
ProxyProtocol uint32 `json:"proxyProtocol"`
|
||||||
|
IPsBlocked *StringList `json:"ipsBlocked"`
|
||||||
|
FinalRules []*FreedomFinalRuleConfig `json:"finalRules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Fragment struct {
|
type Fragment struct {
|
||||||
@@ -39,8 +43,21 @@ type Noise struct {
|
|||||||
ApplyTo string `json:"applyTo"`
|
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
|
// Build implements Buildable
|
||||||
func (c *FreedomConfig) Build() (proto.Message, error) {
|
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)
|
config := new(freedom.Config)
|
||||||
targetStrategy := c.TargetStrategy
|
targetStrategy := c.TargetStrategy
|
||||||
if targetStrategy == "" {
|
if targetStrategy == "" {
|
||||||
@@ -140,12 +157,13 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.UserLevel = c.UserLevel
|
config.UserLevel = c.UserLevel
|
||||||
|
|
||||||
if len(c.Redirect) > 0 {
|
if len(c.Redirect) > 0 {
|
||||||
host, portStr, err := net.SplitHostPort(c.Redirect)
|
host, portStr, err := net.SplitHostPort(c.Redirect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid redirect address: ", c.Redirect, ": ", err).Base(err)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.New("invalid redirect port: ", c.Redirect, ": ", err).Base(err)
|
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 {
|
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 {
|
if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 {
|
||||||
config.ProxyProtocol = c.ProxyProtocol
|
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
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,3 +248,41 @@ func ParseNoise(noise *Noise) (*freedom.Noise, error) {
|
|||||||
}
|
}
|
||||||
return NConfig, nil
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
. "github.com/xtls/xray-core/infra/conf"
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
@@ -38,5 +39,64 @@ func TestFreedomConfig(t *testing.T) {
|
|||||||
UserLevel: 1,
|
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/errors"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"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"
|
||||||
"github.com/xtls/xray-core/proxy/hysteria/account"
|
"github.com/xtls/xray-core/proxy/hysteria/account"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -44,16 +45,22 @@ type HysteriaServerConfig struct {
|
|||||||
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
|
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
|
||||||
config := new(hysteria.ServerConfig)
|
config := new(hysteria.ServerConfig)
|
||||||
|
|
||||||
if c.Users != nil {
|
if len(c.Users) > 0 {
|
||||||
for _, user := range c.Users {
|
config.Users = make([]*protocol.User, len(c.Users))
|
||||||
account := &account.Account{
|
processUser := func(idx int) error {
|
||||||
|
user := c.Users[idx]
|
||||||
|
acc := &account.Account{
|
||||||
Auth: user.Auth,
|
Auth: user.Auth,
|
||||||
}
|
}
|
||||||
config.Users = append(config.Users, &protocol.User{
|
config.Users[idx] = &protocol.User{
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Level: user.Level,
|
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{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{10, 0, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
|
||||||
Prefix: 8,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
|
||||||
Prefix: 128,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -216,17 +214,15 @@ func TestRouterConfig(t *testing.T) {
|
|||||||
Ip: []*geodata.IPRule{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{10, 0, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
|
||||||
Prefix: 8,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
|
||||||
Prefix: 128,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,12 +5,22 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/platform"
|
||||||
creflect "github.com/xtls/xray-core/common/reflect"
|
creflect "github.com/xtls/xray-core/common/reflect"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
"github.com/xtls/xray-core/infra/conf"
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
"github.com/xtls/xray-core/main/confloader"
|
"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) {
|
func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) {
|
||||||
c, err := mergeConfigs(files)
|
c, err := mergeConfigs(files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -31,7 +41,11 @@ func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to read config: ", file).Base(err)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.New("failed to decode config: ", file).Base(err)
|
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
|
// DecodeJSONConfig reads from reader and decode the config into *conf.Config
|
||||||
// syntax error could be detected.
|
// 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) {
|
func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
|
||||||
jsonConfig := &conf.Config{}
|
jsonConfig := &conf.Config{}
|
||||||
|
|
||||||
@@ -69,6 +72,24 @@ func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) {
|
|||||||
return jsonConfig, nil
|
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) {
|
func LoadJSONConfig(reader io.Reader) (*core.Config, error) {
|
||||||
jsonConfig, err := DecodeJSONConfig(reader)
|
jsonConfig, err := DecodeJSONConfig(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+34
-19
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"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"
|
||||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -59,23 +60,31 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
|||||||
config.Network = v.NetworkList.Build()
|
config.Network = v.NetworkList.Build()
|
||||||
|
|
||||||
if v.Users != nil {
|
if v.Users != nil {
|
||||||
for _, user := range v.Users {
|
if len(v.Users) > 0 {
|
||||||
account := &shadowsocks.Account{
|
config.Users = make([]*protocol.User, len(v.Users))
|
||||||
Password: user.Password,
|
processUser := func(idx int) error {
|
||||||
CipherType: cipherFromString(user.Cipher),
|
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 == "" {
|
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
|
||||||
return nil, errors.New("Shadowsocks password is not specified.")
|
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 {
|
} else {
|
||||||
account := &shadowsocks.Account{
|
account := &shadowsocks.Account{
|
||||||
@@ -121,18 +130,24 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
|||||||
config.Key = v.Password
|
config.Key = v.Password
|
||||||
config.Network = v.NetworkList.Build()
|
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 != "" {
|
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{
|
account := &shadowsocks_2022.Account{
|
||||||
Key: user.Password,
|
Key: user.Password,
|
||||||
}
|
}
|
||||||
config.Users = append(config.Users, &protocol.User{
|
config.Users[idx] = &protocol.User{
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Level: uint32(user.Level),
|
Level: uint32(user.Level),
|
||||||
Account: serial.ToTypedMessage(account),
|
Account: serial.ToTypedMessage(account),
|
||||||
})
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := task.ParallelForN(len(v.Users), processUser); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1231,6 +1232,8 @@ func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||||
|
|
||||||
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||||
"fragment": func() interface{} { return new(FragmentMask) },
|
"fragment": func() interface{} { return new(FragmentMask) },
|
||||||
@@ -1256,11 +1259,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TCPItem struct {
|
type TCPItem struct {
|
||||||
Delay Int32Range `json:"delay"`
|
Delay Int32Range `json:"delay"`
|
||||||
Rand int32 `json:"rand"`
|
Rand int32 `json:"rand"`
|
||||||
RandRange *Int32Range `json:"randRange"`
|
RandRange *Int32Range `json:"randRange"`
|
||||||
Type string `json:"type"`
|
Capture string `json:"capture"`
|
||||||
Packet json.RawMessage `json:"packet"`
|
Type string `json:"type"`
|
||||||
|
Reuse string `json:"reuse"`
|
||||||
|
Transform *CustomTransform `json:"transform"`
|
||||||
|
Packet json.RawMessage `json:"packet"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeaderCustomTCP struct {
|
type HeaderCustomTCP struct {
|
||||||
@@ -1272,22 +1278,22 @@ type HeaderCustomTCP struct {
|
|||||||
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||||
for _, value := range c.Clients {
|
for _, value := range c.Clients {
|
||||||
for _, item := range value {
|
for _, item := range value {
|
||||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, value := range c.Servers {
|
for _, value := range c.Servers {
|
||||||
for _, item := range value {
|
for _, item := range value {
|
||||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, value := range c.Errors {
|
for _, value := range c.Errors {
|
||||||
for _, item := range value {
|
for _, item := range value {
|
||||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
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 {
|
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
transform, err := buildCustomTransform(item.Transform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
|
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
|
||||||
DelayMin: int64(item.Delay.From),
|
DelayMin: int64(item.Delay.From),
|
||||||
DelayMax: int64(item.Delay.To),
|
DelayMax: int64(item.Delay.To),
|
||||||
@@ -1315,6 +1325,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
|||||||
RandMin: item.RandRange.From,
|
RandMin: item.RandRange.From,
|
||||||
RandMax: item.RandRange.To,
|
RandMax: item.RandRange.To,
|
||||||
Packet: item.Packet,
|
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 {
|
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
transform, err := buildCustomTransform(item.Transform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
|
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
|
||||||
DelayMin: int64(item.Delay.From),
|
DelayMin: int64(item.Delay.From),
|
||||||
DelayMax: int64(item.Delay.To),
|
DelayMax: int64(item.Delay.To),
|
||||||
@@ -1340,6 +1357,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
|||||||
RandMin: item.RandRange.From,
|
RandMin: item.RandRange.From,
|
||||||
RandMax: item.RandRange.To,
|
RandMax: item.RandRange.To,
|
||||||
Packet: item.Packet,
|
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 {
|
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
transform, err := buildCustomTransform(item.Transform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
|
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
|
||||||
DelayMin: int64(item.Delay.From),
|
DelayMin: int64(item.Delay.From),
|
||||||
DelayMax: int64(item.Delay.To),
|
DelayMax: int64(item.Delay.To),
|
||||||
@@ -1365,6 +1389,9 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
|||||||
RandMin: item.RandRange.From,
|
RandMin: item.RandRange.From,
|
||||||
RandMax: item.RandRange.To,
|
RandMax: item.RandRange.To,
|
||||||
Packet: item.Packet,
|
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 {
|
type UDPItem struct {
|
||||||
Rand int32 `json:"rand"`
|
Rand int32 `json:"rand"`
|
||||||
RandRange *Int32Range `json:"randRange"`
|
RandRange *Int32Range `json:"randRange"`
|
||||||
Type string `json:"type"`
|
Capture string `json:"capture"`
|
||||||
Packet json.RawMessage `json:"packet"`
|
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 {
|
type HeaderCustomUDP struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
Client []UDPItem `json:"client"`
|
Client []UDPItem `json:"client"`
|
||||||
Server []UDPItem `json:"server"`
|
Server []UDPItem `json:"server"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HeaderCustomUDP) Build() (proto.Message, error) {
|
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 {
|
for _, item := range c.Client {
|
||||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, item := range c.Server {
|
for _, item := range c.Server {
|
||||||
if len(item.Packet) > 0 && item.Rand > 0 {
|
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0")
|
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 {
|
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
transform, err := buildCustomTransform(item.Transform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
client = append(client, &custom.UDPItem{
|
client = append(client, &custom.UDPItem{
|
||||||
Rand: item.Rand,
|
Rand: item.Rand,
|
||||||
RandMin: item.RandRange.From,
|
RandMin: item.RandRange.From,
|
||||||
RandMax: item.RandRange.To,
|
RandMax: item.RandRange.To,
|
||||||
Packet: item.Packet,
|
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 {
|
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
transform, err := buildCustomTransform(item.Transform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
server = append(server, &custom.UDPItem{
|
server = append(server, &custom.UDPItem{
|
||||||
Rand: item.Rand,
|
Rand: item.Rand,
|
||||||
RandMin: item.RandRange.From,
|
RandMin: item.RandRange.From,
|
||||||
RandMax: item.RandRange.To,
|
RandMax: item.RandRange.To,
|
||||||
Packet: item.Packet,
|
Packet: item.Packet,
|
||||||
|
Save: item.Capture,
|
||||||
|
Var: item.Reuse,
|
||||||
|
Expr: transform,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return &custom.UDPConfig{
|
return &custom.UDPConfig{
|
||||||
Client: client,
|
Client: client,
|
||||||
Server: server,
|
Server: server,
|
||||||
|
Mode: c.Mode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package conf_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/xtls/xray-core/infra/conf"
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
|
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,3 +158,137 @@ func TestSocketConfig(t *testing.T) {
|
|||||||
t.Fatalf("unexpected parsed TFO value, which should be -1")
|
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/net"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
|
"github.com/xtls/xray-core/common/task"
|
||||||
"github.com/xtls/xray-core/proxy/trojan"
|
"github.com/xtls/xray-core/proxy/trojan"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -123,9 +124,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
|||||||
Users: make([]*protocol.User, len(c.Clients)),
|
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 != "" {
|
if rawUser.Flow != "" {
|
||||||
return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
|
return errors.PrintRemovedFeatureError(`Flow for Trojan`, ``)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Users[idx] = &protocol.User{
|
config.Users[idx] = &protocol.User{
|
||||||
@@ -135,6 +137,10 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
|||||||
Password: rawUser.Password,
|
Password: rawUser.Password,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := task.ParallelForN(len(c.Clients), processClient); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fb := range c.Fallbacks {
|
for _, fb := range c.Fallbacks {
|
||||||
|
|||||||
+3
-6
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type TunConfig struct {
|
type TunConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MTU []uint32 `json:"mtu"`
|
MTU uint32 `json:"mtu"`
|
||||||
Gateway []string `json:"gateway"`
|
Gateway []string `json:"gateway"`
|
||||||
DNS []string `json:"dns"`
|
DNS []string `json:"dns"`
|
||||||
UserLevel uint32 `json:"userLevel"`
|
UserLevel uint32 `json:"userLevel"`
|
||||||
@@ -34,11 +34,8 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
|||||||
if config.Name == "" {
|
if config.Name == "" {
|
||||||
config.Name = "xray0"
|
config.Name = "xray0"
|
||||||
}
|
}
|
||||||
if len(config.MTU) == 0 {
|
if config.MTU == 0 {
|
||||||
config.MTU = []uint32{1500, 1280}
|
config.MTU = 1500
|
||||||
}
|
|
||||||
if len(config.MTU) == 1 {
|
|
||||||
config.MTU = append(config.MTU, config.MTU[0])
|
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-8
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"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/common/uuid"
|
||||||
"github.com/xtls/xray-core/proxy/vless"
|
"github.com/xtls/xray-core/proxy/vless"
|
||||||
"github.com/xtls/xray-core/proxy/vless/inbound"
|
"github.com/xtls/xray-core/proxy/vless/inbound"
|
||||||
@@ -46,19 +47,20 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
|||||||
default:
|
default:
|
||||||
return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`)
|
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)
|
user := new(protocol.User)
|
||||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
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)
|
account := new(vless.Account)
|
||||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
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)
|
u, err := uuid.ParseString(account.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
account.Id = u.String()
|
account.Id = u.String()
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
|||||||
account.Flow = c.Flow
|
account.Flow = c.Flow
|
||||||
case vless.XRV:
|
case vless.XRV:
|
||||||
default:
|
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 {
|
if len(account.Testseed) < 4 {
|
||||||
@@ -75,20 +77,25 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if account.Encryption != "" {
|
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 != nil {
|
||||||
if account.Reverse.Tag == "" {
|
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
|
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)
|
user.Account = serial.ToTypedMessage(account)
|
||||||
config.Clients[idx] = user
|
config.Clients[idx] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := task.ParallelForN(len(c.Clients), processClient); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Decryption = c.Decryption
|
config.Decryption = c.Decryption
|
||||||
|
|||||||
+10
-4
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"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/common/uuid"
|
||||||
"github.com/xtls/xray-core/proxy/vmess"
|
"github.com/xtls/xray-core/proxy/vmess"
|
||||||
"github.com/xtls/xray-core/proxy/vmess/inbound"
|
"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))
|
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)
|
user := new(protocol.User)
|
||||||
if err := json.Unmarshal(rawData, user); err != nil {
|
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)
|
account := new(VMessAccount)
|
||||||
if err := json.Unmarshal(rawData, account); err != nil {
|
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)
|
u, err := uuid.ParseString(account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
account.ID = u.String()
|
account.ID = u.String()
|
||||||
|
|
||||||
user.Account = serial.ToTypedMessage(account.Build())
|
user.Account = serial.ToTypedMessage(account.Build())
|
||||||
config.User[idx] = user
|
config.User[idx] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := task.ParallelForN(len(c.Users), processUser); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ type Config struct {
|
|||||||
Observatory *ObservatoryConfig `json:"observatory"`
|
Observatory *ObservatoryConfig `json:"observatory"`
|
||||||
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
|
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
|
||||||
Version *VersionConfig `json:"version"`
|
Version *VersionConfig `json:"version"`
|
||||||
|
Geodata *GeodataConfig `json:"geodata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) findInboundTag(tag string) int {
|
func (c *Config) findInboundTag(tag string) int {
|
||||||
@@ -433,6 +434,10 @@ func (c *Config) Override(o *Config, fn string) {
|
|||||||
c.Version = o.Version
|
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
|
// update the Inbound in slice if the only one in override config has same tag
|
||||||
if len(o.InboundConfigs) > 0 {
|
if len(o.InboundConfigs) > 0 {
|
||||||
for i := range o.InboundConfigs {
|
for i := range o.InboundConfigs {
|
||||||
@@ -542,6 +547,7 @@ func (c *Config) Build() (*core.Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Reverse != nil {
|
if c.Reverse != nil {
|
||||||
|
return nil, errors.PrintRemovedFeatureError(`"legacy reverse"`, `"VLESS Reverse Proxy"`)
|
||||||
r, err := c.Reverse.Build()
|
r, err := c.Reverse.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to build reverse configuration").Base(err)
|
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))
|
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
|
var inbounds []InboundDetourConfig
|
||||||
|
|
||||||
if len(c.InboundConfigs) > 0 {
|
if len(c.InboundConfigs) > 0 {
|
||||||
|
|||||||
@@ -99,9 +99,8 @@ func TestXrayConfig(t *testing.T) {
|
|||||||
Ip: []*geodata.IPRule{
|
Ip: []*geodata.IPRule{
|
||||||
{
|
{
|
||||||
Value: &geodata.IPRule_Custom{
|
Value: &geodata.IPRule_Custom{
|
||||||
Custom: &geodata.CIDR{
|
Custom: &geodata.CIDRRule{
|
||||||
Ip: []byte{10, 0, 0, 0},
|
Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
|
||||||
Prefix: 8,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -216,8 +215,12 @@ func TestSniffingConfig_Build(t *testing.T) {
|
|||||||
if rule == nil {
|
if rule == nil {
|
||||||
t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i)
|
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 {
|
cidr := rule.GetCidr()
|
||||||
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)
|
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.
|
// Other optional features.
|
||||||
_ "github.com/xtls/xray-core/app/dns"
|
_ "github.com/xtls/xray-core/app/dns"
|
||||||
_ "github.com/xtls/xray-core/app/dns/fakedns"
|
_ "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/log"
|
||||||
_ "github.com/xtls/xray-core/app/metrics"
|
_ "github.com/xtls/xray-core/app/metrics"
|
||||||
_ "github.com/xtls/xray-core/app/policy"
|
_ "github.com/xtls/xray-core/app/policy"
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ func init() {
|
|||||||
}
|
}
|
||||||
return cf.Build()
|
return cf.Build()
|
||||||
case io.Reader:
|
case io.Reader:
|
||||||
|
if serial.UseStrictJSON {
|
||||||
|
cfg, err := serial.DecodeJSONConfigStrict(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cfg.Build()
|
||||||
|
}
|
||||||
return serial.LoadJSONConfig(v)
|
return serial.LoadJSONConfig(v)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown type")
|
return nil, errors.New("unknown type")
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
|
"github.com/xtls/xray-core/common/buf"
|
||||||
|
"github.com/xtls/xray-core/common/dice"
|
||||||
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/session"
|
"github.com/xtls/xray-core/common/session"
|
||||||
|
"github.com/xtls/xray-core/common/signal"
|
||||||
"github.com/xtls/xray-core/transport"
|
"github.com/xtls/xray-core/transport"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"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.
|
// Sleep a little here to make sure the response is sent to client.
|
||||||
time.Sleep(time.Second)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+162
-41
@@ -7,6 +7,7 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
geodata "github.com/xtls/xray-core/common/geodata"
|
||||||
net "github.com/xtls/xray-core/common/net"
|
net "github.com/xtls/xray-core/common/net"
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
@@ -22,21 +23,130 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = 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 {
|
type Config struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Server is the DNS server address. If specified, this address overrides the
|
UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||||
// original one.
|
Rule []*DNSRuleConfig `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
|
||||||
Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
Server *net.Endpoint `protobuf:"bytes,3,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"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) Reset() {
|
func (x *Config) Reset() {
|
||||||
*x = Config{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -48,7 +158,7 @@ func (x *Config) String() string {
|
|||||||
func (*Config) ProtoMessage() {}
|
func (*Config) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
mi := &file_proxy_dns_config_proto_msgTypes[1]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -61,14 +171,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||||
func (*Config) Descriptor() ([]byte, []int) {
|
func (*Config) Descriptor() ([]byte, []int) {
|
||||||
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
|
return file_proxy_dns_config_proto_rawDescGZIP(), []int{1}
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Config) GetServer() *net.Endpoint {
|
|
||||||
if x != nil {
|
|
||||||
return x.Server
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetUserLevel() uint32 {
|
func (x *Config) GetUserLevel() uint32 {
|
||||||
@@ -78,16 +181,16 @@ func (x *Config) GetUserLevel() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetNon_IPQuery() string {
|
func (x *Config) GetRule() []*DNSRuleConfig {
|
||||||
if x != nil {
|
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 {
|
if x != nil {
|
||||||
return x.BlockTypes
|
return x.Server
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -96,15 +199,25 @@ var File_proxy_dns_config_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_proxy_dns_config_proto_rawDesc = "" +
|
const file_proxy_dns_config_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" +
|
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" +
|
||||||
"\x06Config\x121\n" +
|
"\rDNSRuleConfig\x122\n" +
|
||||||
"\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\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" +
|
"\n" +
|
||||||
"user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" +
|
"user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" +
|
||||||
"\fnon_IP_query\x18\x03 \x01(\tR\n" +
|
"\x04rule\x18\x02 \x03(\v2\x1d.xray.proxy.dns.DNSRuleConfigR\x04rule\x121\n" +
|
||||||
"nonIPQuery\x12\x1f\n" +
|
"\x06server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\x06server*:\n" +
|
||||||
"\vblock_types\x18\x04 \x03(\x05R\n" +
|
"\n" +
|
||||||
"blockTypesBL\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"
|
"\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -119,18 +232,25 @@ func file_proxy_dns_config_proto_rawDescGZIP() []byte {
|
|||||||
return file_proxy_dns_config_proto_rawDescData
|
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{
|
var file_proxy_dns_config_proto_goTypes = []any{
|
||||||
(*Config)(nil), // 0: xray.proxy.dns.Config
|
(RuleAction)(0), // 0: xray.proxy.dns.RuleAction
|
||||||
(*net.Endpoint)(nil), // 1: xray.common.net.Endpoint
|
(*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{
|
var file_proxy_dns_config_proto_depIdxs = []int32{
|
||||||
1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
|
0, // 0: xray.proxy.dns.DNSRuleConfig.action:type_name -> xray.proxy.dns.RuleAction
|
||||||
1, // [1:1] is the sub-list for method output_type
|
3, // 1: xray.proxy.dns.DNSRuleConfig.domain:type_name -> xray.common.geodata.DomainRule
|
||||||
1, // [1:1] is the sub-list for method input_type
|
1, // 2: xray.proxy.dns.Config.rule:type_name -> xray.proxy.dns.DNSRuleConfig
|
||||||
1, // [1:1] is the sub-list for extension type_name
|
4, // 3: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
|
||||||
1, // [1:1] is the sub-list for extension extendee
|
4, // [4:4] is the sub-list for method output_type
|
||||||
0, // [0:1] is the sub-list for field type_name
|
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() }
|
func init() { file_proxy_dns_config_proto_init() }
|
||||||
@@ -143,13 +263,14 @@ func file_proxy_dns_config_proto_init() {
|
|||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 1,
|
||||||
NumMessages: 1,
|
NumMessages: 2,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
GoTypes: file_proxy_dns_config_proto_goTypes,
|
GoTypes: file_proxy_dns_config_proto_goTypes,
|
||||||
DependencyIndexes: file_proxy_dns_config_proto_depIdxs,
|
DependencyIndexes: file_proxy_dns_config_proto_depIdxs,
|
||||||
|
EnumInfos: file_proxy_dns_config_proto_enumTypes,
|
||||||
MessageInfos: file_proxy_dns_config_proto_msgTypes,
|
MessageInfos: file_proxy_dns_config_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_proxy_dns_config_proto = out.File
|
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;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
import "common/net/destination.proto";
|
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 {
|
message Config {
|
||||||
// Server is the DNS server address. If specified, this address overrides the
|
uint32 user_level = 1;
|
||||||
// original one.
|
repeated DNSRuleConfig rule = 2;
|
||||||
xray.common.net.Endpoint server = 1;
|
xray.common.net.Endpoint server = 3;
|
||||||
uint32 user_level = 2;
|
|
||||||
string non_IP_query = 3;
|
|
||||||
repeated int32 block_types = 4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-50
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
dns_proto "github.com/xtls/xray-core/common/protocol/dns"
|
dns_proto "github.com/xtls/xray-core/common/protocol/dns"
|
||||||
"github.com/xtls/xray-core/common/session"
|
"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 {
|
type ownLinkVerifier interface {
|
||||||
IsOwnLink(ctx context.Context) bool
|
IsOwnLink(ctx context.Context) bool
|
||||||
}
|
}
|
||||||
@@ -50,8 +76,7 @@ type Handler struct {
|
|||||||
ownLinkVerifier ownLinkVerifier
|
ownLinkVerifier ownLinkVerifier
|
||||||
server net.Destination
|
server net.Destination
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
nonIPQuery string
|
rules []*DNSRule
|
||||||
blockTypes []int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error {
|
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 {
|
if config.Server != nil {
|
||||||
h.server = config.Server.AsDestination()
|
h.server = config.Server.AsDestination()
|
||||||
}
|
}
|
||||||
h.nonIPQuery = config.Non_IPQuery
|
|
||||||
if h.nonIPQuery == "" {
|
h.rules = make([]*DNSRule, 0, len(config.Rule))
|
||||||
h.nonIPQuery = "reject"
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,30 +117,38 @@ func (h *Handler) isOwnLink(ctx context.Context) bool {
|
|||||||
return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx)
|
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
|
var parser dnsmessage.Parser
|
||||||
header, err := parser.Start(b)
|
header, err := parser.Start(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogInfoInner(context.Background(), err, "parser start")
|
errors.LogInfoInner(context.Background(), err, "parser start")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id = header.ID
|
id = header.ID
|
||||||
q, err := parser.Question()
|
q, err := parser.Question()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogInfoInner(context.Background(), err, "question")
|
errors.LogInfoInner(context.Background(), err, "question")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
domain = q.Name.String()
|
|
||||||
qType = q.Type
|
qType = q.Type
|
||||||
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
|
domain = q.Name.String()
|
||||||
return
|
ok = true
|
||||||
}
|
|
||||||
|
|
||||||
r = true
|
|
||||||
return
|
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.
|
// Process implements proxy.Outbound.
|
||||||
func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error {
|
func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error {
|
||||||
outbounds := session.OutboundsFromContext(ctx)
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
@@ -183,51 +231,51 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.Update()
|
timer.Update()
|
||||||
|
|
||||||
if !h.isOwnLink(ctx) {
|
if h.isOwnLink(ctx) {
|
||||||
isIPQuery, domain, id, qType := parseIPQuery(b.Bytes())
|
if err := connWriter.WriteMessage(b); err != nil {
|
||||||
if len(h.blockTypes) > 0 {
|
return err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := connWriter.WriteMessage(b); err != nil {
|
id, qType, domain, ok := parseQuery(b.Bytes())
|
||||||
return err
|
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/inbound"
|
||||||
_ "github.com/xtls/xray-core/app/proxyman/outbound"
|
_ "github.com/xtls/xray-core/app/proxyman/outbound"
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
@@ -368,3 +369,126 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
|
|||||||
t.Error(r)
|
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)))
|
dest.Port = net.Port(common.Must2(strconv.Atoi(port)))
|
||||||
}
|
}
|
||||||
if d.portMap != nil && d.portMap[port] != "" {
|
if d.portMap != nil && d.portMap[port] != "" {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package freedom
|
|
||||||
+239
-25
@@ -7,6 +7,8 @@
|
|||||||
package freedom
|
package freedom
|
||||||
|
|
||||||
import (
|
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"
|
protocol "github.com/xtls/xray-core/common/protocol"
|
||||||
internet "github.com/xtls/xray-core/transport/internet"
|
internet "github.com/xtls/xray-core/transport/internet"
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
@@ -23,6 +25,52 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = 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 {
|
type DestinationOverride struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
||||||
@@ -251,6 +299,134 @@ func (x *Noise) GetApplyTo() string {
|
|||||||
return ""
|
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 {
|
type Config struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) Reset() {
|
func (x *Config) Reset() {
|
||||||
*x = Config{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -277,7 +454,7 @@ func (x *Config) String() string {
|
|||||||
func (*Config) ProtoMessage() {}
|
func (*Config) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proxy_freedom_config_proto_msgTypes[3]
|
mi := &file_proxy_freedom_config_proto_msgTypes[5]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -290,7 +467,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||||
func (*Config) Descriptor() ([]byte, []int) {
|
func (*Config) Descriptor() ([]byte, []int) {
|
||||||
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3}
|
return file_proxy_freedom_config_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetDomainStrategy() internet.DomainStrategy {
|
func (x *Config) GetDomainStrategy() internet.DomainStrategy {
|
||||||
@@ -335,11 +512,18 @@ func (x *Config) GetNoises() []*Noise {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Config) GetFinalRules() []*FinalRuleConfig {
|
||||||
|
if x != nil {
|
||||||
|
return x.FinalRules
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_proxy_freedom_config_proto protoreflect.FileDescriptor
|
var File_proxy_freedom_config_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_proxy_freedom_config_proto_rawDesc = "" +
|
const file_proxy_freedom_config_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\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" +
|
"\x13DestinationOverride\x12<\n" +
|
||||||
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
|
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
|
||||||
"\bFragment\x12!\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_min\x18\x03 \x01(\x04R\bdelayMin\x12\x1b\n" +
|
||||||
"\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" +
|
"\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" +
|
||||||
"\x06packet\x18\x05 \x01(\fR\x06packet\x12\x19\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" +
|
"\x06Config\x12P\n" +
|
||||||
"\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\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" +
|
"\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" +
|
"user_level\x18\x04 \x01(\rR\tuserLevel\x128\n" +
|
||||||
"\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" +
|
"\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" +
|
||||||
"\x0eproxy_protocol\x18\x06 \x01(\rR\rproxyProtocol\x121\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"
|
"\x16com.xray.proxy.freedomP\x01Z'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -385,26 +585,39 @@ func file_proxy_freedom_config_proto_rawDescGZIP() []byte {
|
|||||||
return file_proxy_freedom_config_proto_rawDescData
|
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{
|
var file_proxy_freedom_config_proto_goTypes = []any{
|
||||||
(*DestinationOverride)(nil), // 0: xray.proxy.freedom.DestinationOverride
|
(RuleAction)(0), // 0: xray.proxy.freedom.RuleAction
|
||||||
(*Fragment)(nil), // 1: xray.proxy.freedom.Fragment
|
(*DestinationOverride)(nil), // 1: xray.proxy.freedom.DestinationOverride
|
||||||
(*Noise)(nil), // 2: xray.proxy.freedom.Noise
|
(*Fragment)(nil), // 2: xray.proxy.freedom.Fragment
|
||||||
(*Config)(nil), // 3: xray.proxy.freedom.Config
|
(*Noise)(nil), // 3: xray.proxy.freedom.Noise
|
||||||
(*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint
|
(*Range)(nil), // 4: xray.proxy.freedom.Range
|
||||||
(internet.DomainStrategy)(0), // 5: xray.transport.internet.DomainStrategy
|
(*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{
|
var file_proxy_freedom_config_proto_depIdxs = []int32{
|
||||||
4, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
|
7, // 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, // 1: xray.proxy.freedom.FinalRuleConfig.action:type_name -> xray.proxy.freedom.RuleAction
|
||||||
0, // 2: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
|
8, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network
|
||||||
1, // 3: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
|
9, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList
|
||||||
2, // 4: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
|
10, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule
|
||||||
5, // [5:5] is the sub-list for method output_type
|
4, // 5: xray.proxy.freedom.FinalRuleConfig.block_delay:type_name -> xray.proxy.freedom.Range
|
||||||
5, // [5:5] is the sub-list for method input_type
|
11, // 6: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
1, // 7: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
2, // 8: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
|
||||||
0, // [0:5] is the sub-list for field type_name
|
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() }
|
func init() { file_proxy_freedom_config_proto_init() }
|
||||||
@@ -417,13 +630,14 @@ func file_proxy_freedom_config_proto_init() {
|
|||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 1,
|
||||||
NumMessages: 4,
|
NumMessages: 6,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
GoTypes: file_proxy_freedom_config_proto_goTypes,
|
GoTypes: file_proxy_freedom_config_proto_goTypes,
|
||||||
DependencyIndexes: file_proxy_freedom_config_proto_depIdxs,
|
DependencyIndexes: file_proxy_freedom_config_proto_depIdxs,
|
||||||
|
EnumInfos: file_proxy_freedom_config_proto_enumTypes,
|
||||||
MessageInfos: file_proxy_freedom_config_proto_msgTypes,
|
MessageInfos: file_proxy_freedom_config_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_proxy_freedom_config_proto = out.File
|
File_proxy_freedom_config_proto = out.File
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ option java_multiple_files = true;
|
|||||||
|
|
||||||
import "common/protocol/server_spec.proto";
|
import "common/protocol/server_spec.proto";
|
||||||
import "transport/internet/config.proto";
|
import "transport/internet/config.proto";
|
||||||
|
import "common/net/port.proto";
|
||||||
|
import "common/net/network.proto";
|
||||||
|
import "common/geodata/geodat.proto";
|
||||||
|
|
||||||
message DestinationOverride {
|
message DestinationOverride {
|
||||||
xray.common.protocol.ServerEndpoint server = 1;
|
xray.common.protocol.ServerEndpoint server = 1;
|
||||||
@@ -23,6 +26,7 @@ message Fragment {
|
|||||||
uint64 max_split_min = 7;
|
uint64 max_split_min = 7;
|
||||||
uint64 max_split_max = 8;
|
uint64 max_split_max = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Noise {
|
message Noise {
|
||||||
uint64 length_min = 1;
|
uint64 length_min = 1;
|
||||||
uint64 length_max = 2;
|
uint64 length_max = 2;
|
||||||
@@ -32,6 +36,24 @@ message Noise {
|
|||||||
string apply_to = 6;
|
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 {
|
message Config {
|
||||||
xray.transport.internet.DomainStrategy domain_strategy = 1;
|
xray.transport.internet.DomainStrategy domain_strategy = 1;
|
||||||
DestinationOverride destination_override = 3;
|
DestinationOverride destination_override = 3;
|
||||||
@@ -39,4 +61,5 @@ message Config {
|
|||||||
Fragment fragment = 5;
|
Fragment fragment = 5;
|
||||||
uint32 proxy_protocol = 6;
|
uint32 proxy_protocol = 6;
|
||||||
repeated Noise noises = 7;
|
repeated Noise noises = 7;
|
||||||
|
repeated FinalRuleConfig final_rules = 8;
|
||||||
}
|
}
|
||||||
|
|||||||
+341
-62
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/crypto"
|
"github.com/xtls/xray-core/common/crypto"
|
||||||
"github.com/xtls/xray-core/common/dice"
|
"github.com/xtls/xray-core/common/dice"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
"github.com/xtls/xray-core/common/platform"
|
||||||
"github.com/xtls/xray-core/common/retry"
|
"github.com/xtls/xray-core/common/retry"
|
||||||
@@ -29,10 +31,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var useSplice bool
|
var useSplice bool
|
||||||
|
var allNetworks [8]bool
|
||||||
|
var defaultBlockPrivateRule *FinalRule
|
||||||
|
var defaultBlockAllRule *FinalRule
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||||
h := new(Handler)
|
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 {
|
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
|
||||||
return h.Init(config.(*Config), pm)
|
return h.Init(config.(*Config), pm)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -40,24 +53,172 @@ func init() {
|
|||||||
}
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||||
switch value {
|
switch value {
|
||||||
case defaultFlagValue, "auto", "enable":
|
case defaultFlagValue, "auto", "enable":
|
||||||
useSplice = true
|
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.
|
// Handler handles Freedom connections.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
policyManager policy.Manager
|
policyManager policy.Manager
|
||||||
config *Config
|
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.
|
// Init initializes the Handler with necessary parameters.
|
||||||
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
||||||
h.config = config
|
h.config = config
|
||||||
h.policyManager = pm
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +227,41 @@ func (h *Handler) policy() policy.Session {
|
|||||||
return p
|
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 {
|
func isValidAddress(addr *net.IPOrDomain) bool {
|
||||||
if addr == nil {
|
if addr == nil {
|
||||||
return false
|
return false
|
||||||
@@ -85,6 +281,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
ob.Name = "freedom"
|
ob.Name = "freedom"
|
||||||
ob.CanSpliceCopy = 1
|
ob.CanSpliceCopy = 1
|
||||||
inbound := session.InboundFromContext(ctx)
|
inbound := session.InboundFromContext(ctx)
|
||||||
|
defaultRule := getDefaultFinalRule(inbound)
|
||||||
|
|
||||||
destination := ob.Target
|
destination := ob.Target
|
||||||
origTargetAddr := ob.OriginalTarget.Address
|
origTargetAddr := ob.OriginalTarget.Address
|
||||||
@@ -110,26 +307,76 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
output := link.Writer
|
output := link.Writer
|
||||||
|
|
||||||
var conn stat.Connection
|
var conn stat.Connection
|
||||||
|
var blockedDest *net.Destination
|
||||||
|
var blockedRule *FinalRule
|
||||||
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||||
dialDest := destination
|
dialDest := destination
|
||||||
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
|
|
||||||
strategy := h.config.DomainStrategy
|
if dialDest.Address.Family().IsDomain() {
|
||||||
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
|
if strategy := h.config.DomainStrategy; strategy.HasStrategy() {
|
||||||
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
|
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)
|
} else {
|
||||||
if err != nil {
|
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
blockedDest = &dialDest
|
||||||
if h.config.DomainStrategy.ForceIP() {
|
blockedRule = rule
|
||||||
return err
|
return nil
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialDest = net.Destination{
|
|
||||||
Network: dialDest.Network,
|
|
||||||
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
|
|
||||||
Port: dialDest.Port,
|
|
||||||
}
|
|
||||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,23 +385,38 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
return err
|
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
|
conn = rawConn
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to open connection to ", destination).Base(err)
|
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()
|
defer conn.Close()
|
||||||
errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr())
|
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)
|
writer = buf.NewWriter(conn)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writer = NewPacketWriter(conn, h, UDPOverride, destination)
|
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
|
||||||
if h.config.Noises != nil {
|
if h.config.Noises != nil {
|
||||||
errors.LogDebug(ctx, "NOISE", h.config.Noises)
|
errors.LogDebug(ctx, "NOISE", h.config.Noises)
|
||||||
writer = &NoisePacketWriter{
|
writer = &NoisePacketWriter{
|
||||||
@@ -224,7 +486,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
if destination.Network == net.Network_TCP {
|
if destination.Network == net.Network_TCP {
|
||||||
reader = buf.NewReader(conn)
|
reader = buf.NewReader(conn)
|
||||||
} else {
|
} else {
|
||||||
reader = NewPacketReader(conn, UDPOverride, destination)
|
reader = NewPacketReader(conn, h, defaultRule, UDPOverride, destination)
|
||||||
}
|
}
|
||||||
if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {
|
if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {
|
||||||
return errors.New("failed to process response").Base(err)
|
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
|
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
|
iConn := conn
|
||||||
statConn, ok := iConn.(*stat.CounterConnection)
|
statConn, ok := iConn.(*stat.CounterConnection)
|
||||||
if ok {
|
if ok {
|
||||||
@@ -262,6 +524,8 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
|
|||||||
return &PacketReader{
|
return &PacketReader{
|
||||||
PacketConnWrapper: c,
|
PacketConnWrapper: c,
|
||||||
Counter: counter,
|
Counter: counter,
|
||||||
|
Handler: h,
|
||||||
|
DefaultRule: defaultRule,
|
||||||
IsOverridden: isOverridden,
|
IsOverridden: isOverridden,
|
||||||
InitUnchangedAddr: DialDest.Address,
|
InitUnchangedAddr: DialDest.Address,
|
||||||
InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).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 {
|
type PacketReader struct {
|
||||||
*internet.PacketConnWrapper
|
*internet.PacketConnWrapper
|
||||||
stats.Counter
|
stats.Counter
|
||||||
|
Handler *Handler
|
||||||
|
DefaultRule *FinalRule
|
||||||
IsOverridden bool
|
IsOverridden bool
|
||||||
InitUnchangedAddr net.Address
|
InitUnchangedAddr net.Address
|
||||||
InitChangedAddr net.Address
|
InitChangedAddr net.Address
|
||||||
@@ -281,33 +547,40 @@ type PacketReader struct {
|
|||||||
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||||
b := buf.New()
|
b := buf.New()
|
||||||
b.Resize(0, buf.Size)
|
b.Resize(0, buf.Size)
|
||||||
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
|
for {
|
||||||
if err != nil {
|
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
|
||||||
b.Release()
|
if err != nil {
|
||||||
return nil, err
|
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
|
|
||||||
}
|
}
|
||||||
b.UDP = &net.Destination{
|
udpAddr := d.(*net.UDPAddr)
|
||||||
Address: address,
|
sourceAddr := net.IPAddress(udpAddr.IP)
|
||||||
Port: net.Port(d.(*net.UDPAddr).Port),
|
if rule := r.Handler.matchFinalRule(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
Network: net.Network_UDP,
|
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
|
// 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
|
iConn := conn
|
||||||
statConn, ok := iConn.(*stat.CounterConnection)
|
statConn, ok := iConn.(*stat.CounterConnection)
|
||||||
if ok {
|
if ok {
|
||||||
@@ -328,9 +601,10 @@ func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, Dia
|
|||||||
PacketConnWrapper: c,
|
PacketConnWrapper: c,
|
||||||
Counter: counter,
|
Counter: counter,
|
||||||
Handler: h,
|
Handler: h,
|
||||||
|
DefaultRule: defaultRule,
|
||||||
UDPOverride: UDPOverride,
|
UDPOverride: UDPOverride,
|
||||||
ResolvedUDPAddr: resolvedUDPAddr,
|
ResolvedUDPAddr: resolvedUDPAddr,
|
||||||
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
|
OutGateway: outGateway,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -341,6 +615,7 @@ type PacketWriter struct {
|
|||||||
*internet.PacketConnWrapper
|
*internet.PacketConnWrapper
|
||||||
stats.Counter
|
stats.Counter
|
||||||
*Handler
|
*Handler
|
||||||
|
DefaultRule *FinalRule
|
||||||
UDPOverride net.Destination
|
UDPOverride net.Destination
|
||||||
|
|
||||||
// Dest of udp packets might be a domain, we will resolve them to IP
|
// 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
|
// Resulting in these packets being sent to many different IPs randomly
|
||||||
// So, cache and keep the resolve result
|
// So, cache and keep the resolve result
|
||||||
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
|
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
|
||||||
LocalAddr net.Address
|
OutGateway net.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
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 {
|
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
|
||||||
b.UDP.Address = ip
|
b.UDP.Address = ip
|
||||||
} else {
|
} else {
|
||||||
ShouldUseSystemResolver := true
|
shouldUseSystemResolver := true
|
||||||
if w.Handler.config.DomainStrategy.HasStrategy() {
|
if resolveStrategy := w.Handler.udpDomainStrategy(); resolveStrategy.HasStrategy() {
|
||||||
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr)
|
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), resolveStrategy, w.OutGateway)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// drop packet if resolve failed when forceIP
|
// drop packet if resolve failed when forceIP
|
||||||
if w.Handler.config.DomainStrategy.ForceIP() {
|
if resolveStrategy.ForceIP() {
|
||||||
b.Release()
|
b.Release()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ip = net.IPAddress(ips[dice.Roll(len(ips))])
|
ip = net.IPAddress(ips[dice.Roll(len(ips))])
|
||||||
ShouldUseSystemResolver = false
|
shouldUseSystemResolver = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ShouldUseSystemResolver {
|
if shouldUseSystemResolver {
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
|
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Release()
|
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()
|
destAddr := b.UDP.RawNetAddr()
|
||||||
if destAddr == nil {
|
if destAddr == nil {
|
||||||
b.Release()
|
b.Release()
|
||||||
|
|||||||
+45
-48
@@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/task"
|
"github.com/xtls/xray-core/common/task"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
"github.com/xtls/xray-core/features/policy"
|
"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"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
"github.com/xtls/xray-core/transport/internet/hysteria"
|
"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
|
ob.CanSpliceCopy = 3
|
||||||
target := ob.Target
|
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 {
|
if err != nil {
|
||||||
return errors.New("failed to find an available destination").AtWarning().Base(err)
|
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 {
|
if target.Network == net.Network_UDP {
|
||||||
iConn := stat.TryUnwrapStatsConn(conn)
|
iConn := stat.TryUnwrapStatsConn(conn)
|
||||||
_, ok := iConn.(*hysteria.InterUdpConn)
|
_, ok := iConn.(*hysteria.InterConn)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("udp requires hysteria udp transport")
|
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)
|
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||||
|
|
||||||
writer := &UDPWriter{
|
writer := &UDPWriter{
|
||||||
Writer: conn,
|
writer: conn,
|
||||||
buf: make([]byte, MaxUDPSize),
|
|
||||||
addr: target.NetAddr(),
|
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)
|
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||||
|
|
||||||
reader := &UDPReader{
|
reader := &UDPReader{
|
||||||
Reader: conn,
|
reader: conn,
|
||||||
buf: make([]byte, MaxUDPSize),
|
|
||||||
df: &Defragger{},
|
df: &Defragger{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,28 +170,22 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UDPWriter struct {
|
type UDPWriter struct {
|
||||||
Writer io.Writer
|
writer io.Writer
|
||||||
buf []byte
|
|
||||||
addr string
|
addr string
|
||||||
|
buf [buf.Size]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *UDPWriter) sendMsg(msg *UDPMessage) error {
|
func (w *UDPWriter) SendMessage(msg *UDPMessage) error {
|
||||||
msgN := msg.Serialize(w.buf)
|
msgN := msg.Serialize(w.buf[:])
|
||||||
if msgN < 0 {
|
if msgN < 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, err := w.Writer.Write(w.buf[:msgN])
|
_, err := w.writer.Write(w.buf[:msgN])
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
for {
|
for i, b := range mb {
|
||||||
mb2, b := buf.SplitFirst(mb)
|
|
||||||
mb = mb2
|
|
||||||
if b == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := w.addr
|
addr := w.addr
|
||||||
if b.UDP != nil {
|
if b.UDP != nil {
|
||||||
addr = b.UDP.NetAddr()
|
addr = b.UDP.NetAddr()
|
||||||
@@ -209,22 +200,20 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|||||||
Data: b.Bytes(),
|
Data: b.Bytes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := w.sendMsg(msg)
|
err := w.SendMessage(msg)
|
||||||
var errTooLarge *quic.DatagramTooLargeError
|
var errTooLarge *quic.DatagramTooLargeError
|
||||||
if go_errors.As(err, &errTooLarge) {
|
if go_errors.As(err, &errTooLarge) {
|
||||||
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
|
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
|
||||||
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
|
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
|
||||||
for _, fMsg := range fMsgs {
|
for _, fMsg := range fMsgs {
|
||||||
err := w.sendMsg(&fMsg)
|
err := w.SendMessage(&fMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Release()
|
buf.ReleaseMulti(mb[i:])
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
b.Release()
|
buf.ReleaseMulti(mb[i:])
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,30 +224,21 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UDPReader struct {
|
type UDPReader struct {
|
||||||
Reader io.Reader
|
reader io.Reader
|
||||||
buf []byte
|
df *Defragger
|
||||||
df *Defragger
|
firstBuf *buf.Buffer
|
||||||
firstMsg *UDPMessage
|
|
||||||
firstDest *net.Destination
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err 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
|
|
||||||
}
|
|
||||||
for {
|
for {
|
||||||
n, err := r.Reader.Read(r.buf)
|
var buf [hysteria.MaxDatagramFrameSize]byte
|
||||||
|
|
||||||
|
n, err := r.reader.Read(buf[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := ParseUDPMessage(r.buf[:n])
|
msg, err := ParseUDPMessage(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -270,14 +250,31 @@ func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|||||||
|
|
||||||
dest, err := net.ParseDestination("udp:" + dfMsg.Addr)
|
dest, err := net.ParseDestination("udp:" + dfMsg.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := buf.New()
|
if len(p) < len(dfMsg.Data) {
|
||||||
buffer.Write(dfMsg.Data)
|
continue
|
||||||
buffer.UDP = &dest
|
}
|
||||||
|
|
||||||
return buf.MultiBuffer{buffer}, nil
|
return copy(p, dfMsg.Data), &dest, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||||
|
if r.firstBuf != nil {
|
||||||
|
mb := buf.MultiBuffer{r.firstBuf}
|
||||||
|
r.firstBuf = nil
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
b := buf.New()
|
||||||
|
b.Resize(0, buf.Size)
|
||||||
|
n, addr, err := r.ReadFrom(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
b.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.Resize(0, int32(n))
|
||||||
|
b.UDP = addr
|
||||||
|
return buf.MultiBuffer{b}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
package hysteria
|
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/apernet/quic-go/quicvarint"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/hysteria"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -17,8 +18,6 @@ const (
|
|||||||
MaxMessageLength = 2048
|
MaxMessageLength = 2048
|
||||||
MaxPaddingLength = 4096
|
MaxPaddingLength = 4096
|
||||||
|
|
||||||
MaxUDPSize = 4096
|
|
||||||
|
|
||||||
maxVarInt1 = 63
|
maxVarInt1 = 63
|
||||||
maxVarInt2 = 16383
|
maxVarInt2 = 16383
|
||||||
maxVarInt4 = 1073741823
|
maxVarInt4 = 1073741823
|
||||||
@@ -62,7 +61,7 @@ func ReadTCPRequest(r io.Reader) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func WriteTCPRequest(w io.Writer, addr string) error {
|
func WriteTCPRequest(w io.Writer, addr string) error {
|
||||||
padding := tcpRequestPadding.String()
|
padding := hysteria.TcpRequestPadding.String()
|
||||||
paddingLen := len(padding)
|
paddingLen := len(padding)
|
||||||
addrLen := len(addr)
|
addrLen := len(addr)
|
||||||
sz := int(quicvarint.Len(uint64(addrLen))) + addrLen +
|
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 {
|
func WriteTCPResponse(w io.Writer, ok bool, msg string) error {
|
||||||
padding := tcpResponsePadding.String()
|
padding := hysteria.TcpResponsePadding.String()
|
||||||
paddingLen := len(padding)
|
paddingLen := len(padding)
|
||||||
msgLen := len(msg)
|
msgLen := len(msg)
|
||||||
sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +
|
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))
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"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 := session.InboundFromContext(ctx)
|
||||||
inbound.Name = "hysteria"
|
inbound.Name = "hysteria"
|
||||||
inbound.CanSpliceCopy = 3
|
inbound.CanSpliceCopy = 3
|
||||||
|
inbound.User = &protocol.MemoryUser{}
|
||||||
|
|
||||||
iConn := stat.TryUnwrapStatsConn(conn)
|
iConn := stat.TryUnwrapStatsConn(conn)
|
||||||
|
|
||||||
var useremail string
|
|
||||||
var userlevel uint32
|
|
||||||
type User interface{ User() *protocol.MemoryUser }
|
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()
|
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 {
|
if _, ok := iConn.(*hysteria.InterConn); 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
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := &UDPReader{
|
reader := &UDPReader{
|
||||||
Reader: r,
|
reader: conn,
|
||||||
buf: b,
|
df: &Defragger{},
|
||||||
df: df,
|
|
||||||
firstMsg: firstMsg,
|
|
||||||
firstDest: &firstDest,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 := &UDPWriter{
|
||||||
Writer: conn,
|
writer: conn,
|
||||||
buf: make([]byte, MaxUDPSize),
|
addr: addr.NetAddr(),
|
||||||
addr: firstMsg.Addr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatcher.DispatchLink(ctx, firstDest, &transport.Link{
|
return dispatcher.DispatchLink(ctx, *addr, &transport.Link{
|
||||||
Reader: reader,
|
Reader: reader,
|
||||||
Writer: writer,
|
Writer: writer,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
sessionPolicy := s.policyManager.ForLevel(userlevel)
|
sessionPolicy := s.policyManager.ForLevel(inbound.User.Level)
|
||||||
|
|
||||||
common.Must(conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)))
|
common.Must(conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)))
|
||||||
addr, err := ReadTCPRequest(conn)
|
addr, err := ReadTCPRequest(conn)
|
||||||
@@ -178,7 +142,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
|
|||||||
To: dest,
|
To: dest,
|
||||||
Status: log.AccessAccepted,
|
Status: log.AccessAccepted,
|
||||||
Reason: "",
|
Reason: "",
|
||||||
Email: useremail,
|
Email: inbound.User.Email,
|
||||||
})
|
})
|
||||||
errors.LogInfo(ctx, "tunnelling request to ", dest)
|
errors.LogInfo(ctx, "tunnelling request to ", dest)
|
||||||
|
|
||||||
|
|||||||
+14
-76
@@ -4,13 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"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/session"
|
||||||
"github.com/xtls/xray-core/common/task"
|
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
"github.com/xtls/xray-core/features/routing"
|
"github.com/xtls/xray-core/features/routing"
|
||||||
"github.com/xtls/xray-core/transport"
|
"github.com/xtls/xray-core/transport"
|
||||||
@@ -32,81 +27,24 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
|
|||||||
destination := ob.Target
|
destination := ob.Target
|
||||||
|
|
||||||
errors.LogInfo(ctx, "opening connection to ", destination)
|
errors.LogInfo(ctx, "opening connection to ", destination)
|
||||||
|
content := new(session.Content)
|
||||||
|
content.SkipDNSResolve = true
|
||||||
|
|
||||||
input := link.Reader
|
ctx = session.ContextWithContent(ctx, content)
|
||||||
output := link.Writer
|
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 := l.dispatcherInstance.DispatchLink(ctx, destination, link)
|
||||||
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
|
|
||||||
})
|
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -41,10 +41,12 @@ Here is simple Xray config snippet to enable the inbound:
|
|||||||
|
|
||||||
- IPv4 and IPv6
|
- IPv4 and IPv6
|
||||||
- TCP and UDP
|
- TCP and UDP
|
||||||
|
- ICMP Echo (ping)
|
||||||
|
|
||||||
## LIMITATION
|
## 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
|
- 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
|
## 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). \
|
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
|
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
|
## MAC OS X SUPPORT
|
||||||
|
|
||||||
Darwin (Mac OS X) support of the same functionality is implemented through utun (userspace tunnel).
|
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:
|
Build using gomobile for iOS framework integration:
|
||||||
```
|
```
|
||||||
gomobile bind -target=ios
|
gomobile bind -target=ios
|
||||||
```
|
```
|
||||||
|
|||||||
+60
-13
@@ -3,6 +3,8 @@ package tun
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
@@ -45,26 +47,53 @@ func (updater *InterfaceUpdater) Update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var got *net.Interface
|
var got *net.Interface
|
||||||
for _, iface := range interfaces {
|
if updater.fixedName != "" {
|
||||||
if iface.Index == updater.tunIndex {
|
for _, iface := range interfaces {
|
||||||
continue
|
if iface.Index == updater.tunIndex {
|
||||||
}
|
continue
|
||||||
if updater.fixedName != "" {
|
}
|
||||||
if iface.Name == updater.fixedName {
|
if iface.Name == updater.fixedName {
|
||||||
got = &iface
|
got = &iface
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
addrs, err := iface.Addrs()
|
} else {
|
||||||
if err != nil {
|
var ifs []struct {
|
||||||
|
index int
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
for i, iface := range interfaces {
|
||||||
|
if iface.Index == updater.tunIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (iface.Flags&net.FlagUp != 0) &&
|
if strings.Contains(iface.Name, "vEthernet") {
|
||||||
(iface.Flags&net.FlagLoopback == 0) &&
|
continue
|
||||||
len(addrs) > 0 {
|
|
||||||
got = &iface
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
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
|
updater.iface = got
|
||||||
errors.LogInfo(context.Background(), "[tun] update interface ", got.Name, " ", got.Index)
|
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 {
|
type Config struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
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"`
|
Gateway []string `protobuf:"bytes,3,rep,name=gateway,proto3" json:"gateway,omitempty"`
|
||||||
DNS []string `protobuf:"bytes,4,rep,name=DNS,proto3" json:"DNS,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"`
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetMTU() []uint32 {
|
func (x *Config) GetMTU() uint32 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.MTU
|
return x.MTU
|
||||||
}
|
}
|
||||||
return nil
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetGateway() []string {
|
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" +
|
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||||
"\x06Config\x12\x12\n" +
|
"\x06Config\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\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" +
|
"\agateway\x18\x03 \x03(\tR\agateway\x12\x10\n" +
|
||||||
"\x03DNS\x18\x04 \x03(\tR\x03DNS\x12\x1d\n" +
|
"\x03DNS\x18\x04 \x03(\tR\x03DNS\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ option java_multiple_files = true;
|
|||||||
|
|
||||||
message Config {
|
message Config {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
repeated uint32 MTU = 2;
|
uint32 MTU = 2;
|
||||||
repeated string gateway = 3;
|
repeated string gateway = 3;
|
||||||
repeated string DNS = 4;
|
repeated string DNS = 4;
|
||||||
uint32 user_level = 5;
|
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/ipv4"
|
||||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
"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/tcp"
|
||||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||||
"gvisor.dev/gvisor/pkg/waiter"
|
"gvisor.dev/gvisor/pkg/waiter"
|
||||||
@@ -110,14 +111,15 @@ func (t *stackGVisor) Start() error {
|
|||||||
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
|
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
|
||||||
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
|
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
|
||||||
if srcIP == nil || dstIP == nil {
|
if srcIP == nil || dstIP == nil {
|
||||||
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice())
|
panic(id)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
|
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
|
||||||
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
|
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
|
||||||
udpForwarder.HandlePacket(src, dst, data)
|
udpForwarder.HandlePacket(src, dst, data)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, t.handleICMPv4Packet)
|
||||||
|
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, t.handleICMPv6Packet)
|
||||||
|
|
||||||
t.stack = ipStack
|
t.stack = ipStack
|
||||||
t.endpoint = linkEndpoint
|
t.endpoint = linkEndpoint
|
||||||
@@ -206,7 +208,7 @@ func (t *stackGVisor) Close() error {
|
|||||||
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
|
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
|
||||||
opts := stack.Options{
|
opts := stack.Options{
|
||||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
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,
|
HandleLocal: false,
|
||||||
}
|
}
|
||||||
gStack := stack.New(opts)
|
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) {
|
func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||||
return fdbased.New(&fdbased.Options{
|
return fdbased.New(&fdbased.Options{
|
||||||
FDs: []int{t.tunFd},
|
FDs: []int{t.tunFd},
|
||||||
MTU: t.options.MTU[0],
|
MTU: t.options.MTU,
|
||||||
RXChecksumOffload: true,
|
RXChecksumOffload: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-12
@@ -3,7 +3,7 @@
|
|||||||
package tun
|
package tun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
go_errors "errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
|
||||||
"github.com/xtls/xray-core/common/platform"
|
"github.com/xtls/xray-core/common/platform"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
"gvisor.dev/gvisor/pkg/buffer"
|
"gvisor.dev/gvisor/pkg/buffer"
|
||||||
@@ -76,7 +75,7 @@ func NewTun(options *Config) (Tun, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setup(options.Name, options.MTU[0])
|
err = setup(options.Name, options.MTU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tunFile.Close()
|
_ = tunFile.Close()
|
||||||
return nil, err
|
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
|
// WritePacket implements GVisorDevice method to write one packet to the tun device
|
||||||
func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||||
// request memory to write from reusable buffer pool
|
// 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()
|
defer b.Release()
|
||||||
|
|
||||||
// prepare Darwin specific packet header
|
// prepare Darwin specific packet header
|
||||||
@@ -143,7 +142,7 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
|||||||
b.SetByte(3, family)
|
b.SetByte(3, family)
|
||||||
|
|
||||||
if _, err := t.tunFile.Write(b.Bytes()); err != nil {
|
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.ErrWouldBlock{}
|
||||||
}
|
}
|
||||||
return &tcpip.ErrAborted{}
|
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
|
// which will make the stack call Wait which should implement desired push-back
|
||||||
func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||||
// request memory to write from reusable buffer pool
|
// 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
|
// read the bytes to the interface file
|
||||||
n, err := b.ReadFrom(t.tunFile)
|
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()
|
b.Release()
|
||||||
return 0, nil, ErrQueueEmpty
|
return 0, nil, ErrQueueEmpty
|
||||||
}
|
}
|
||||||
@@ -193,7 +192,7 @@ func (t *DarwinTun) Wait() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
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
|
// 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 {
|
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||||
|
var err1, err2 error
|
||||||
|
|
||||||
switch network {
|
switch network {
|
||||||
case "tcp4", "udp4", "ip4":
|
|
||||||
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
|
|
||||||
case "tcp6", "udp6", "ip6":
|
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:
|
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