mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-26 16:58:47 +03:00
Compare commits
1
Commits
REALITY-read
..
utls
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
805abcf5f9 |
@@ -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=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
|
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
|
||||||
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
COPY --from=build --chown=0:0 --chmod=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=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
|
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
|
||||||
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
COPY --from=build --chown=0:0 --chmod=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
|
||||||
|
|||||||
+22
-27
@@ -158,12 +158,9 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
|||||||
clients = append(clients, client)
|
clients = append(clients, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
var domainMatcher geodata.DomainMatcher
|
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||||
if len(effectiveRules) > 0 {
|
if err != nil {
|
||||||
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
return nil, err
|
||||||
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
|
||||||
@@ -274,27 +271,25 @@ func (s *DNS) sortClients(domain string) []*Client {
|
|||||||
|
|
||||||
// Priority domain matching
|
// Priority domain matching
|
||||||
hasMatch := false
|
hasMatch := false
|
||||||
if s.domainMatcher != nil {
|
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||||
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
sort.Slice(matchSlice, func(i, j int) bool {
|
||||||
sort.Slice(matchSlice, func(i, j int) bool {
|
return matchSlice[i] < matchSlice[j]
|
||||||
return matchSlice[i] < matchSlice[j]
|
})
|
||||||
})
|
for _, match := range matchSlice {
|
||||||
for _, match := range matchSlice {
|
info := s.matcherInfos[match]
|
||||||
info := s.matcherInfos[match]
|
client := s.clients[info.clientIdx]
|
||||||
client := s.clients[info.clientIdx]
|
domainRule := info.domainRule
|
||||||
domainRule := info.domainRule
|
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
if clientUsed[info.clientIdx] {
|
||||||
if clientUsed[info.clientIdx] {
|
continue
|
||||||
continue
|
}
|
||||||
}
|
clientUsed[info.clientIdx] = true
|
||||||
clientUsed[info.clientIdx] = true
|
clients = append(clients, client)
|
||||||
clients = append(clients, client)
|
clientNames = append(clientNames, client.Name())
|
||||||
clientNames = append(clientNames, client.Name())
|
hasMatch = true
|
||||||
hasMatch = true
|
if client.finalQuery {
|
||||||
if client.finalQuery {
|
logDecision(s.ctx, domain, domainRules, clientNames)
|
||||||
logDecision(s.ctx, domain, domainRules, clientNames)
|
return clients
|
||||||
return clients
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-14
@@ -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 {
|
||||||
responses [][]net.Address
|
reps [][]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{
|
||||||
responses: reps,
|
reps: 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 _, idx := range h.matcher.Match(domain) {
|
for _, ruleIdx := range h.matcher.Match(domain) {
|
||||||
for _, rep := range h.responses[idx] {
|
for _, rep := range h.reps[ruleIdx] {
|
||||||
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.responses[idx]...)
|
ips = append(ips, h.reps[ruleIdx]...)
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
@@ -122,8 +122,5 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
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))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ 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 {
|
||||||
@@ -357,13 +356,7 @@ 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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,54 +23,10 @@ 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) {
|
||||||
@@ -101,30 +57,25 @@ 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.MatcherSet // TODO: cleanup
|
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
|
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, 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 s := f.shared[key]; s != nil {
|
if m := f.shared[key]; m != nil {
|
||||||
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
|
return m, nil
|
||||||
return s, nil
|
|
||||||
}
|
}
|
||||||
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
|
|
||||||
|
|
||||||
s := strmatcher.NewLinearAnyMatcher()
|
g := strmatcher.NewLinearValueMatcher()
|
||||||
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
|
||||||
@@ -136,19 +87,16 @@ 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
|
||||||
}
|
}
|
||||||
s.Add(m)
|
g.Add(m, 0)
|
||||||
}
|
}
|
||||||
f.shared[key] = s
|
f.shared[key] = g
|
||||||
return s, err
|
return g, 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.MatcherSet, 0, len(rules)),
|
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
|
||||||
values: make([]uint32, 0, len(rules)),
|
values: make([]uint32, 0, len(rules)),
|
||||||
}
|
}
|
||||||
for i, r := range rules {
|
for i, r := range rules {
|
||||||
@@ -178,7 +126,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
|
|||||||
|
|
||||||
type CompactDomainMatcher struct {
|
type CompactDomainMatcher struct {
|
||||||
custom strmatcher.ValueMatcher
|
custom strmatcher.ValueMatcher
|
||||||
matchers []strmatcher.MatcherSet
|
matchers []strmatcher.MatcherGroup
|
||||||
values []uint32
|
values []uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,8 +178,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
|||||||
func newDomainMatcherFactory() DomainMatcherFactory {
|
func newDomainMatcherFactory() DomainMatcherFactory {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "ios", "android":
|
case "ios", "android":
|
||||||
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
||||||
default:
|
default:
|
||||||
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
return &MphDomainMatcherFactory{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
||||||
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
||||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||||
@@ -31,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
|||||||
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
||||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||||
|
|
||||||
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
||||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
||||||
@@ -50,7 +50,7 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
||||||
matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
|
matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,59 +1,11 @@
|
|||||||
package geodata
|
package geodata
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DomainRegistry struct {
|
type DomainRegistry struct {
|
||||||
mu sync.Mutex
|
factory DomainMatcherFactory
|
||||||
factory DomainMatcherFactory
|
|
||||||
matchers []*DynamicDomainMatcher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||||
r.mu.Lock()
|
return r.factory.BuildMatcher(rules)
|
||||||
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 {
|
||||||
@@ -63,32 +15,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -816,10 +816,8 @@ 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 {
|
||||||
@@ -917,31 +915,24 @@ 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 !has4 {
|
if max4 == 0 {
|
||||||
max4 = 0xff
|
max4 = 0xff
|
||||||
} else if max4 == 0 {
|
|
||||||
max4 = 0xfe
|
|
||||||
}
|
}
|
||||||
if !has6 {
|
if max6 == 0 {
|
||||||
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
|
||||||
@@ -1016,7 +1007,3 @@ 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,90 +97,6 @@ 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",
|
||||||
|
|||||||
@@ -1,135 +1,17 @@
|
|||||||
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) {
|
||||||
r.mu.Lock()
|
return buildOptimizedIPMatcher(r.ipsetFactory, rules)
|
||||||
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: newIPSetFactory(),
|
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -100,6 +100,10 @@ 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)
|
||||||
@@ -284,65 +288,3 @@ 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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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
|
||||||
@@ -101,21 +101,3 @@ 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,7 +1,6 @@
|
|||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -27,48 +26,11 @@ func ReadFile(path string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReadAsset(file string) ([]byte, error) {
|
func ReadAsset(file string) ([]byte, error) {
|
||||||
path, _, err := getAssetFileLocation(file)
|
return ReadFile(platform.GetAssetLocation(file))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ReadFile(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenAsset(file string) (io.ReadCloser, error) {
|
func OpenAsset(file string) (io.ReadCloser, error) {
|
||||||
path, _, err := getAssetFileLocation(file)
|
return NewFileReader(platform.GetAssetLocation(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) {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package filesystem_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
. "github.com/xtls/xray-core/common/platform/filesystem"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStatAssetRejectsInvalidPath(t *testing.T) {
|
|
||||||
for _, file := range []string{
|
|
||||||
"",
|
|
||||||
".",
|
|
||||||
"..",
|
|
||||||
"../geoip.dat",
|
|
||||||
"nested/..",
|
|
||||||
"nested/../geoip.dat",
|
|
||||||
"nested//geoip.dat",
|
|
||||||
"/geoip.dat",
|
|
||||||
"/tmp/geoip.dat",
|
|
||||||
`C:\geoip.dat`,
|
|
||||||
`C:geoip.dat`,
|
|
||||||
`\\server\share\geoip.dat`,
|
|
||||||
`nested\geoip.dat`,
|
|
||||||
`nested\..\geoip.dat`,
|
|
||||||
filepath.Join(t.TempDir(), "geoip.dat"),
|
|
||||||
} {
|
|
||||||
if _, err := StatAsset(file); err == nil {
|
|
||||||
t.Fatalf("expected error for %q", file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-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 = 25
|
Version_z byte = 17
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -80,17 +80,13 @@ 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 {
|
||||||
errs = append(errs, err)
|
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := errors.Combine(errs...)
|
return nil
|
||||||
if err != nil {
|
|
||||||
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ require (
|
|||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/pelletier/go-toml v1.9.5
|
||||||
github.com/pires/go-proxyproto v0.12.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.0
|
|
||||||
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
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d
|
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||||
@@ -28,7 +27,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 v1.0.1
|
golang.zx2c4.com/wireguard/windows v0.6.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
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ 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.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
|
||||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 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=
|
||||||
@@ -69,8 +67,6 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
|
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
||||||
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d h1:ca0n8upCDojatNr25id4npJBwUsMmLgtrvLYDj4J0Hg=
|
|
||||||
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
@@ -135,8 +131,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 v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
golang.zx2c4.com/wireguard/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
|
||||||
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=
|
||||||
|
|||||||
+11
-128
@@ -1,70 +1,19 @@
|
|||||||
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"`
|
||||||
Rules []*DNSOutboundRuleConfig `json:"rules"`
|
NonIPQuery string `json:"nonIPQuery"`
|
||||||
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
|
BlockTypes []int32 `json:"blockTypes"`
|
||||||
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||||
@@ -78,78 +27,12 @@ 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 {
|
||||||
// todo: remove legacy
|
|
||||||
if c.NonIPQuery != nil || c.BlockTypes != nil {
|
|
||||||
if c.Rules != nil {
|
|
||||||
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
|
|
||||||
}
|
|
||||||
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes" in DNS outbound`, `"rules"`)
|
|
||||||
rules, err := c.buildLegacyDNSPolicy()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.Rule = rules
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range c.Rules {
|
|
||||||
rule, err := r.Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.Rule = append(config.Rule, rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove legacy
|
|
||||||
func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) {
|
|
||||||
rules := make([]*dns.DNSRuleConfig, 0, 3)
|
|
||||||
|
|
||||||
mode := "reject"
|
|
||||||
if c.NonIPQuery != nil && *c.NonIPQuery != "" {
|
|
||||||
mode = *c.NonIPQuery
|
|
||||||
}
|
|
||||||
switch mode {
|
|
||||||
case "", "reject", "drop", "skip":
|
case "", "reject", "drop", "skip":
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown nonIPQuery: ", mode)
|
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
|
||||||
}
|
}
|
||||||
|
config.Non_IPQuery = c.NonIPQuery
|
||||||
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
|
config.BlockTypes = c.BlockTypes
|
||||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
|
return config, nil
|
||||||
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,10 +1,8 @@
|
|||||||
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"
|
||||||
@@ -31,208 +29,5 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -361,7 +361,6 @@ 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 {
|
||||||
@@ -434,10 +433,6 @@ 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 {
|
||||||
@@ -547,7 +542,6 @@ 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)
|
||||||
@@ -587,14 +581,6 @@ 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 {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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"
|
||||||
|
|||||||
+50
-171
@@ -7,7 +7,6 @@
|
|||||||
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"
|
||||||
@@ -23,130 +22,21 @@ 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"`
|
||||||
UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
// Server is the DNS server address. If specified, this address overrides the
|
||||||
Rule []*DNSRuleConfig `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
|
// original one.
|
||||||
Server *net.Endpoint `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"`
|
Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
||||||
|
UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||||
|
Non_IPQuery string `protobuf:"bytes,3,opt,name=non_IP_query,json=nonIPQuery,proto3" json:"non_IP_query,omitempty"`
|
||||||
|
BlockTypes []int32 `protobuf:"varint,4,rep,packed,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"`
|
||||||
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[1]
|
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -158,7 +48,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[1]
|
mi := &file_proxy_dns_config_proto_msgTypes[0]
|
||||||
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 {
|
||||||
@@ -171,21 +61,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{1}
|
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Config) GetUserLevel() uint32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.UserLevel
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Config) GetRule() []*DNSRuleConfig {
|
|
||||||
if x != nil {
|
|
||||||
return x.Rule
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetServer() *net.Endpoint {
|
func (x *Config) GetServer() *net.Endpoint {
|
||||||
@@ -195,29 +71,40 @@ func (x *Config) GetServer() *net.Endpoint {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Config) GetUserLevel() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserLevel
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Config) GetNon_IPQuery() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Non_IPQuery
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Config) GetBlockTypes() []int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.BlockTypes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_proxy_dns_config_proto protoreflect.FileDescriptor
|
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\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" +
|
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" +
|
||||||
"\rDNSRuleConfig\x122\n" +
|
"\x06Config\x121\n" +
|
||||||
"\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" +
|
"\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\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\x01 \x01(\rR\tuserLevel\x121\n" +
|
"user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" +
|
||||||
"\x04rule\x18\x02 \x03(\v2\x1d.xray.proxy.dns.DNSRuleConfigR\x04rule\x121\n" +
|
"\fnon_IP_query\x18\x03 \x01(\tR\n" +
|
||||||
"\x06server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\x06server*:\n" +
|
"nonIPQuery\x12\x1f\n" +
|
||||||
"\n" +
|
"\vblock_types\x18\x04 \x03(\x05R\n" +
|
||||||
"RuleAction\x12\n" +
|
"blockTypesBL\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 (
|
||||||
@@ -232,25 +119,18 @@ 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_enumTypes = make([]protoimpl.EnumInfo, 1)
|
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 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{
|
||||||
(RuleAction)(0), // 0: xray.proxy.dns.RuleAction
|
(*Config)(nil), // 0: xray.proxy.dns.Config
|
||||||
(*DNSRuleConfig)(nil), // 1: xray.proxy.dns.DNSRuleConfig
|
(*net.Endpoint)(nil), // 1: xray.common.net.Endpoint
|
||||||
(*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{
|
||||||
0, // 0: xray.proxy.dns.DNSRuleConfig.action:type_name -> xray.proxy.dns.RuleAction
|
1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
|
||||||
3, // 1: xray.proxy.dns.DNSRuleConfig.domain:type_name -> xray.common.geodata.DomainRule
|
1, // [1:1] is the sub-list for method output_type
|
||||||
1, // 2: xray.proxy.dns.Config.rule:type_name -> xray.proxy.dns.DNSRuleConfig
|
1, // [1:1] is the sub-list for method input_type
|
||||||
4, // 3: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
4, // [4:4] is the sub-list for method output_type
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
4, // [4:4] is the sub-list for method input_type
|
0, // [0:1] is the sub-list for field type_name
|
||||||
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() }
|
||||||
@@ -263,14 +143,13 @@ 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: 1,
|
NumEnums: 0,
|
||||||
NumMessages: 2,
|
NumMessages: 1,
|
||||||
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
|
||||||
|
|||||||
+6
-17
@@ -7,23 +7,12 @@ 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 {
|
||||||
uint32 user_level = 1;
|
// Server is the DNS server address. If specified, this address overrides the
|
||||||
repeated DNSRuleConfig rule = 2;
|
// original one.
|
||||||
xray.common.net.Endpoint server = 3;
|
xray.common.net.Endpoint server = 1;
|
||||||
|
uint32 user_level = 2;
|
||||||
|
string non_IP_query = 3;
|
||||||
|
repeated int32 block_types = 4;
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-96
@@ -11,7 +11,6 @@ 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"
|
||||||
@@ -41,31 +40,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -76,7 +50,8 @@ type Handler struct {
|
|||||||
ownLinkVerifier ownLinkVerifier
|
ownLinkVerifier ownLinkVerifier
|
||||||
server net.Destination
|
server net.Destination
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rules []*DNSRule
|
nonIPQuery string
|
||||||
|
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 {
|
||||||
@@ -90,26 +65,11 @@ 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
|
||||||
h.rules = make([]*DNSRule, 0, len(config.Rule))
|
if h.nonIPQuery == "" {
|
||||||
for _, r := range config.Rule {
|
h.nonIPQuery = "reject"
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,36 +77,28 @@ 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 parseQuery(b []byte) (id uint16, qType dnsmessage.Type, domain string, ok bool) {
|
func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) {
|
||||||
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
|
||||||
}
|
}
|
||||||
qType = q.Type
|
|
||||||
domain = q.Name.String()
|
domain = q.Name.String()
|
||||||
ok = true
|
qType = q.Type
|
||||||
return
|
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction {
|
r = true
|
||||||
qCode := uint16(qType)
|
return
|
||||||
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.
|
||||||
@@ -231,51 +183,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) {
|
||||||
if err := connWriter.WriteMessage(b); err != nil {
|
isIPQuery, domain, id, qType := parseIPQuery(b.Bytes())
|
||||||
return err
|
if len(h.blockTypes) > 0 {
|
||||||
|
for _, blocktype := range h.blockTypes {
|
||||||
|
if blocktype == int32(qType) {
|
||||||
|
b.Release()
|
||||||
|
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
|
||||||
|
if h.nonIPQuery == "reject" {
|
||||||
|
err := h.rejectNonIPQuery(id, qType, domain, writer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
if isIPQuery {
|
||||||
}
|
b.Release()
|
||||||
|
go h.handleIPQuery(id, qType, domain, writer, timer)
|
||||||
id, qType, domain, ok := parseQuery(b.Bytes())
|
continue
|
||||||
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:
|
if h.nonIPQuery == "drop" {
|
||||||
b.Release()
|
b.Release()
|
||||||
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
|
continue
|
||||||
errors.LogError(ctx, "can only hijack A/AAAA records")
|
}
|
||||||
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
|
if h.nonIPQuery == "reject" {
|
||||||
|
b.Release()
|
||||||
|
err := h.rejectNonIPQuery(id, qType, domain, writer)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
continue
|
||||||
go h.handleIPQuery(id, qType, domain, writer, timer)
|
|
||||||
}
|
}
|
||||||
case RuleAction_Direct:
|
}
|
||||||
if err := connWriter.WriteMessage(b); err != nil {
|
|
||||||
return err
|
if err := connWriter.WriteMessage(b); err != nil {
|
||||||
}
|
return err
|
||||||
default:
|
|
||||||
panic("unknown rule action")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ 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"
|
||||||
@@ -369,126 +368,3 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -348,19 +348,20 @@ 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)
|
|
||||||
for {
|
for {
|
||||||
|
b.Resize(0, buf.Size)
|
||||||
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
|
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Release()
|
b.Release()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
b.Resize(0, int32(n))
|
||||||
|
|
||||||
udpAddr := d.(*net.UDPAddr)
|
udpAddr := d.(*net.UDPAddr)
|
||||||
sourceAddr := net.IPAddress(udpAddr.IP)
|
sourceAddr := net.IPAddress(udpAddr.IP)
|
||||||
if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) {
|
if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.Resize(0, int32(n))
|
|
||||||
|
|
||||||
// if udp dest addr is changed, we are unable to get the correct src addr
|
// 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
|
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
|
||||||
|
|||||||
+78
-14
@@ -4,8 +4,13 @@ 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"
|
||||||
@@ -27,24 +32,83 @@ 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
|
|
||||||
|
|
||||||
ctx = session.ContextWithContent(ctx, content)
|
input := link.Reader
|
||||||
inbound := &session.Inbound{}
|
output := link.Writer
|
||||||
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)
|
|
||||||
|
|
||||||
err := l.dispatcherInstance.DispatchLink(ctx, destination, link)
|
var conn net.Conn
|
||||||
|
err := retry.ExponentialBackoff(2, 100).On(func() error {
|
||||||
|
dialDest := destination
|
||||||
|
|
||||||
|
content := new(session.Content)
|
||||||
|
content.SkipDNSResolve = true
|
||||||
|
|
||||||
|
ctx = session.ContextWithContent(ctx, content)
|
||||||
|
|
||||||
|
inbound := &session.Inbound{}
|
||||||
|
originInbound := session.InboundFromContext(ctx)
|
||||||
|
if originInbound != nil {
|
||||||
|
// get a shallow copy to avoid modifying the inbound tag in upstream context
|
||||||
|
*inbound = *originInbound
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound.Tag = l.config.InboundTag
|
||||||
|
|
||||||
|
ctx = session.ContextWithInbound(ctx, inbound)
|
||||||
|
|
||||||
|
rawConn, err := l.dispatcherInstance.Dispatch(ctx, dialDest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var readerOpt cnc.ConnectionOption
|
||||||
|
if dialDest.Network == net.Network_TCP {
|
||||||
|
readerOpt = cnc.ConnectionOutputMulti(rawConn.Reader)
|
||||||
|
} else {
|
||||||
|
readerOpt = cnc.ConnectionOutputMultiUDP(rawConn.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = cnc.NewConnection(cnc.ConnectionInputMulti(rawConn.Writer), readerOpt)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.New(ctx, "failed to process loopback connection").Base(err)
|
return errors.New("failed to open connection to ", destination).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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, erro
|
|||||||
var conn net.PacketConn
|
var conn net.PacketConn
|
||||||
var err error
|
var err error
|
||||||
if raddr.Addr().Is4() {
|
if raddr.Addr().Is4() {
|
||||||
conn, err = d.lc.ListenPacket(context.Background(), "udp", "0.0.0.0:0")
|
conn, err = d.lc.ListenPacket(context.Background(), "udp4", ":0")
|
||||||
} else {
|
} else {
|
||||||
conn, err = d.lc.ListenPacket(context.Background(), "udp", "[::]:0")
|
conn, err = d.lc.ListenPacket(context.Background(), "udp6", ":0")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -27,8 +26,6 @@ type task struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var conns chan *websocket.Conn
|
var conns chan *websocket.Conn
|
||||||
var server *http.Server
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
var upgrader = &websocket.Upgrader{
|
var upgrader = &websocket.Upgrader{
|
||||||
ReadBufferSize: 0,
|
ReadBufferSize: 0,
|
||||||
@@ -39,48 +36,27 @@ var upgrader = &websocket.Upgrader{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used by external projects when using xray as a go module
|
func init() {
|
||||||
func Reload() {
|
|
||||||
addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
|
addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
if server != nil {
|
|
||||||
server.Close()
|
|
||||||
}
|
|
||||||
if HasBrowserDialer() {
|
|
||||||
for len(conns) > 0 {
|
|
||||||
select {
|
|
||||||
case c := <-conns:
|
|
||||||
c.Close()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conns = nil
|
|
||||||
}
|
|
||||||
if addr != "" {
|
if addr != "" {
|
||||||
token := uuid.New()
|
token := uuid.New()
|
||||||
csrfToken := token.String()
|
csrfToken := token.String()
|
||||||
webpage := bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken))
|
webpage = bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken))
|
||||||
conns = make(chan *websocket.Conn, 256)
|
conns = make(chan *websocket.Conn, 256)
|
||||||
server = &http.Server{
|
go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
Addr: addr,
|
if r.URL.Path == "/websocket" {
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
if r.URL.Query().Get("token") == csrfToken {
|
||||||
if r.URL.Path == "/websocket" {
|
if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
|
||||||
if r.URL.Query().Get("token") == csrfToken {
|
conns <- conn
|
||||||
if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
|
} else {
|
||||||
conns <- conn
|
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error")
|
||||||
} else {
|
|
||||||
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*");
|
|
||||||
w.Write(webpage)
|
|
||||||
}
|
}
|
||||||
}),
|
} else {
|
||||||
}
|
w.Header().Set("Access-Control-Allow-Origin", "*");
|
||||||
go server.ListenAndServe()
|
w.Write(webpage)
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,8 +194,3 @@ func CheckOK(conn *websocket.Conn) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
Reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ type packet struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type xdnsConnClient struct {
|
type xdnsConnClient struct {
|
||||||
net.PacketConn
|
conn net.PacketConn
|
||||||
|
resolverConns []net.PacketConn
|
||||||
resolverAddrs []*net.UDPAddr
|
resolverAddrs []*net.UDPAddr
|
||||||
resolverIdx uint32
|
resolverIdx uint32
|
||||||
resolverSend map[string]*atomic.Uint32
|
resolverSend []atomic.Uint32
|
||||||
|
|
||||||
clientID []byte
|
clientID []byte
|
||||||
domains []Name
|
domains []Name
|
||||||
@@ -74,8 +74,9 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
|||||||
servers = append(servers, parts[1])
|
servers = append(servers, parts[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resolverConns []net.PacketConn
|
||||||
var resolverAddrs []*net.UDPAddr
|
var resolverAddrs []*net.UDPAddr
|
||||||
var resolverSend = make(map[string]*atomic.Uint32)
|
var resolverSend []atomic.Uint32
|
||||||
for _, rs := range servers {
|
for _, rs := range servers {
|
||||||
h, p, err := net.SplitHostPort(rs)
|
h, p, err := net.SplitHostPort(rs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,16 +90,27 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
|||||||
if port == 0 {
|
if port == 0 {
|
||||||
return nil, errors.New("invalid port")
|
return nil, errors.New("invalid port")
|
||||||
}
|
}
|
||||||
addr := &net.UDPAddr{IP: ip, Port: port}
|
var uc net.PacketConn
|
||||||
resolverAddrs = append(resolverAddrs, addr)
|
if ip.To4() != nil {
|
||||||
resolverSend[addr.String()] = &atomic.Uint32{}
|
uc, err = net.ListenPacket("udp4", ":0")
|
||||||
|
} else {
|
||||||
|
uc, err = net.ListenPacket("udp6", ":0")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
for _, rc := range resolverConns {
|
||||||
|
rc.Close()
|
||||||
|
}
|
||||||
|
return nil, errors.New("failed to create resolver socket: ", err)
|
||||||
|
}
|
||||||
|
resolverConns = append(resolverConns, uc)
|
||||||
|
resolverAddrs = append(resolverAddrs, &net.UDPAddr{IP: ip, Port: port})
|
||||||
}
|
}
|
||||||
|
resolverSend = make([]atomic.Uint32, len(resolverConns))
|
||||||
|
|
||||||
conn := &xdnsConnClient{
|
conn := &xdnsConnClient{
|
||||||
PacketConn: raw,
|
conn: raw,
|
||||||
|
resolverConns: resolverConns,
|
||||||
resolverAddrs: resolverAddrs,
|
resolverAddrs: resolverAddrs,
|
||||||
resolverIdx: 0,
|
|
||||||
resolverSend: resolverSend,
|
resolverSend: resolverSend,
|
||||||
|
|
||||||
clientID: make([]byte, 8),
|
clientID: make([]byte, 8),
|
||||||
@@ -118,68 +130,70 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *xdnsConnClient) recvLoop() {
|
func (c *xdnsConnClient) recvLoop() {
|
||||||
var buf [finalmask.UDPSize]byte
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for {
|
for i, rc := range c.resolverConns {
|
||||||
if c.closed {
|
wg.Add(1)
|
||||||
break
|
go func() {
|
||||||
}
|
defer wg.Done()
|
||||||
|
|
||||||
n, addr, err := c.PacketConn.ReadFrom(buf[:])
|
var buf [finalmask.UDPSize]byte
|
||||||
if err != nil {
|
|
||||||
if go_errors.Is(err, net.ErrClosed) {
|
for {
|
||||||
break
|
if c.closed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
n, addr, err := rc.ReadFrom(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
if go_errors.Is(err, net.ErrClosed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := MessageFromWireFormat(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
errors.LogDebug(context.Background(), addr, " xdns from wireformat err ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := dnsResponsePayload(&resp, c.domains)
|
||||||
|
|
||||||
|
r := bytes.NewReader(payload)
|
||||||
|
anyPacket := false
|
||||||
|
for {
|
||||||
|
p, err := nextPacket(r)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
anyPacket = true
|
||||||
|
|
||||||
|
buf := make([]byte, len(p))
|
||||||
|
copy(buf, p)
|
||||||
|
select {
|
||||||
|
case c.readQueue <- &packet{
|
||||||
|
p: buf,
|
||||||
|
addr: addr,
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
errors.LogDebug(context.Background(), addr, " mask read err queue full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if anyPacket {
|
||||||
|
c.resolverSend[i].Store(0)
|
||||||
|
select {
|
||||||
|
case c.pollChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
if addr == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
send := c.resolverSend[addr.String()]
|
|
||||||
if send == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := MessageFromWireFormat(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
errors.LogDebug(context.Background(), addr, " xdns from wireformat err ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := dnsResponsePayload(&resp, c.domains)
|
|
||||||
|
|
||||||
r := bytes.NewReader(payload)
|
|
||||||
anyPacket := false
|
|
||||||
for {
|
|
||||||
p, err := nextPacket(r)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
anyPacket = true
|
|
||||||
|
|
||||||
buf := make([]byte, len(p))
|
|
||||||
copy(buf, p)
|
|
||||||
select {
|
|
||||||
case c.readQueue <- &packet{
|
|
||||||
p: buf,
|
|
||||||
addr: addr,
|
|
||||||
}:
|
|
||||||
default:
|
|
||||||
errors.LogDebug(context.Background(), addr, " mask read err queue full")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if anyPacket {
|
|
||||||
send.Store(0)
|
|
||||||
select {
|
|
||||||
case c.pollChan <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
errors.LogDebug(context.Background(), "xdns closed")
|
errors.LogDebug(context.Background(), "xdns closed")
|
||||||
|
|
||||||
close(c.pollChan)
|
close(c.pollChan)
|
||||||
@@ -240,15 +254,15 @@ func (c *xdnsConnClient) sendLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cur := c.resolverIdx
|
cur := c.resolverIdx
|
||||||
curSend := c.resolverSend[c.resolverAddrs[cur].String()].Add(1)
|
curSend := c.resolverSend[c.resolverIdx].Add(1)
|
||||||
_, _ = c.PacketConn.WriteTo(p.p, c.resolverAddrs[cur])
|
_, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx])
|
||||||
for {
|
for {
|
||||||
c.resolverIdx += 1
|
c.resolverIdx += 1
|
||||||
c.resolverIdx %= uint32(len(c.resolverAddrs))
|
c.resolverIdx %= uint32(len(c.resolverConns))
|
||||||
if c.resolverIdx == cur {
|
if c.resolverIdx == cur {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if c.resolverSend[c.resolverAddrs[c.resolverIdx].String()].Load() < curSend {
|
if c.resolverSend[c.resolverIdx].Load() < curSend {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +290,7 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|||||||
return 0, io.ErrClosedPipe
|
return 0, io.ErrClosedPipe
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded, err := encode(p, c.clientID, c.domains[c.resolverIdx%uint32(len(c.resolverAddrs))])
|
encoded, err := encode(p, c.clientID, c.domains[c.resolverIdx%uint32(len(c.resolverConns))])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p))
|
errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p))
|
||||||
return 0, nil
|
return 0, nil
|
||||||
@@ -296,7 +310,35 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|||||||
|
|
||||||
func (c *xdnsConnClient) Close() error {
|
func (c *xdnsConnClient) Close() error {
|
||||||
c.closed = true
|
c.closed = true
|
||||||
return c.PacketConn.Close()
|
for _, rc := range c.resolverConns {
|
||||||
|
rc.Close()
|
||||||
|
}
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *xdnsConnClient) LocalAddr() net.Addr {
|
||||||
|
return c.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *xdnsConnClient) SetDeadline(t time.Time) error {
|
||||||
|
for _, rc := range c.resolverConns {
|
||||||
|
rc.SetDeadline(t)
|
||||||
|
}
|
||||||
|
return c.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *xdnsConnClient) SetReadDeadline(t time.Time) error {
|
||||||
|
for _, rc := range c.resolverConns {
|
||||||
|
rc.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
return c.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *xdnsConnClient) SetWriteDeadline(t time.Time) error {
|
||||||
|
for _, rc := range c.resolverConns {
|
||||||
|
rc.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
return c.conn.SetWriteDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(p []byte, clientID []byte, domain Name) ([]byte, error) {
|
func encode(p []byte, clientID []byte, domain Name) ([]byte, error) {
|
||||||
|
|||||||
@@ -2,23 +2,27 @@ package xdns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/hysteria/udphop"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Config) UDP() {
|
func (c *Config) UDP() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||||
// _, ok1 := raw.(*internet.FakePacketConn)
|
_, ok1 := raw.(*internet.FakePacketConn)
|
||||||
// _, ok2 := raw.(*udphop.UdpHopPacketConn)
|
_, ok2 := raw.(*udphop.UdpHopPacketConn)
|
||||||
// if level != 0 || ok1 || ok2 {
|
if level != 0 || ok1 || ok2 {
|
||||||
// return nil, errors.New("xdns requires being at the outermost level")
|
return nil, errors.New("xdns requires being at the outermost level")
|
||||||
// }
|
}
|
||||||
return NewConnClient(c, raw)
|
return NewConnClient(c, raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||||
// if level != 0 {
|
if level != 0 {
|
||||||
// return nil, errors.New("xdns requires being at the outermost level")
|
return nil, errors.New("xdns requires being at the outermost level")
|
||||||
// }
|
}
|
||||||
return NewConnServer(c, raw)
|
return NewConnServer(c, raw)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,12 +373,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
|||||||
if requestURL.Host == "" {
|
if requestURL.Host == "" {
|
||||||
requestURL.Host = dest.Address.String()
|
requestURL.Host = dest.Address.String()
|
||||||
}
|
}
|
||||||
if browser_dialer.HasBrowserDialer() && realityConfig == nil {
|
|
||||||
// For Browser Dialer's optimized IP and non-standard port
|
|
||||||
if !(requestURL.Scheme == "http" && dest.Port == 80) && !(requestURL.Scheme == "https" && dest.Port == 443) {
|
|
||||||
requestURL.Host += ":" + dest.Port.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestURL.Path = transportConfiguration.GetNormalizedPath()
|
requestURL.Path = transportConfiguration.GetNormalizedPath()
|
||||||
requestURL.RawQuery = transportConfiguration.GetNormalizedQuery()
|
requestURL.RawQuery = transportConfiguration.GetNormalizedQuery()
|
||||||
@@ -440,12 +434,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
|||||||
if requestURL2.Host == "" {
|
if requestURL2.Host == "" {
|
||||||
requestURL2.Host = dest2.Address.String()
|
requestURL2.Host = dest2.Address.String()
|
||||||
}
|
}
|
||||||
if browser_dialer.HasBrowserDialer() && realityConfig2 == nil {
|
|
||||||
// For Browser Dialer's optimized IP and non-standard port
|
|
||||||
if !(requestURL2.Scheme == "http" && dest2.Port == 80) && !(requestURL2.Scheme == "https" && dest2.Port == 443) {
|
|
||||||
requestURL2.Host += ":" + dest2.Port.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestURL2.Path = config2.GetNormalizedPath()
|
requestURL2.Path = config2.GetNormalizedPath()
|
||||||
requestURL2.RawQuery = config2.GetNormalizedQuery()
|
requestURL2.RawQuery = config2.GetNormalizedQuery()
|
||||||
httpClient2, xmuxClient2 = getHTTPClient(ctx, dest2, memory2)
|
httpClient2, xmuxClient2 = getHTTPClient(ctx, dest2, memory2)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
utls "github.com/refraction-networking/utls"
|
utls "github.com/refraction-networking/utls"
|
||||||
@@ -90,18 +91,24 @@ func (c *UConn) HandshakeContextServerName(ctx context.Context) string {
|
|||||||
return c.ConnectionState().ServerName
|
return c.ConnectionState().ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send
|
// WebsocketHandshakeContext basically calls UConn.Handshake inside it but it will try
|
||||||
// http/1.1 in its ALPN.
|
// to build outer ALPN to `http/1.1` or `h2 http/1.1` (if manually specified for camouflage)
|
||||||
func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error {
|
func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error {
|
||||||
|
config := *utils.AccessField[*utls.Config](c, "config")
|
||||||
|
ALPN := slices.Clone(config.NextProtos)
|
||||||
|
// set other kinds of ALPN to http/1.1
|
||||||
|
if !slices.Equal(ALPN, []string{"h2", "http/1.1"}) {
|
||||||
|
ALPN = []string{"http/1.1"}
|
||||||
|
}
|
||||||
// Build the handshake state. This will apply every variable of the TLS of the
|
// Build the handshake state. This will apply every variable of the TLS of the
|
||||||
// fingerprint in the UConn
|
// fingerprint in the UConn
|
||||||
if err := c.BuildHandshakeState(); err != nil {
|
if err := c.BuildHandshakeState(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
config := *utils.AccessField[*utls.Config](c, "config")
|
// Do not modify outer ALPN if ECH is used
|
||||||
// Do not modify outer ALPN to http/1.1 if ECH is used
|
// Outer ALPN will be h2,http/1.1, and real http/1.1 in config will be hidden in ECH
|
||||||
// Outer ALPN will be h2,http/1.1, and real ALPN in config will be hidden in ECH
|
|
||||||
if config.EncryptedClientHelloConfigList != nil {
|
if config.EncryptedClientHelloConfigList != nil {
|
||||||
|
config.NextProtos = []string{"http/1.1"}
|
||||||
return c.HandshakeContext(ctx)
|
return c.HandshakeContext(ctx)
|
||||||
}
|
}
|
||||||
// Iterate over extensions and check for utls.ALPNExtension
|
// Iterate over extensions and check for utls.ALPNExtension
|
||||||
@@ -109,12 +116,12 @@ func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error {
|
|||||||
for _, extension := range c.Extensions {
|
for _, extension := range c.Extensions {
|
||||||
if alpn, ok := extension.(*utls.ALPNExtension); ok {
|
if alpn, ok := extension.(*utls.ALPNExtension); ok {
|
||||||
hasALPNExtension = true
|
hasALPNExtension = true
|
||||||
alpn.AlpnProtocols = []string{"http/1.1"}
|
alpn.AlpnProtocols = ALPN
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasALPNExtension { // Append extension if doesn't exists
|
if !hasALPNExtension { // Append extension if doesn't exists
|
||||||
c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}})
|
c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: ALPN})
|
||||||
}
|
}
|
||||||
// Rebuild the client hello and do the handshake
|
// Rebuild the client hello and do the handshake
|
||||||
if err := c.BuildHandshakeState(); err != nil {
|
if err := c.BuildHandshakeState(); err != nil {
|
||||||
@@ -146,9 +153,7 @@ func copyConfig(c *tls.Config) *utls.Config {
|
|||||||
VerifyPeerCertificate: c.VerifyPeerCertificate,
|
VerifyPeerCertificate: c.VerifyPeerCertificate,
|
||||||
KeyLogWriter: c.KeyLogWriter,
|
KeyLogWriter: c.KeyLogWriter,
|
||||||
EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList,
|
EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList,
|
||||||
}
|
NextProtos: c.NextProtos,
|
||||||
if config.EncryptedClientHelloConfigList != nil {
|
|
||||||
config.NextProtos = c.NextProtos
|
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,20 +111,13 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if browser_dialer.HasBrowserDialer() {
|
host := dest.NetAddr()
|
||||||
// For Browser Dialer's optimized IP and non-standard port
|
if (protocol == "ws" && dest.Port == 80) || (protocol == "wss" && dest.Port == 443) {
|
||||||
host := wsSettings.Host
|
host = dest.Address.String()
|
||||||
if host == "" && tConfig.ServerName != "" {
|
}
|
||||||
host = tConfig.ServerName
|
uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
host = dest.Address.String()
|
|
||||||
}
|
|
||||||
if !(protocol == "ws" && dest.Port == 80) && !(protocol == "wss" && dest.Port == 443) {
|
|
||||||
host += ":" + dest.Port.String()
|
|
||||||
}
|
|
||||||
uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
|
|
||||||
|
|
||||||
|
if browser_dialer.HasBrowserDialer() {
|
||||||
conn, err := browser_dialer.DialWS(uri, ed)
|
conn, err := browser_dialer.DialWS(uri, ed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -133,12 +126,6 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
|
|||||||
return NewConnection(conn, conn.RemoteAddr(), nil, wsSettings.HeartbeatPeriod), nil
|
return NewConnection(conn, conn.RemoteAddr(), nil, wsSettings.HeartbeatPeriod), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
host := dest.Address.String()
|
|
||||||
if !(protocol == "ws" && dest.Port == 80) && !(protocol == "wss" && dest.Port == 443) {
|
|
||||||
host += ":" + dest.Port.String()
|
|
||||||
}
|
|
||||||
uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
|
|
||||||
|
|
||||||
header := wsSettings.GetRequestHeader()
|
header := wsSettings.GetRequestHeader()
|
||||||
// See dialer.DialContext()
|
// See dialer.DialContext()
|
||||||
header.Set("Host", wsSettings.Host)
|
header.Set("Host", wsSettings.Host)
|
||||||
|
|||||||
Reference in New Issue
Block a user