Compare commits

..

1 Commits

Author SHA1 Message Date
Fangliding 805abcf5f9 Allow "h2" ws 2026-04-28 18:47:28 +08:00
51 changed files with 435 additions and 2839 deletions
+2 -2
View File
@@ -45,8 +45,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/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=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+2 -2
View File
@@ -54,8 +54,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/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=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+22 -27
View File
@@ -158,12 +158,9 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, client)
}
var domainMatcher geodata.DomainMatcher
if len(effectiveRules) > 0 {
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
// If there is no DNS client in config, add a `localhost` DNS client
@@ -274,27 +271,25 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching
hasMatch := false
if s.domainMatcher != nil {
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
+11 -14
View File
@@ -13,8 +13,8 @@ import (
// StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct {
responses [][]net.Address
matcher geodata.DomainMatcher
reps [][]net.Address
matcher geodata.DomainMatcher
}
// NewStaticHosts creates a new StaticHosts instance.
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
rep = append(rep, addr)
}
}
// if len(rep) == 0 {
// errors.LogError(context.Background(), "empty value in static hosts, ignore this rule: ", mapping.Domain)
// continue
// }
reps = append(reps, rep)
rules = append(rules, mapping.Domain)
}
if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
responses: reps,
matcher: matcher,
reps: reps,
matcher: matcher,
}, nil
}
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0)
found := false
for _, idx := range h.matcher.Match(domain) {
for _, rep := range h.responses[idx] {
for _, ruleIdx := range h.matcher.Match(domain) {
for _, rep := range h.reps[ruleIdx] {
if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
return nil, err
}
}
ips = append(ips, h.responses[idx]...)
ips = append(ips, h.reps[ruleIdx]...)
found = true
}
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.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5)
}
-198
View File
@@ -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
}
-21
View File
@@ -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;
}
-304
View File
@@ -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
}
-134
View File
@@ -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))
}))
}
+1 -8
View File
@@ -12,7 +12,6 @@ import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
)
type Condition interface {
@@ -357,13 +356,7 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
var dstIP string
var dstPort uint16 = 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 {
if len(ctx.GetTargetIPs()) > 0 {
dstIP = ctx.GetTargetIPs()[0].String()
dstPort = uint16(ctx.GetTargetPort())
}
+13 -65
View File
@@ -23,54 +23,10 @@ type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}
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()
}
type MphDomainMatcherFactory struct{}
// BuildMatcher implements DomainMatcherFactory.
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
key := buildDomainRulesKey(rules)
if key != "" {
f.Lock()
defer f.Unlock()
if g := f.shared[key]; g != nil {
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
return g, nil
}
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
}
g := strmatcher.NewMphValueMatcher()
for i, r := range rules {
switch v := r.Value.(type) {
@@ -101,30 +57,25 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
if err := g.Build(); err != nil {
return nil, err
}
if key != "" {
f.shared[key] = g
}
return g, nil
}
type CompactDomainMatcherFactory struct {
sync.Mutex
shared map[string]strmatcher.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
f.Lock()
defer f.Unlock()
if s := f.shared[key]; s != nil {
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil
if m := f.shared[key]; m != nil {
return m, 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)
if err != nil {
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)
continue
}
s.Add(m)
g.Add(m, 0)
}
f.shared[key] = s
return s, err
f.shared[key] = g
return g, err
}
// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
@@ -178,7 +126,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherSet
matchers []strmatcher.MatcherGroup
values []uint32
}
@@ -230,8 +178,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
default:
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
return &MphDomainMatcherFactory{}
}
}
+3 -3
View File
@@ -10,7 +10,7 @@ import (
)
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{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
@@ -31,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
@@ -50,7 +50,7 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{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_Domain, Value: "example.com"}}},
})
+2 -79
View File
@@ -1,59 +1,11 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
)
type DomainRegistry struct {
mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
factory DomainMatcherFactory
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := r.factory.BuildMatcher(rules)
if err != nil {
return nil, err
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *DomainRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
factory := newDomainMatcherFactory()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
return nil
return r.factory.BuildMatcher(rules)
}
func newDomainRegistry() *DomainRegistry {
@@ -63,32 +15,3 @@ func newDomainRegistry() *DomainRegistry {
}
var DomainReg = newDomainRegistry()
type domainMatcherState struct {
matcher DomainMatcher
}
type DynamicDomainMatcher struct {
rules []*DomainRule
state atomic.Pointer[domainMatcherState]
}
// Match implements DomainMatcher.
func (d *DynamicDomainMatcher) Match(input string) []uint32 {
return d.state.Load().matcher.Match(input)
}
// MatchAny implements DomainMatcher.
func (d *DynamicDomainMatcher) MatchAny(input string) bool {
return d.state.Load().matcher.MatchAny(input)
}
func (d *DynamicDomainMatcher) Reload(newMatcher DomainMatcher) {
d.state.Store(&domainMatcherState{matcher: newMatcher})
}
func NewDynamicDomainMatcher(rules []*DomainRule, matcher DomainMatcher) *DynamicDomainMatcher {
d := &DynamicDomainMatcher{rules: rules}
d.Reload(matcher)
return d
}
+2 -15
View File
@@ -816,10 +816,8 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
defer f.Unlock()
if ipset := f.shared[key]; ipset != nil {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil
}
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules {
@@ -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)
}
var has4, has6 bool
var max4, max6 int
for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 {
max4 = b
}
}
for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 {
max6 = b
}
}
if !has4 {
if max4 == 0 {
max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
}
if !has6 {
if max6 == 0 {
max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
}
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
}
}
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: make(map[string]*IPSet)}
}
-84
View File
@@ -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) {
matcher := buildIPMatcher(
"98.108.20.0/22",
+2 -120
View File
@@ -1,135 +1,17 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
return nil
return buildOptimizedIPMatcher(r.ipsetFactory, rules)
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
ipsetFactory: newIPSetFactory(),
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
}
}
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)
}
+4 -62
View File
@@ -100,6 +100,10 @@ func (t Type) New(pattern string) (Matcher, error) {
case Substr:
return SubstrMatcher(pattern), nil
case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern)
@@ -284,65 +288,3 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
return result
}
}
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
type MatcherSetForAll interface {
AddMatcher(matcher Matcher)
}
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
type MatcherSetForFull interface {
AddFullMatcher(matcher FullMatcher)
}
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
type MatcherSetForDomain interface {
AddDomainMatcher(matcher DomainMatcher)
}
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
type MatcherSetForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher)
}
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
type MatcherSetForRegex interface {
AddRegexMatcher(matcher *RegexMatcher)
}
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
// It returns error if the MatcherSet does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherSet.
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
if s, ok := s.(IndexMatcher); ok {
s.Add(matcher)
return nil
}
if s, ok := s.(MatcherSetForAll); ok {
s.AddMatcher(matcher)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if s, ok := s.(MatcherSetForFull); ok {
s.AddFullMatcher(matcher)
return nil
}
case DomainMatcher:
if s, ok := s.(MatcherSetForDomain); ok {
s.AddDomainMatcher(matcher)
return nil
}
case SubstrMatcher:
if s, ok := s.(MatcherSetForSubstr); ok {
s.AddSubstrMatcher(matcher)
return nil
}
case *RegexMatcher:
if s, ok := s.(MatcherSetForRegex); ok {
s.AddRegexMatcher(matcher)
return nil
}
}
return errors.New("cannot add matcher to matcher set")
}
@@ -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)
}
}
}
+1 -19
View File
@@ -15,7 +15,7 @@ const (
)
// Matcher is the interface to determine a string matches a pattern.
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
type Matcher interface {
// Type returns the matcher's type.
Type() Type
@@ -101,21 +101,3 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherSet interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
type AnyMatcher interface {
// Add adds a new Matcher to AnyMatcher.
Add(matcher Matcher)
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
+2 -40
View File
@@ -1,7 +1,6 @@
package filesystem
import (
"errors"
"io"
"os"
"path/filepath"
@@ -27,48 +26,11 @@ func ReadFile(path string) ([]byte, error) {
}
func ReadAsset(file string) ([]byte, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
return ReadFile(platform.GetAssetLocation(file))
}
func OpenAsset(file string) (io.ReadCloser, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file)
return path, err
}
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
if !filepath.IsLocal(file) || file == "." {
return "", nil, errors.New("asset path must stay in asset directory")
}
local, err := filepath.Localize(file)
if err != nil {
return "", nil, err
}
path := platform.GetAssetLocation(local)
info, err := os.Stat(path)
if err != nil {
return "", nil, err
}
if !info.Mode().IsRegular() {
return "", nil, errors.New("asset is not a regular file")
}
return path, info, nil
return NewFileReader(platform.GetAssetLocation(file))
}
func ReadCert(file string) ([]byte, error) {
-32
View File
@@ -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
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 4
Version_z byte = 25
Version_z byte = 17
)
var (
+3 -7
View File
@@ -80,17 +80,13 @@ func New() *Client {
d := &net.Dialer{
Timeout: time.Second * 16,
Control: func(network, address string, c syscall.RawConn) error {
var errs []error
for _, ctl := range internet.Controllers {
if err := ctl(network, address, c); err != nil {
errs = append(errs, err)
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
return err
}
}
err := errors.Combine(errs...)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
}
return err
return nil
},
}
+2 -3
View File
@@ -14,12 +14,11 @@ require (
github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.0
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.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
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
@@ -28,7 +27,7 @@ require (
golang.org/x/sys v0.43.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1
golang.zx2c4.com/wireguard/windows v0.6.1
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
+2 -6
View File
@@ -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/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.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/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
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/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-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=
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=
@@ -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/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
golang.zx2c4.com/wireguard/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
+11 -128
View File
@@ -1,70 +1,19 @@
package conf
import (
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/proxy/dns"
"google.golang.org/protobuf/proto"
)
type DNSOutboundRuleConfig struct {
Action string `json:"action"`
QType *PortList `json:"qtype"`
Domain *StringList `json:"domain"`
}
func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
rule := &dns.DNSRuleConfig{}
switch strings.ToLower(c.Action) {
case "direct":
rule.Action = dns.RuleAction_Direct
case "drop":
rule.Action = dns.RuleAction_Drop
case "reject":
rule.Action = dns.RuleAction_Reject
case "hijack":
rule.Action = dns.RuleAction_Hijack
default:
return nil, errors.New("unknown action: ", c.Action)
}
if c.QType != nil {
for _, r := range c.QType.Range {
if r.From > r.To {
return nil, errors.New("invalid qtype range: ", r.String())
}
if r.To > 65535 {
return nil, errors.New("dns rule qtype out of range: ", r.String())
}
for qtype := r.From; qtype <= r.To; qtype++ {
rule.Qtype = append(rule.Qtype, int32(qtype))
}
}
}
if c.Domain != nil {
rules, err := geodata.ParseDomainRules(*c.Domain, geodata.Domain_Substr)
if err != nil {
return nil, err
}
rule.Domain = rules
}
return rule, nil
}
type DNSOutboundConfig struct {
Network Network `json:"network"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
UserLevel uint32 `json:"userLevel"`
Rules []*DNSOutboundRuleConfig `json:"rules"`
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
Network Network `json:"network"`
Address *Address `json:"address"`
Port uint16 `json:"port"`
UserLevel uint32 `json:"userLevel"`
NonIPQuery string `json:"nonIPQuery"`
BlockTypes []int32 `json:"blockTypes"`
}
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
@@ -78,78 +27,12 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) {
if c.Address != nil {
config.Server.Address = c.Address.Build()
}
// 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 {
switch c.NonIPQuery {
case "", "reject", "drop", "skip":
default:
return nil, errors.New("unknown nonIPQuery: ", mode)
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
}
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
if mode == "reject" {
rule.Action = dns.RuleAction_Reject
}
for _, qtype := range *c.BlockTypes {
if qtype < 0 || qtype > 65535 {
return nil, errors.New("legacy blockTypes qtype out of range: ", qtype)
}
rule.Qtype = append(rule.Qtype, qtype)
}
rules = append(rules, rule)
}
{
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack}
rule.Qtype = append(rule.Qtype, 1)
rule.Qtype = append(rule.Qtype, 28)
rules = append(rules, rule)
}
{
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject}
if mode == "reject" {
rule.Action = dns.RuleAction_Reject
} else if mode == "drop" {
rule.Action = dns.RuleAction_Drop
} else if mode == "skip" {
rule.Action = dns.RuleAction_Direct
}
rules = append(rules, rule)
}
return rules, nil
config.Non_IPQuery = c.NonIPQuery
config.BlockTypes = c.BlockTypes
return config, nil
}
-205
View File
@@ -1,10 +1,8 @@
package conf_test
import (
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/dns"
@@ -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)
}
}
-71
View File
@@ -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
}
-75
View File
@@ -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)
}
}
}
-14
View File
@@ -361,7 +361,6 @@ type Config struct {
Observatory *ObservatoryConfig `json:"observatory"`
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
Version *VersionConfig `json:"version"`
Geodata *GeodataConfig `json:"geodata"`
}
func (c *Config) findInboundTag(tag string) int {
@@ -434,10 +433,6 @@ func (c *Config) Override(o *Config, fn string) {
c.Version = o.Version
}
if o.Geodata != nil {
c.Geodata = o.Geodata
}
// update the Inbound in slice if the only one in override config has same tag
if len(o.InboundConfigs) > 0 {
for i := range o.InboundConfigs {
@@ -547,7 +542,6 @@ func (c *Config) Build() (*core.Config, error) {
}
if c.Reverse != nil {
return nil, errors.PrintRemovedFeatureError(`"legacy reverse"`, `"VLESS Reverse Proxy"`)
r, err := c.Reverse.Build()
if err != nil {
return nil, errors.New("failed to build reverse configuration").Base(err)
@@ -587,14 +581,6 @@ func (c *Config) Build() (*core.Config, error) {
config.App = append(config.App, serial.ToTypedMessage(r))
}
if c.Geodata != nil {
r, err := c.Geodata.Build()
if err != nil {
return nil, errors.New("failed to build geodata configuration").Base(err)
}
config.App = append(config.App, serial.ToTypedMessage(r))
}
var inbounds []InboundDetourConfig
if len(c.InboundConfigs) > 0 {
-1
View File
@@ -20,7 +20,6 @@ import (
// Other optional features.
_ "github.com/xtls/xray-core/app/dns"
_ "github.com/xtls/xray-core/app/dns/fakedns"
_ "github.com/xtls/xray-core/app/geodata"
_ "github.com/xtls/xray-core/app/log"
_ "github.com/xtls/xray-core/app/metrics"
_ "github.com/xtls/xray-core/app/policy"
+50 -171
View File
@@ -7,7 +7,6 @@
package dns
import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -23,130 +22,21 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type RuleAction int32
const (
RuleAction_Direct RuleAction = 0
RuleAction_Drop RuleAction = 1
RuleAction_Reject RuleAction = 2
RuleAction_Hijack RuleAction = 3
)
// Enum value maps for RuleAction.
var (
RuleAction_name = map[int32]string{
0: "Direct",
1: "Drop",
2: "Reject",
3: "Hijack",
}
RuleAction_value = map[string]int32{
"Direct": 0,
"Drop": 1,
"Reject": 2,
"Hijack": 3,
}
)
func (x RuleAction) Enum() *RuleAction {
p := new(RuleAction)
*p = x
return p
}
func (x RuleAction) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (RuleAction) Descriptor() protoreflect.EnumDescriptor {
return file_proxy_dns_config_proto_enumTypes[0].Descriptor()
}
func (RuleAction) Type() protoreflect.EnumType {
return &file_proxy_dns_config_proto_enumTypes[0]
}
func (x RuleAction) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use RuleAction.Descriptor instead.
func (RuleAction) EnumDescriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
type DNSRuleConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.dns.RuleAction" json:"action,omitempty"`
Qtype []int32 `protobuf:"varint,2,rep,packed,name=qtype,proto3" json:"qtype,omitempty"`
Domain []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DNSRuleConfig) Reset() {
*x = DNSRuleConfig{}
mi := &file_proxy_dns_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DNSRuleConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSRuleConfig) ProtoMessage() {}
func (x *DNSRuleConfig) ProtoReflect() protoreflect.Message {
mi := &file_proxy_dns_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSRuleConfig.ProtoReflect.Descriptor instead.
func (*DNSRuleConfig) Descriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
func (x *DNSRuleConfig) GetAction() RuleAction {
if x != nil {
return x.Action
}
return RuleAction_Direct
}
func (x *DNSRuleConfig) GetQtype() []int32 {
if x != nil {
return x.Qtype
}
return nil
}
func (x *DNSRuleConfig) GetDomain() []*geodata.DomainRule {
if x != nil {
return x.Domain
}
return nil
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
Rule []*DNSRuleConfig `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
Server *net.Endpoint `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
// Server is the DNS server address. If specified, this address overrides the
// original one.
Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
Non_IPQuery string `protobuf:"bytes,3,opt,name=non_IP_query,json=nonIPQuery,proto3" json:"non_IP_query,omitempty"`
BlockTypes []int32 `protobuf:"varint,4,rep,packed,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*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.StoreMessageInfo(mi)
}
@@ -158,7 +48,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -171,21 +61,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dns_config_proto_rawDescGZIP(), []int{1}
}
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
return file_proxy_dns_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetServer() *net.Endpoint {
@@ -195,29 +71,40 @@ func (x *Config) GetServer() *net.Endpoint {
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
const file_proxy_dns_config_proto_rawDesc = "" +
"\n" +
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" +
"\rDNSRuleConfig\x122\n" +
"\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" +
"\x05qtype\x18\x02 \x03(\x05R\x05qtype\x127\n" +
"\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\"\x8d\x01\n" +
"\x06Config\x12\x1d\n" +
"\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" +
"\x06Config\x121\n" +
"\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\n" +
"\n" +
"user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" +
"\x04rule\x18\x02 \x03(\v2\x1d.xray.proxy.dns.DNSRuleConfigR\x04rule\x121\n" +
"\x06server\x18\x03 \x01(\v2\x19.xray.common.net.EndpointR\x06server*:\n" +
"\n" +
"RuleAction\x12\n" +
"\n" +
"\x06Direct\x10\x00\x12\b\n" +
"\x04Drop\x10\x01\x12\n" +
"\n" +
"\x06Reject\x10\x02\x12\n" +
"\n" +
"\x06Hijack\x10\x03BL\n" +
"user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" +
"\fnon_IP_query\x18\x03 \x01(\tR\n" +
"nonIPQuery\x12\x1f\n" +
"\vblock_types\x18\x04 \x03(\x05R\n" +
"blockTypesBL\n" +
"\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3"
var (
@@ -232,25 +119,18 @@ func file_proxy_dns_config_proto_rawDescGZIP() []byte {
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, 2)
var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proxy_dns_config_proto_goTypes = []any{
(RuleAction)(0), // 0: xray.proxy.dns.RuleAction
(*DNSRuleConfig)(nil), // 1: xray.proxy.dns.DNSRuleConfig
(*Config)(nil), // 2: xray.proxy.dns.Config
(*geodata.DomainRule)(nil), // 3: xray.common.geodata.DomainRule
(*net.Endpoint)(nil), // 4: xray.common.net.Endpoint
(*Config)(nil), // 0: xray.proxy.dns.Config
(*net.Endpoint)(nil), // 1: xray.common.net.Endpoint
}
var file_proxy_dns_config_proto_depIdxs = []int32{
0, // 0: xray.proxy.dns.DNSRuleConfig.action:type_name -> xray.proxy.dns.RuleAction
3, // 1: xray.proxy.dns.DNSRuleConfig.domain:type_name -> xray.common.geodata.DomainRule
1, // 2: xray.proxy.dns.Config.rule:type_name -> xray.proxy.dns.DNSRuleConfig
4, // 3: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proxy_dns_config_proto_init() }
@@ -263,14 +143,13 @@ func file_proxy_dns_config_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)),
NumEnums: 1,
NumMessages: 2,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proxy_dns_config_proto_goTypes,
DependencyIndexes: file_proxy_dns_config_proto_depIdxs,
EnumInfos: file_proxy_dns_config_proto_enumTypes,
MessageInfos: file_proxy_dns_config_proto_msgTypes,
}.Build()
File_proxy_dns_config_proto = out.File
+6 -17
View File
@@ -7,23 +7,12 @@ option java_package = "com.xray.proxy.dns";
option java_multiple_files = true;
import "common/net/destination.proto";
import "common/geodata/geodat.proto";
enum RuleAction {
Direct = 0;
Drop = 1;
Reject = 2;
Hijack = 3;
}
message DNSRuleConfig {
RuleAction action = 1;
repeated int32 qtype = 2;
repeated xray.common.geodata.DomainRule domain = 3;
}
message Config {
uint32 user_level = 1;
repeated DNSRuleConfig rule = 2;
xray.common.net.Endpoint server = 3;
// Server is the DNS server address. If specified, this address overrides the
// original one.
xray.common.net.Endpoint server = 1;
uint32 user_level = 2;
string non_IP_query = 3;
repeated int32 block_types = 4;
}
+48 -96
View File
@@ -11,7 +11,6 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
dns_proto "github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session"
@@ -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 {
IsOwnLink(ctx context.Context) bool
}
@@ -76,7 +50,8 @@ type Handler struct {
ownLinkVerifier ownLinkVerifier
server net.Destination
timeout time.Duration
rules []*DNSRule
nonIPQuery string
blockTypes []int32
}
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 {
h.server = config.Server.AsDestination()
}
h.rules = make([]*DNSRule, 0, len(config.Rule))
for _, r := range config.Rule {
rule := &DNSRule{
action: r.Action,
qTypes: make([]uint16, 0, len(r.Qtype)),
}
for _, t := range r.Qtype {
rule.qTypes = append(rule.qTypes, uint16(t))
}
if len(r.Domain) > 0 {
m, err := geodata.DomainReg.BuildDomainMatcher(r.Domain)
if err != nil {
return err
}
rule.domains = m
}
h.rules = append(h.rules, rule)
h.nonIPQuery = config.Non_IPQuery
if h.nonIPQuery == "" {
h.nonIPQuery = "reject"
}
h.blockTypes = config.BlockTypes
return nil
}
@@ -117,36 +77,28 @@ func (h *Handler) isOwnLink(ctx context.Context) bool {
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
header, err := parser.Start(b)
if err != nil {
errors.LogInfoInner(context.Background(), err, "parser start")
return
}
id = header.ID
q, err := parser.Question()
if err != nil {
errors.LogInfoInner(context.Background(), err, "question")
return
}
qType = q.Type
domain = q.Name.String()
ok = true
return
}
qType = q.Type
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
return
}
func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction {
qCode := uint16(qType)
for _, r := range h.rules {
if r.Apply(qCode, domain) {
return r.action
}
}
if qType == dnsmessage.TypeA || qType == dnsmessage.TypeAAAA {
return RuleAction_Hijack
}
return RuleAction_Reject
r = true
return
}
// Process implements proxy.Outbound.
@@ -231,51 +183,51 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.
if err == io.EOF {
return nil
}
if err != nil {
return err
}
timer.Update()
if h.isOwnLink(ctx) {
if err := connWriter.WriteMessage(b); err != nil {
return err
if !h.isOwnLink(ctx) {
isIPQuery, domain, id, qType := parseIPQuery(b.Bytes())
if len(h.blockTypes) > 0 {
for _, blocktype := range h.blockTypes {
if blocktype == int32(qType) {
b.Release()
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
if h.nonIPQuery == "reject" {
err := h.rejectNonIPQuery(id, qType, domain, writer)
if err != nil {
return err
}
}
return nil
}
}
}
continue
}
id, qType, domain, ok := parseQuery(b.Bytes())
if !ok {
b.Release()
continue
}
switch h.applyRules(qType, domain) {
case RuleAction_Drop:
b.Release()
errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain)
case RuleAction_Reject:
b.Release()
errors.LogInfo(ctx, "rejected type ", qType, " query for domain ", domain)
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
return err
if isIPQuery {
b.Release()
go h.handleIPQuery(id, qType, domain, writer, timer)
continue
}
case RuleAction_Hijack:
b.Release()
if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA {
errors.LogError(ctx, "can only hijack A/AAAA records")
if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil {
if h.nonIPQuery == "drop" {
b.Release()
continue
}
if h.nonIPQuery == "reject" {
b.Release()
err := h.rejectNonIPQuery(id, qType, domain, writer)
if err != nil {
return err
}
} else {
go h.handleIPQuery(id, qType, domain, writer, timer)
continue
}
case RuleAction_Direct:
if err := connWriter.WriteMessage(b); err != nil {
return err
}
default:
panic("unknown rule action")
}
if err := connWriter.WriteMessage(b); err != nil {
return err
}
}
}
-124
View File
@@ -14,7 +14,6 @@ import (
_ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
@@ -369,126 +368,3 @@ func TestUDP2TCPDNSTunnel(t *testing.T) {
t.Error(r)
}
}
func TestDNSRules(t *testing.T) {
port := udp.PickPort()
dnsServer := dns.Server{
Addr: "127.0.0.1:" + port.String(),
Net: "udp",
Handler: &staticHandler{},
}
defer dnsServer.Shutdown()
go dnsServer.ListenAndServe()
time.Sleep(time.Second)
serverPort := udp.PickPort()
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dnsapp.Config{
NameServer: []*dnsapp.NameServer{
{
Address: &net.Endpoint{
Network: net.Network_UDP,
Address: &net.IPOrDomain{
Address: &net.IPOrDomain_Ip{
Ip: []byte{127, 0, 0, 1},
},
},
Port: uint32(port),
},
},
},
}),
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&policy.Config{}),
},
Inbound: []*core.InboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(port),
Networks: []net.Network{net.Network_UDP},
}),
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{
Rule: []*dns_proxy.DNSRuleConfig{
{
Qtype: []int32{int32(dns.TypeA)},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Domain,
Value: "facebook.com",
},
},
},
},
Action: dns_proxy.RuleAction_Direct,
},
{
Qtype: []int32{int32(dns.TypeA)},
Domain: []*geodata.DomainRule{
{
Value: &geodata.DomainRule_Custom{
Custom: &geodata.Domain{
Type: geodata.Domain_Full,
Value: "google.com",
},
},
},
},
Action: dns_proxy.RuleAction_Reject,
},
},
}),
},
},
}
v, err := core.New(config)
common.Must(err)
common.Must(v.Start())
defer v.Close()
{
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = []dns.Question{{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
c := new(dns.Client)
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
common.Must(err)
if in.Rcode != dns.RcodeRefused {
t.Fatal("expected Refused, but got ", in.Rcode)
}
}
{
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = []dns.Question{{Name: "facebook.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}
c := new(dns.Client)
in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort)))
common.Must(err)
if in.Rcode != dns.RcodeSuccess {
t.Fatal("expected Success, but got ", in.Rcode)
}
}
}
+3 -2
View File
@@ -348,19 +348,20 @@ type PacketReader struct {
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
b := buf.New()
b.Resize(0, buf.Size)
for {
b.Resize(0, buf.Size)
n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
if err != nil {
b.Release()
return nil, err
}
b.Resize(0, int32(n))
udpAddr := d.(*net.UDPAddr)
sourceAddr := net.IPAddress(udpAddr.IP)
if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) {
continue
}
b.Resize(0, int32(n))
// if udp dest addr is changed, we are unable to get the correct src addr
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
+78 -14
View File
@@ -4,8 +4,13 @@ import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/retry"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
@@ -27,24 +32,83 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
destination := ob.Target
errors.LogInfo(ctx, "opening connection to ", destination)
content := new(session.Content)
content.SkipDNSResolve = true
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)
input := link.Reader
output := link.Writer
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 {
errors.New(ctx, "failed to process loopback connection").Base(err)
return err
return errors.New("failed to open connection to ", destination).Base(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
}
+2 -2
View File
@@ -78,9 +78,9 @@ func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, erro
var conn net.PacketConn
var err error
if raddr.Addr().Is4() {
conn, err = d.lc.ListenPacket(context.Background(), "udp", "0.0.0.0:0")
conn, err = d.lc.ListenPacket(context.Background(), "udp4", ":0")
} else {
conn, err = d.lc.ListenPacket(context.Background(), "udp", "[::]:0")
conn, err = d.lc.ListenPacket(context.Background(), "udp6", ":0")
}
if err != nil {
return nil, err
+14 -43
View File
@@ -7,7 +7,6 @@ import (
"encoding/base64"
"encoding/json"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
@@ -27,8 +26,6 @@ type task struct {
}
var conns chan *websocket.Conn
var server *http.Server
var mu sync.Mutex
var upgrader = &websocket.Upgrader{
ReadBufferSize: 0,
@@ -39,48 +36,27 @@ var upgrader = &websocket.Upgrader{
},
}
// Used by external projects when using xray as a go module
func Reload() {
func init() {
addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
mu.Lock()
defer mu.Unlock()
if server != nil {
server.Close()
}
if HasBrowserDialer() {
for len(conns) > 0 {
select {
case c := <-conns:
c.Close()
default:
}
}
conns = nil
}
if addr != "" {
token := uuid.New()
csrfToken := token.String()
webpage := bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken))
webpage = bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken))
conns = make(chan *websocket.Conn, 256)
server = &http.Server{
Addr: addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/websocket" {
if r.URL.Query().Get("token") == csrfToken {
if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
conns <- conn
} else {
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error")
}
go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/websocket" {
if r.URL.Query().Get("token") == csrfToken {
if conn, err := upgrader.Upgrade(w, r, nil); err == nil {
conns <- conn
} else {
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error")
}
} else {
w.Header().Set("Access-Control-Allow-Origin", "*");
w.Write(webpage)
}
}),
}
go server.ListenAndServe()
} else {
w.Header().Set("Access-Control-Allow-Origin", "*");
w.Write(webpage)
}
}))
}
}
@@ -218,8 +194,3 @@ func CheckOK(conn *websocket.Conn) error {
return nil
}
func init() {
Reload()
}
+115 -73
View File
@@ -37,11 +37,11 @@ type packet struct {
}
type xdnsConnClient struct {
net.PacketConn
conn net.PacketConn
resolverConns []net.PacketConn
resolverAddrs []*net.UDPAddr
resolverIdx uint32
resolverSend map[string]*atomic.Uint32
resolverSend []atomic.Uint32
clientID []byte
domains []Name
@@ -74,8 +74,9 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
servers = append(servers, parts[1])
}
var resolverConns []net.PacketConn
var resolverAddrs []*net.UDPAddr
var resolverSend = make(map[string]*atomic.Uint32)
var resolverSend []atomic.Uint32
for _, rs := range servers {
h, p, err := net.SplitHostPort(rs)
if err != nil {
@@ -89,16 +90,27 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
if port == 0 {
return nil, errors.New("invalid port")
}
addr := &net.UDPAddr{IP: ip, Port: port}
resolverAddrs = append(resolverAddrs, addr)
resolverSend[addr.String()] = &atomic.Uint32{}
var uc net.PacketConn
if ip.To4() != nil {
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{
PacketConn: raw,
conn: raw,
resolverConns: resolverConns,
resolverAddrs: resolverAddrs,
resolverIdx: 0,
resolverSend: resolverSend,
clientID: make([]byte, 8),
@@ -118,68 +130,70 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
}
func (c *xdnsConnClient) recvLoop() {
var buf [finalmask.UDPSize]byte
var wg sync.WaitGroup
for {
if c.closed {
break
}
for i, rc := range c.resolverConns {
wg.Add(1)
go func() {
defer wg.Done()
n, addr, err := c.PacketConn.ReadFrom(buf[:])
if err != nil {
if go_errors.Is(err, net.ErrClosed) {
break
var buf [finalmask.UDPSize]byte
for {
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")
close(c.pollChan)
@@ -240,15 +254,15 @@ func (c *xdnsConnClient) sendLoop() {
}
cur := c.resolverIdx
curSend := c.resolverSend[c.resolverAddrs[cur].String()].Add(1)
_, _ = c.PacketConn.WriteTo(p.p, c.resolverAddrs[cur])
curSend := c.resolverSend[c.resolverIdx].Add(1)
_, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx])
for {
c.resolverIdx += 1
c.resolverIdx %= uint32(len(c.resolverAddrs))
c.resolverIdx %= uint32(len(c.resolverConns))
if c.resolverIdx == cur {
break
}
if c.resolverSend[c.resolverAddrs[c.resolverIdx].String()].Load() < curSend {
if c.resolverSend[c.resolverIdx].Load() < curSend {
break
}
}
@@ -276,7 +290,7 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
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 {
errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p))
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 {
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) {
+12 -8
View File
@@ -2,23 +2,27 @@ package xdns
import (
"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) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
// _, ok1 := raw.(*internet.FakePacketConn)
// _, ok2 := raw.(*udphop.UdpHopPacketConn)
// if level != 0 || ok1 || ok2 {
// return nil, errors.New("xdns requires being at the outermost level")
// }
_, ok1 := raw.(*internet.FakePacketConn)
_, ok2 := raw.(*udphop.UdpHopPacketConn)
if level != 0 || ok1 || ok2 {
return nil, errors.New("xdns requires being at the outermost level")
}
return NewConnClient(c, raw)
}
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
// if level != 0 {
// return nil, errors.New("xdns requires being at the outermost level")
// }
if level != 0 {
return nil, errors.New("xdns requires being at the outermost level")
}
return NewConnServer(c, raw)
}
-12
View File
@@ -373,12 +373,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
if requestURL.Host == "" {
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.RawQuery = transportConfiguration.GetNormalizedQuery()
@@ -440,12 +434,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
if requestURL2.Host == "" {
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.RawQuery = config2.GetNormalizedQuery()
httpClient2, xmuxClient2 = getHTTPClient(ctx, dest2, memory2)
+15 -10
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/tls"
"math/big"
"slices"
"time"
utls "github.com/refraction-networking/utls"
@@ -90,18 +91,24 @@ func (c *UConn) HandshakeContextServerName(ctx context.Context) string {
return c.ConnectionState().ServerName
}
// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send
// http/1.1 in its ALPN.
// WebsocketHandshakeContext basically calls UConn.Handshake inside it but it will try
// 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 {
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
// fingerprint in the UConn
if err := c.BuildHandshakeState(); err != nil {
return err
}
config := *utils.AccessField[*utls.Config](c, "config")
// Do not modify outer ALPN to http/1.1 if ECH is used
// Outer ALPN will be h2,http/1.1, and real ALPN in config will be hidden in ECH
// Do not modify outer ALPN if ECH is used
// Outer ALPN will be h2,http/1.1, and real http/1.1 in config will be hidden in ECH
if config.EncryptedClientHelloConfigList != nil {
config.NextProtos = []string{"http/1.1"}
return c.HandshakeContext(ctx)
}
// 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 {
if alpn, ok := extension.(*utls.ALPNExtension); ok {
hasALPNExtension = true
alpn.AlpnProtocols = []string{"http/1.1"}
alpn.AlpnProtocols = ALPN
break
}
}
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
if err := c.BuildHandshakeState(); err != nil {
@@ -146,9 +153,7 @@ func copyConfig(c *tls.Config) *utls.Config {
VerifyPeerCertificate: c.VerifyPeerCertificate,
KeyLogWriter: c.KeyLogWriter,
EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList,
}
if config.EncryptedClientHelloConfigList != nil {
config.NextProtos = c.NextProtos
NextProtos: c.NextProtos,
}
return config
}
+6 -19
View File
@@ -111,20 +111,13 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
}
}
if browser_dialer.HasBrowserDialer() {
// For Browser Dialer's optimized IP and non-standard port
host := wsSettings.Host
if host == "" && tConfig.ServerName != "" {
host = tConfig.ServerName
}
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()
host := dest.NetAddr()
if (protocol == "ws" && dest.Port == 80) || (protocol == "wss" && dest.Port == 443) {
host = dest.Address.String()
}
uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
if browser_dialer.HasBrowserDialer() {
conn, err := browser_dialer.DialWS(uri, ed)
if err != nil {
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
}
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()
// See dialer.DialContext()
header.Set("Host", wsSettings.Host)